Skip to content

Commit 53727c7

Browse files
fix: substitute YYYY-MM-DD at session startup and post-compaction (#32363) (#32381)
Merged via squash. Prepared head SHA: aee998a Co-authored-by: chengzhichao-xydt <[email protected]> Co-authored-by: jalehman <[email protected]> Reviewed-by: @jalehman
1 parent 3fe4c19 commit 53727c7

File tree

10 files changed

+140
-13
lines changed

10 files changed

+140
-13
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
1717
- 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.
1818
- 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.
1919
- 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.
20+
- 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.
2021
- 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.
2122
- 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.
2223
- 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.

src/auto-reply/reply/agent-runner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -666,7 +666,7 @@ export async function runReplyAgent(params: {
666666
// Inject post-compaction workspace context for the next agent turn
667667
if (sessionKey) {
668668
const workspaceDir = process.cwd();
669-
readPostCompactionContext(workspaceDir)
669+
readPostCompactionContext(workspaceDir, cfg)
670670
.then((contextContent) => {
671671
if (contextContent) {
672672
enqueueSystemEvent(contextContent, { sessionKey });

src/auto-reply/reply/get-reply-run.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import type { createModelSelectionState } from "./model-selection.js";
4343
import { resolveOriginMessageProvider } from "./origin-routing.js";
4444
import { resolveQueueSettings } from "./queue.js";
4545
import { routeReply } from "./route-reply.js";
46-
import { BARE_SESSION_RESET_PROMPT } from "./session-reset-prompt.js";
46+
import { buildBareSessionResetPrompt } from "./session-reset-prompt.js";
4747
import { buildQueuedSystemPrompt, ensureSkillSnapshot } from "./session-updates.js";
4848
import { resolveTypingMode } from "./typing-mode.js";
4949
import { resolveRunTypingPolicy } from "./typing-policy.js";
@@ -290,7 +290,7 @@ export async function runPreparedReply(
290290
const isBareSessionReset =
291291
isNewSession &&
292292
((baseBodyTrimmedRaw.length === 0 && rawBodyTrimmed.length > 0) || isBareNewOrReset);
293-
const baseBodyFinal = isBareSessionReset ? BARE_SESSION_RESET_PROMPT : baseBody;
293+
const baseBodyFinal = isBareSessionReset ? buildBareSessionResetPrompt(cfg) : baseBody;
294294
const inboundUserContext = buildInboundUserContextPrefix(
295295
isNewSession
296296
? {

src/auto-reply/reply/post-compaction-context.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from "node:fs";
22
import path from "node:path";
33
import { describe, it, expect, beforeEach, afterEach } from "vitest";
4+
import type { OpenClawConfig } from "../../config/config.js";
45
import { readPostCompactionContext } from "./post-compaction-context.js";
56

67
describe("readPostCompactionContext", () => {
@@ -190,4 +191,39 @@ Never do Y.
190191
expect(result).toBeNull();
191192
},
192193
);
194+
195+
it("substitutes YYYY-MM-DD with the actual date in extracted sections", async () => {
196+
const content = `## Session Startup
197+
198+
Read memory/YYYY-MM-DD.md and memory/yesterday.md.
199+
200+
## Red Lines
201+
202+
Never modify memory/YYYY-MM-DD.md destructively.
203+
`;
204+
fs.writeFileSync(path.join(tmpDir, "AGENTS.md"), content);
205+
const cfg = {
206+
agents: { defaults: { userTimezone: "America/New_York" } },
207+
} as OpenClawConfig;
208+
// 2026-03-03 14:00 UTC = 2026-03-03 09:00 EST
209+
const nowMs = Date.UTC(2026, 2, 3, 14, 0, 0);
210+
const result = await readPostCompactionContext(tmpDir, cfg, nowMs);
211+
expect(result).not.toBeNull();
212+
expect(result).toContain("memory/2026-03-03.md");
213+
expect(result).not.toContain("memory/YYYY-MM-DD.md");
214+
expect(result).toContain("Current time:");
215+
expect(result).toContain("America/New_York");
216+
});
217+
218+
it("appends current time line even when no YYYY-MM-DD placeholder is present", async () => {
219+
const content = `## Session Startup
220+
221+
Read WORKFLOW.md on startup.
222+
`;
223+
fs.writeFileSync(path.join(tmpDir, "AGENTS.md"), content);
224+
const nowMs = Date.UTC(2026, 2, 3, 14, 0, 0);
225+
const result = await readPostCompactionContext(tmpDir, undefined, nowMs);
226+
expect(result).not.toBeNull();
227+
expect(result).toContain("Current time:");
228+
});
193229
});

src/auto-reply/reply/post-compaction-context.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,39 @@
11
import fs from "node:fs";
22
import path from "node:path";
3+
import { resolveCronStyleNow } from "../../agents/current-time.js";
4+
import { resolveUserTimezone } from "../../agents/date-time.js";
5+
import type { OpenClawConfig } from "../../config/config.js";
36
import { openBoundaryFile } from "../../infra/boundary-file-read.js";
47

58
const MAX_CONTEXT_CHARS = 3000;
69

10+
function formatDateStamp(nowMs: number, timezone: string): string {
11+
const parts = new Intl.DateTimeFormat("en-US", {
12+
timeZone: timezone,
13+
year: "numeric",
14+
month: "2-digit",
15+
day: "2-digit",
16+
}).formatToParts(new Date(nowMs));
17+
const year = parts.find((p) => p.type === "year")?.value;
18+
const month = parts.find((p) => p.type === "month")?.value;
19+
const day = parts.find((p) => p.type === "day")?.value;
20+
if (year && month && day) {
21+
return `${year}-${month}-${day}`;
22+
}
23+
return new Date(nowMs).toISOString().slice(0, 10);
24+
}
25+
726
/**
827
* Read critical sections from workspace AGENTS.md for post-compaction injection.
928
* Returns formatted system event text, or null if no AGENTS.md or no relevant sections.
29+
* Substitutes YYYY-MM-DD placeholders with the real date so agents read the correct
30+
* daily memory files instead of guessing based on training cutoff.
1031
*/
11-
export async function readPostCompactionContext(workspaceDir: string): Promise<string | null> {
32+
export async function readPostCompactionContext(
33+
workspaceDir: string,
34+
cfg?: OpenClawConfig,
35+
nowMs?: number,
36+
): Promise<string | null> {
1237
const agentsPath = path.join(workspaceDir, "AGENTS.md");
1338

1439
try {
@@ -36,7 +61,14 @@ export async function readPostCompactionContext(workspaceDir: string): Promise<s
3661
return null;
3762
}
3863

39-
const combined = sections.join("\n\n");
64+
const resolvedNowMs = nowMs ?? Date.now();
65+
const timezone = resolveUserTimezone(cfg?.agents?.defaults?.userTimezone);
66+
const dateStamp = formatDateStamp(resolvedNowMs, timezone);
67+
// Always append the real runtime timestamp — AGENTS.md content may itself contain
68+
// "Current time:" as user-authored text, so we must not gate on that substring.
69+
const { timeLine } = resolveCronStyleNow(cfg ?? {}, resolvedNowMs);
70+
71+
const combined = sections.join("\n\n").replaceAll("YYYY-MM-DD", dateStamp);
4072
const safeContent =
4173
combined.length > MAX_CONTEXT_CHARS
4274
? combined.slice(0, MAX_CONTEXT_CHARS) + "\n...[truncated]..."
@@ -46,8 +78,7 @@ export async function readPostCompactionContext(workspaceDir: string): Promise<s
4678
"[Post-compaction context refresh]\n\n" +
4779
"Session was just compacted. The conversation summary above is a hint, NOT a substitute for your startup sequence. " +
4880
"Execute your Session Startup sequence now — read the required files before responding to the user.\n\n" +
49-
"Critical rules from AGENTS.md:\n\n" +
50-
safeContent
81+
`Critical rules from AGENTS.md:\n\n${safeContent}\n\n${timeLine}`
5182
);
5283
} catch {
5384
return null;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { describe, it, expect } from "vitest";
2+
import type { OpenClawConfig } from "../../config/config.js";
3+
import { buildBareSessionResetPrompt } from "./session-reset-prompt.js";
4+
5+
describe("buildBareSessionResetPrompt", () => {
6+
it("includes the core session startup instruction", () => {
7+
const prompt = buildBareSessionResetPrompt();
8+
expect(prompt).toContain("Execute your Session Startup sequence now");
9+
expect(prompt).toContain("read the required files before responding to the user");
10+
});
11+
12+
it("appends current time line so agents know the date", () => {
13+
const cfg = {
14+
agents: { defaults: { userTimezone: "America/New_York" } },
15+
} as OpenClawConfig;
16+
// 2026-03-03 14:00 UTC = 2026-03-03 09:00 EST
17+
const nowMs = Date.UTC(2026, 2, 3, 14, 0, 0);
18+
const prompt = buildBareSessionResetPrompt(cfg, nowMs);
19+
expect(prompt).toContain("Current time:");
20+
expect(prompt).toContain("America/New_York");
21+
});
22+
23+
it("does not append a duplicate current time line", () => {
24+
const nowMs = Date.UTC(2026, 2, 3, 14, 0, 0);
25+
const prompt = buildBareSessionResetPrompt(undefined, nowMs);
26+
expect((prompt.match(/Current time:/g) ?? []).length).toBe(1);
27+
});
28+
29+
it("falls back to UTC when no timezone configured", () => {
30+
const nowMs = Date.UTC(2026, 2, 3, 14, 0, 0);
31+
const prompt = buildBareSessionResetPrompt(undefined, nowMs);
32+
expect(prompt).toContain("Current time:");
33+
});
34+
});
Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,21 @@
1-
export const BARE_SESSION_RESET_PROMPT =
1+
import { appendCronStyleCurrentTimeLine } from "../../agents/current-time.js";
2+
import type { OpenClawConfig } from "../../config/config.js";
3+
4+
const BARE_SESSION_RESET_PROMPT_BASE =
25
"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.";
6+
7+
/**
8+
* Build the bare session reset prompt, appending the current date/time so agents
9+
* know which daily memory files to read during their Session Startup sequence.
10+
* Without this, agents on /new or /reset guess the date from their training cutoff.
11+
*/
12+
export function buildBareSessionResetPrompt(cfg?: OpenClawConfig, nowMs?: number): string {
13+
return appendCronStyleCurrentTimeLine(
14+
BARE_SESSION_RESET_PROMPT_BASE,
15+
cfg ?? {},
16+
nowMs ?? Date.now(),
17+
);
18+
}
19+
20+
/** @deprecated Use buildBareSessionResetPrompt(cfg) instead */
21+
export const BARE_SESSION_RESET_PROMPT = BARE_SESSION_RESET_PROMPT_BASE;

src/gateway/server-methods/agent.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -525,8 +525,13 @@ describe("gateway agent handler", () => {
525525
{ reqId: "4" },
526526
);
527527

528-
const call = await expectResetCall(BARE_SESSION_RESET_PROMPT);
528+
await vi.waitFor(() => expect(mocks.agentCommand).toHaveBeenCalled());
529+
expect(mocks.sessionsResetHandler).toHaveBeenCalledTimes(1);
530+
const call = readLastAgentCommandCall();
531+
// Message is now dynamically built with current date — check key substrings
529532
expect(call?.message).toContain("Execute your Session Startup sequence now");
533+
expect(call?.message).toContain("Current time:");
534+
expect(call?.message).not.toBe(BARE_SESSION_RESET_PROMPT);
530535
expect(call?.sessionId).toBe("reset-session-id");
531536
});
532537

src/gateway/server-methods/agent.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { randomUUID } from "node:crypto";
22
import { listAgentIds } from "../../agents/agent-scope.js";
33
import type { AgentInternalEvent } from "../../agents/internal-events.js";
4-
import { BARE_SESSION_RESET_PROMPT } from "../../auto-reply/reply/session-reset-prompt.js";
4+
import { buildBareSessionResetPrompt } from "../../auto-reply/reply/session-reset-prompt.js";
55
import { agentCommandFromIngress } from "../../commands/agent.js";
66
import { loadConfig } from "../../config/config.js";
77
import {
@@ -351,7 +351,9 @@ export const agentHandlers: GatewayRequestHandlers = {
351351
} else {
352352
// Keep bare /new and /reset behavior aligned with chat.send:
353353
// reset first, then run a fresh-session greeting prompt in-place.
354-
message = BARE_SESSION_RESET_PROMPT;
354+
// Date is embedded in the prompt so agents read the correct daily
355+
// memory files; skip further timestamp injection to avoid duplication.
356+
message = buildBareSessionResetPrompt(cfg);
355357
skipTimestampInjection = true;
356358
}
357359
}

src/gateway/server.agent.gateway-server-agent-b.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import path from "node:path";
44
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
55
import { WebSocket } from "ws";
66
import { whatsappPlugin } from "../../extensions/whatsapp/src/channel.js";
7-
import { BARE_SESSION_RESET_PROMPT } from "../auto-reply/reply/session-reset-prompt.js";
87
import type { ChannelPlugin } from "../channels/plugins/types.js";
98
import { emitAgentEvent, registerAgentRunContext } from "../infra/agent-events.js";
109
import { setRegistry } from "./server.agent.gateway-server-agent.mocks.js";
@@ -287,9 +286,9 @@ describe("gateway server agent", () => {
287286

288287
await vi.waitFor(() => expect(calls.length).toBeGreaterThan(callsBefore));
289288
const call = (calls.at(-1)?.[0] ?? {}) as Record<string, unknown>;
290-
expect(call.message).toBe(BARE_SESSION_RESET_PROMPT);
291289
expect(call.message).toBeTypeOf("string");
292290
expect(call.message).toContain("Execute your Session Startup sequence now");
291+
expect(call.message).toContain("Current time:");
293292
expect(typeof call.sessionId).toBe("string");
294293
expect(call.sessionId).not.toBe("sess-main-before-reset");
295294
});

0 commit comments

Comments
 (0)