fix: substitute YYYY-MM-DD at session startup and post-compaction (#32363) (#32381)

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:
chengzhichao-xydt
2026-03-03 22:21:26 +08:00
committed by GitHub
parent 3fe4c19305
commit 53727c72f4
10 changed files with 140 additions and 13 deletions

View File

@@ -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 });

View File

@@ -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
? {

View File

@@ -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:");
});
});

View File

@@ -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;

View 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:");
});
});

View File

@@ -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;