mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
Merged via squash.
Prepared head SHA: aee998a2c1
Co-authored-by: chengzhichao-xydt <264300353+chengzhichao-xydt@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
committed by
GitHub
parent
3fe4c19305
commit
53727c72f4
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Telegram/multi-account default routing clarity: warn only for ambiguous (2+) account setups without an explicit default, add `openclaw doctor` warnings for missing/invalid multi-account defaults across channels, and document explicit-default guidance for channel routing and Telegram config. (#32544) thanks @Sid-Qin.
|
||||
- Telegram/plugin outbound hook parity: run `message_sending` + `message_sent` in Telegram reply delivery, include reply-path hook metadata (`mediaUrls`, `threadId`), and report `message_sent.success=false` when hooks blank text and no outbound message is delivered. (#32649) Thanks @KimGLee.
|
||||
- Agents/Skills runtime loading: propagate run config into embedded attempt and compaction skill-entry loading so explicitly enabled bundled companion skills are discovered consistently when skill snapshots do not already provide resolved entries. Thanks @gumadeiras.
|
||||
- Agents/Session startup date grounding: substitute `YYYY-MM-DD` placeholders in startup/post-compaction AGENTS context and append runtime current-time lines for `/new` and `/reset` prompts so daily-memory references resolve correctly. (#32381) Thanks @chengzhichao-xydt.
|
||||
- Agents/Compaction continuity: expand staged-summary merge instructions to preserve active task status, batch progress, latest user request, and follow-up commitments so compaction handoffs retain in-flight work context. (#8903) thanks @joetomasone.
|
||||
- Gateway/status self version reporting: make Gateway self version in `openclaw status` prefer runtime `VERSION` (while preserving explicit `OPENCLAW_VERSION` override), preventing stale post-upgrade app version output. (#32655) thanks @liuxiaopai-ai.
|
||||
- Memory/QMD index isolation: set `QMD_CONFIG_DIR` alongside `XDG_CONFIG_HOME` so QMD config state stays per-agent despite upstream XDG handling bugs, preventing cross-agent collection indexing and excess disk/CPU usage. (#27028) thanks @HenryLoenwind.
|
||||
|
||||
@@ -666,7 +666,7 @@ export async function runReplyAgent(params: {
|
||||
// Inject post-compaction workspace context for the next agent turn
|
||||
if (sessionKey) {
|
||||
const workspaceDir = process.cwd();
|
||||
readPostCompactionContext(workspaceDir)
|
||||
readPostCompactionContext(workspaceDir, cfg)
|
||||
.then((contextContent) => {
|
||||
if (contextContent) {
|
||||
enqueueSystemEvent(contextContent, { sessionKey });
|
||||
|
||||
@@ -43,7 +43,7 @@ import type { createModelSelectionState } from "./model-selection.js";
|
||||
import { resolveOriginMessageProvider } from "./origin-routing.js";
|
||||
import { resolveQueueSettings } from "./queue.js";
|
||||
import { routeReply } from "./route-reply.js";
|
||||
import { BARE_SESSION_RESET_PROMPT } from "./session-reset-prompt.js";
|
||||
import { buildBareSessionResetPrompt } from "./session-reset-prompt.js";
|
||||
import { buildQueuedSystemPrompt, ensureSkillSnapshot } from "./session-updates.js";
|
||||
import { resolveTypingMode } from "./typing-mode.js";
|
||||
import { resolveRunTypingPolicy } from "./typing-policy.js";
|
||||
@@ -290,7 +290,7 @@ export async function runPreparedReply(
|
||||
const isBareSessionReset =
|
||||
isNewSession &&
|
||||
((baseBodyTrimmedRaw.length === 0 && rawBodyTrimmed.length > 0) || isBareNewOrReset);
|
||||
const baseBodyFinal = isBareSessionReset ? BARE_SESSION_RESET_PROMPT : baseBody;
|
||||
const baseBodyFinal = isBareSessionReset ? buildBareSessionResetPrompt(cfg) : baseBody;
|
||||
const inboundUserContext = buildInboundUserContextPrefix(
|
||||
isNewSession
|
||||
? {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { readPostCompactionContext } from "./post-compaction-context.js";
|
||||
|
||||
describe("readPostCompactionContext", () => {
|
||||
@@ -190,4 +191,39 @@ Never do Y.
|
||||
expect(result).toBeNull();
|
||||
},
|
||||
);
|
||||
|
||||
it("substitutes YYYY-MM-DD with the actual date in extracted sections", async () => {
|
||||
const content = `## Session Startup
|
||||
|
||||
Read memory/YYYY-MM-DD.md and memory/yesterday.md.
|
||||
|
||||
## Red Lines
|
||||
|
||||
Never modify memory/YYYY-MM-DD.md destructively.
|
||||
`;
|
||||
fs.writeFileSync(path.join(tmpDir, "AGENTS.md"), content);
|
||||
const cfg = {
|
||||
agents: { defaults: { userTimezone: "America/New_York" } },
|
||||
} as OpenClawConfig;
|
||||
// 2026-03-03 14:00 UTC = 2026-03-03 09:00 EST
|
||||
const nowMs = Date.UTC(2026, 2, 3, 14, 0, 0);
|
||||
const result = await readPostCompactionContext(tmpDir, cfg, nowMs);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toContain("memory/2026-03-03.md");
|
||||
expect(result).not.toContain("memory/YYYY-MM-DD.md");
|
||||
expect(result).toContain("Current time:");
|
||||
expect(result).toContain("America/New_York");
|
||||
});
|
||||
|
||||
it("appends current time line even when no YYYY-MM-DD placeholder is present", async () => {
|
||||
const content = `## Session Startup
|
||||
|
||||
Read WORKFLOW.md on startup.
|
||||
`;
|
||||
fs.writeFileSync(path.join(tmpDir, "AGENTS.md"), content);
|
||||
const nowMs = Date.UTC(2026, 2, 3, 14, 0, 0);
|
||||
const result = await readPostCompactionContext(tmpDir, undefined, nowMs);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toContain("Current time:");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,39 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { resolveCronStyleNow } from "../../agents/current-time.js";
|
||||
import { resolveUserTimezone } from "../../agents/date-time.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { openBoundaryFile } from "../../infra/boundary-file-read.js";
|
||||
|
||||
const MAX_CONTEXT_CHARS = 3000;
|
||||
|
||||
function formatDateStamp(nowMs: number, timezone: string): string {
|
||||
const parts = new Intl.DateTimeFormat("en-US", {
|
||||
timeZone: timezone,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
}).formatToParts(new Date(nowMs));
|
||||
const year = parts.find((p) => p.type === "year")?.value;
|
||||
const month = parts.find((p) => p.type === "month")?.value;
|
||||
const day = parts.find((p) => p.type === "day")?.value;
|
||||
if (year && month && day) {
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
return new Date(nowMs).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read critical sections from workspace AGENTS.md for post-compaction injection.
|
||||
* Returns formatted system event text, or null if no AGENTS.md or no relevant sections.
|
||||
* Substitutes YYYY-MM-DD placeholders with the real date so agents read the correct
|
||||
* daily memory files instead of guessing based on training cutoff.
|
||||
*/
|
||||
export async function readPostCompactionContext(workspaceDir: string): Promise<string | null> {
|
||||
export async function readPostCompactionContext(
|
||||
workspaceDir: string,
|
||||
cfg?: OpenClawConfig,
|
||||
nowMs?: number,
|
||||
): Promise<string | null> {
|
||||
const agentsPath = path.join(workspaceDir, "AGENTS.md");
|
||||
|
||||
try {
|
||||
@@ -36,7 +61,14 @@ export async function readPostCompactionContext(workspaceDir: string): Promise<s
|
||||
return null;
|
||||
}
|
||||
|
||||
const combined = sections.join("\n\n");
|
||||
const resolvedNowMs = nowMs ?? Date.now();
|
||||
const timezone = resolveUserTimezone(cfg?.agents?.defaults?.userTimezone);
|
||||
const dateStamp = formatDateStamp(resolvedNowMs, timezone);
|
||||
// Always append the real runtime timestamp — AGENTS.md content may itself contain
|
||||
// "Current time:" as user-authored text, so we must not gate on that substring.
|
||||
const { timeLine } = resolveCronStyleNow(cfg ?? {}, resolvedNowMs);
|
||||
|
||||
const combined = sections.join("\n\n").replaceAll("YYYY-MM-DD", dateStamp);
|
||||
const safeContent =
|
||||
combined.length > MAX_CONTEXT_CHARS
|
||||
? combined.slice(0, MAX_CONTEXT_CHARS) + "\n...[truncated]..."
|
||||
@@ -46,8 +78,7 @@ export async function readPostCompactionContext(workspaceDir: string): Promise<s
|
||||
"[Post-compaction context refresh]\n\n" +
|
||||
"Session was just compacted. The conversation summary above is a hint, NOT a substitute for your startup sequence. " +
|
||||
"Execute your Session Startup sequence now — read the required files before responding to the user.\n\n" +
|
||||
"Critical rules from AGENTS.md:\n\n" +
|
||||
safeContent
|
||||
`Critical rules from AGENTS.md:\n\n${safeContent}\n\n${timeLine}`
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
34
src/auto-reply/reply/session-reset-prompt.test.ts
Normal file
34
src/auto-reply/reply/session-reset-prompt.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { buildBareSessionResetPrompt } from "./session-reset-prompt.js";
|
||||
|
||||
describe("buildBareSessionResetPrompt", () => {
|
||||
it("includes the core session startup instruction", () => {
|
||||
const prompt = buildBareSessionResetPrompt();
|
||||
expect(prompt).toContain("Execute your Session Startup sequence now");
|
||||
expect(prompt).toContain("read the required files before responding to the user");
|
||||
});
|
||||
|
||||
it("appends current time line so agents know the date", () => {
|
||||
const cfg = {
|
||||
agents: { defaults: { userTimezone: "America/New_York" } },
|
||||
} as OpenClawConfig;
|
||||
// 2026-03-03 14:00 UTC = 2026-03-03 09:00 EST
|
||||
const nowMs = Date.UTC(2026, 2, 3, 14, 0, 0);
|
||||
const prompt = buildBareSessionResetPrompt(cfg, nowMs);
|
||||
expect(prompt).toContain("Current time:");
|
||||
expect(prompt).toContain("America/New_York");
|
||||
});
|
||||
|
||||
it("does not append a duplicate current time line", () => {
|
||||
const nowMs = Date.UTC(2026, 2, 3, 14, 0, 0);
|
||||
const prompt = buildBareSessionResetPrompt(undefined, nowMs);
|
||||
expect((prompt.match(/Current time:/g) ?? []).length).toBe(1);
|
||||
});
|
||||
|
||||
it("falls back to UTC when no timezone configured", () => {
|
||||
const nowMs = Date.UTC(2026, 2, 3, 14, 0, 0);
|
||||
const prompt = buildBareSessionResetPrompt(undefined, nowMs);
|
||||
expect(prompt).toContain("Current time:");
|
||||
});
|
||||
});
|
||||
@@ -1,2 +1,21 @@
|
||||
export const BARE_SESSION_RESET_PROMPT =
|
||||
import { appendCronStyleCurrentTimeLine } from "../../agents/current-time.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
|
||||
const BARE_SESSION_RESET_PROMPT_BASE =
|
||||
"A new session was started via /new or /reset. Execute your Session Startup sequence now - read the required files before responding to the user. Then greet the user in your configured persona, if one is provided. Be yourself - use your defined voice, mannerisms, and mood. Keep it to 1-3 sentences and ask what they want to do. If the runtime model differs from default_model in the system prompt, mention the default model. Do not mention internal steps, files, tools, or reasoning.";
|
||||
|
||||
/**
|
||||
* Build the bare session reset prompt, appending the current date/time so agents
|
||||
* know which daily memory files to read during their Session Startup sequence.
|
||||
* Without this, agents on /new or /reset guess the date from their training cutoff.
|
||||
*/
|
||||
export function buildBareSessionResetPrompt(cfg?: OpenClawConfig, nowMs?: number): string {
|
||||
return appendCronStyleCurrentTimeLine(
|
||||
BARE_SESSION_RESET_PROMPT_BASE,
|
||||
cfg ?? {},
|
||||
nowMs ?? Date.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/** @deprecated Use buildBareSessionResetPrompt(cfg) instead */
|
||||
export const BARE_SESSION_RESET_PROMPT = BARE_SESSION_RESET_PROMPT_BASE;
|
||||
|
||||
@@ -525,8 +525,13 @@ describe("gateway agent handler", () => {
|
||||
{ reqId: "4" },
|
||||
);
|
||||
|
||||
const call = await expectResetCall(BARE_SESSION_RESET_PROMPT);
|
||||
await vi.waitFor(() => expect(mocks.agentCommand).toHaveBeenCalled());
|
||||
expect(mocks.sessionsResetHandler).toHaveBeenCalledTimes(1);
|
||||
const call = readLastAgentCommandCall();
|
||||
// Message is now dynamically built with current date — check key substrings
|
||||
expect(call?.message).toContain("Execute your Session Startup sequence now");
|
||||
expect(call?.message).toContain("Current time:");
|
||||
expect(call?.message).not.toBe(BARE_SESSION_RESET_PROMPT);
|
||||
expect(call?.sessionId).toBe("reset-session-id");
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { listAgentIds } from "../../agents/agent-scope.js";
|
||||
import type { AgentInternalEvent } from "../../agents/internal-events.js";
|
||||
import { BARE_SESSION_RESET_PROMPT } from "../../auto-reply/reply/session-reset-prompt.js";
|
||||
import { buildBareSessionResetPrompt } from "../../auto-reply/reply/session-reset-prompt.js";
|
||||
import { agentCommandFromIngress } from "../../commands/agent.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import {
|
||||
@@ -351,7 +351,9 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
} else {
|
||||
// Keep bare /new and /reset behavior aligned with chat.send:
|
||||
// reset first, then run a fresh-session greeting prompt in-place.
|
||||
message = BARE_SESSION_RESET_PROMPT;
|
||||
// Date is embedded in the prompt so agents read the correct daily
|
||||
// memory files; skip further timestamp injection to avoid duplication.
|
||||
message = buildBareSessionResetPrompt(cfg);
|
||||
skipTimestampInjection = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import path from "node:path";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { WebSocket } from "ws";
|
||||
import { whatsappPlugin } from "../../extensions/whatsapp/src/channel.js";
|
||||
import { BARE_SESSION_RESET_PROMPT } from "../auto-reply/reply/session-reset-prompt.js";
|
||||
import type { ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import { emitAgentEvent, registerAgentRunContext } from "../infra/agent-events.js";
|
||||
import { setRegistry } from "./server.agent.gateway-server-agent.mocks.js";
|
||||
@@ -287,9 +286,9 @@ describe("gateway server agent", () => {
|
||||
|
||||
await vi.waitFor(() => expect(calls.length).toBeGreaterThan(callsBefore));
|
||||
const call = (calls.at(-1)?.[0] ?? {}) as Record<string, unknown>;
|
||||
expect(call.message).toBe(BARE_SESSION_RESET_PROMPT);
|
||||
expect(call.message).toBeTypeOf("string");
|
||||
expect(call.message).toContain("Execute your Session Startup sequence now");
|
||||
expect(call.message).toContain("Current time:");
|
||||
expect(typeof call.sessionId).toBe("string");
|
||||
expect(call.sessionId).not.toBe("sess-main-before-reset");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user