From 53727c72f4d22e39b5eba07efc78e66344605c2a Mon Sep 17 00:00:00 2001 From: chengzhichao-xydt Date: Tue, 3 Mar 2026 22:21:26 +0800 Subject: [PATCH] fix: substitute YYYY-MM-DD at session startup and post-compaction (#32363) (#32381) Merged via squash. Prepared head SHA: aee998a2c1a911d3fef771aa891ac315a2f7dc53 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 --- CHANGELOG.md | 1 + src/auto-reply/reply/agent-runner.ts | 2 +- src/auto-reply/reply/get-reply-run.ts | 4 +- .../reply/post-compaction-context.test.ts | 36 +++++++++++++++++ .../reply/post-compaction-context.ts | 39 +++++++++++++++++-- .../reply/session-reset-prompt.test.ts | 34 ++++++++++++++++ src/auto-reply/reply/session-reset-prompt.ts | 21 +++++++++- src/gateway/server-methods/agent.test.ts | 7 +++- src/gateway/server-methods/agent.ts | 6 ++- ...erver.agent.gateway-server-agent-b.test.ts | 3 +- 10 files changed, 140 insertions(+), 13 deletions(-) create mode 100644 src/auto-reply/reply/session-reset-prompt.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b975c2882f..f5707d937de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/auto-reply/reply/agent-runner.ts b/src/auto-reply/reply/agent-runner.ts index 5896bf1c163..8b126382dbc 100644 --- a/src/auto-reply/reply/agent-runner.ts +++ b/src/auto-reply/reply/agent-runner.ts @@ -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 }); diff --git a/src/auto-reply/reply/get-reply-run.ts b/src/auto-reply/reply/get-reply-run.ts index 3c46987566a..46f082f26f9 100644 --- a/src/auto-reply/reply/get-reply-run.ts +++ b/src/auto-reply/reply/get-reply-run.ts @@ -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 ? { diff --git a/src/auto-reply/reply/post-compaction-context.test.ts b/src/auto-reply/reply/post-compaction-context.test.ts index 7adb4610619..6e889ade215 100644 --- a/src/auto-reply/reply/post-compaction-context.test.ts +++ b/src/auto-reply/reply/post-compaction-context.test.ts @@ -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:"); + }); }); diff --git a/src/auto-reply/reply/post-compaction-context.ts b/src/auto-reply/reply/post-compaction-context.ts index 7f627d1d153..9c39304369d 100644 --- a/src/auto-reply/reply/post-compaction-context.ts +++ b/src/auto-reply/reply/post-compaction-context.ts @@ -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 { +export async function readPostCompactionContext( + workspaceDir: string, + cfg?: OpenClawConfig, + nowMs?: number, +): Promise { const agentsPath = path.join(workspaceDir, "AGENTS.md"); try { @@ -36,7 +61,14 @@ export async function readPostCompactionContext(workspaceDir: string): Promise MAX_CONTEXT_CHARS ? combined.slice(0, MAX_CONTEXT_CHARS) + "\n...[truncated]..." @@ -46,8 +78,7 @@ export async function readPostCompactionContext(workspaceDir: string): Promise { + 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:"); + }); +}); diff --git a/src/auto-reply/reply/session-reset-prompt.ts b/src/auto-reply/reply/session-reset-prompt.ts index 6a98cd42633..67e693f70b1 100644 --- a/src/auto-reply/reply/session-reset-prompt.ts +++ b/src/auto-reply/reply/session-reset-prompt.ts @@ -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; diff --git a/src/gateway/server-methods/agent.test.ts b/src/gateway/server-methods/agent.test.ts index 8375a49bbc3..35d547e71c9 100644 --- a/src/gateway/server-methods/agent.test.ts +++ b/src/gateway/server-methods/agent.test.ts @@ -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"); }); diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index d45fddb05f9..41228b4ffae 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -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; } } diff --git a/src/gateway/server.agent.gateway-server-agent-b.test.ts b/src/gateway/server.agent.gateway-server-agent-b.test.ts index dad0055ece1..476b3a0ba8f 100644 --- a/src/gateway/server.agent.gateway-server-agent-b.test.ts +++ b/src/gateway/server.agent.gateway-server-agent-b.test.ts @@ -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; - 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"); });