fix(coding-agent): use local date in system prompt
Signed-off-by: Christian Stewart <christian@aperture.us>
This commit is contained in:
@@ -314,6 +314,7 @@ import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
||||
import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
|
||||
import { normalizeModelContextImages } from "../utils/image-loading";
|
||||
import { describeAttachedImagesForTextModel } from "../utils/image-vision-fallback";
|
||||
import { formatLocalCalendarDate } from "../utils/local-date";
|
||||
import { generateSessionTitle } from "../utils/title-generator";
|
||||
import { buildNamedToolChoice, isToolChoiceActive } from "../utils/tool-choice";
|
||||
import type { AuthStorage } from "./auth-storage";
|
||||
@@ -6517,7 +6518,7 @@ export class AgentSession {
|
||||
entries.sort();
|
||||
instructionsSegment = entries.join("\u0006");
|
||||
}
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
const date = formatLocalCalendarDate();
|
||||
return `${nameSegment}\u0003${descriptionSegment}\u0005${registrySegment}\u0007${instructionsSegment}|${date}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import projectPromptTemplate from "./prompts/system/project-prompt.md" with { ty
|
||||
import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
|
||||
import { shortenPath } from "./tools/render-utils";
|
||||
import { type ActiveRepoContext, resolveActiveRepoContext } from "./utils/active-repo-context";
|
||||
import { formatLocalCalendarDate } from "./utils/local-date";
|
||||
import { normalizePromptPath } from "./utils/prompt-path";
|
||||
import { AGENTS_MD_LIMIT, buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
|
||||
|
||||
@@ -677,7 +678,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
||||
}
|
||||
}
|
||||
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
const date = formatLocalCalendarDate();
|
||||
const dateTime = date;
|
||||
const promptCwd = shortenPath(normalizePromptPath(resolvedCwd));
|
||||
const activeRepoContextPrompt = renderActiveRepoContextPrompt(activeRepoContext);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/** formatLocalCalendarDate formats a Date as YYYY-MM-DD in the host local timezone. */
|
||||
export function formatLocalCalendarDate(date: Date = new Date()): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
@@ -388,11 +388,14 @@ describe("AgentSession refreshMCPTools rebuild skipping", () => {
|
||||
await session.refreshMCPTools([dynamicTool]);
|
||||
expect(rebuildCount).toBe(baseline + 1);
|
||||
});
|
||||
it("rebuilds when the calendar date rolls over between tool-stable MCP refreshes", async () => {
|
||||
// `buildSystemPrompt` injects today's date into the prompt body.
|
||||
// A session spanning midnight must not serve yesterday's date after an MCP
|
||||
// reconnect that happens to bring an identical tool set.
|
||||
setSystemTime(new Date("2025-01-01T23:59:58Z"));
|
||||
it("rebuilds when the local calendar date rolls over between tool-stable MCP refreshes", async () => {
|
||||
// `buildSystemPrompt` injects today's date into the prompt body. A session
|
||||
// spanning local midnight must not serve yesterday's date after an MCP
|
||||
// reconnect that happens to bring an identical tool set, even when UTC has
|
||||
// not rolled over.
|
||||
const originalTimezone = process.env.TZ;
|
||||
process.env.TZ = "America/Los_Angeles";
|
||||
setSystemTime(new Date("2026-07-01T06:59:58Z"));
|
||||
try {
|
||||
let rebuildCount = 0;
|
||||
const { session } = newSession(async toolNames => {
|
||||
@@ -405,22 +408,27 @@ describe("AgentSession refreshMCPTools rebuild skipping", () => {
|
||||
await session.refreshMCPTools([tool]);
|
||||
expect(rebuildCount).toBe(1);
|
||||
|
||||
// Same tools, same day: signature matches, skip.
|
||||
// Same tools, same local day: signature matches, skip.
|
||||
await session.refreshMCPTools([tool]);
|
||||
expect(rebuildCount).toBe(1);
|
||||
|
||||
// Advance past midnight.
|
||||
setSystemTime(new Date("2025-01-02T00:00:01Z"));
|
||||
// Advance past local midnight while the UTC date remains 2026-07-01.
|
||||
setSystemTime(new Date("2026-07-01T07:00:01Z"));
|
||||
|
||||
// Same tools, new calendar day: date segment changed, must rebuild.
|
||||
// Same tools, new local calendar day: date segment changed, must rebuild.
|
||||
await session.refreshMCPTools([tool]);
|
||||
expect(rebuildCount).toBe(2);
|
||||
|
||||
// Same tools, same new day: skip again.
|
||||
// Same tools, same new local day: skip again.
|
||||
await session.refreshMCPTools([tool]);
|
||||
expect(rebuildCount).toBe(2);
|
||||
} finally {
|
||||
setSystemTime(); // restore real time
|
||||
setSystemTime();
|
||||
if (originalTimezone === undefined) {
|
||||
delete process.env.TZ;
|
||||
} else {
|
||||
process.env.TZ = originalTimezone;
|
||||
}
|
||||
}
|
||||
});
|
||||
it("does not rebuild when MCP server instructions change only beyond the 4000-char truncation boundary", async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
||||
import { afterEach, beforeEach, describe, expect, it, setSystemTime } from "bun:test";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
@@ -49,6 +49,33 @@ describe("system prompt model identifier", () => {
|
||||
expect(systemPrompt.join("\n\n")).toContain("Model: anthropic/claude-opus-4");
|
||||
});
|
||||
|
||||
it("renders the prompt date from the local timezone rather than UTC", async () => {
|
||||
const originalTimezone = process.env.TZ;
|
||||
process.env.TZ = "America/Los_Angeles";
|
||||
setSystemTime(new Date("2026-07-01T03:15:00Z"));
|
||||
try {
|
||||
const { systemPrompt } = await buildSystemPrompt({
|
||||
cwd: tempDir,
|
||||
contextFiles: [],
|
||||
skills: [],
|
||||
rules: [],
|
||||
toolNames: [],
|
||||
workspaceTree: { ...EMPTY_TREE, rootPath: tempDir },
|
||||
});
|
||||
const rendered = systemPrompt.join("\n\n");
|
||||
|
||||
expect(rendered).toContain("Today is 2026-06-30");
|
||||
expect(rendered).not.toContain("Today is 2026-07-01");
|
||||
} finally {
|
||||
setSystemTime();
|
||||
if (originalTimezone === undefined) {
|
||||
delete process.env.TZ;
|
||||
} else {
|
||||
process.env.TZ = originalTimezone;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("omits the model line when no model is provided", async () => {
|
||||
const { systemPrompt } = await buildSystemPrompt({
|
||||
cwd: tempDir,
|
||||
|
||||
Reference in New Issue
Block a user