From cbe388ece310a5f030516c2014f0b19f7747cacb Mon Sep 17 00:00:00 2001 From: Conroy Whitney Date: Wed, 28 Jan 2026 21:31:08 -0500 Subject: [PATCH 1/7] feat(gateway): inject timestamps into agent handler messages Messages arriving through the gateway agent method (TUI, web, spawned subagents, sessions_send, heartbeats) now get a timestamp prefix automatically. This gives all agent contexts date/time awareness without modifying the system prompt (which is cached for stability). Channel messages (Discord, Telegram, etc.) already have timestamps via envelope formatting in a separate code path and never reach the agent handler, so there is no double-stamping risk. Cron jobs also inject their own 'Current time:' prefix and are detected and skipped. Extracted as a pure function (injectTimestamp) with 12 unit tests covering: timezone handling, 12/24h format, midnight boundaries, envelope detection, cron detection, and empty messages. Integration test verifies the agent handler wires it in correctly. Closes #3658 Refs: #1897, #1928, #2108 --- .../server-methods/agent-timestamp.test.ts | 135 ++++++++++++++++++ src/gateway/server-methods/agent-timestamp.ts | 69 +++++++++ src/gateway/server-methods/agent.test.ts | 59 +++++++- src/gateway/server-methods/agent.ts | 8 ++ 4 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 src/gateway/server-methods/agent-timestamp.test.ts create mode 100644 src/gateway/server-methods/agent-timestamp.ts diff --git a/src/gateway/server-methods/agent-timestamp.test.ts b/src/gateway/server-methods/agent-timestamp.test.ts new file mode 100644 index 000000000..bce2d9435 --- /dev/null +++ b/src/gateway/server-methods/agent-timestamp.test.ts @@ -0,0 +1,135 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { injectTimestamp, timestampOptsFromConfig } from "./agent-timestamp.js"; + +describe("injectTimestamp", () => { + beforeEach(() => { + vi.useFakeTimers(); + // Wednesday, January 28, 2026 at 8:30 PM EST + vi.setSystemTime(new Date("2026-01-29T01:30:00.000Z")); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("prepends a formatted timestamp to a plain message", () => { + const result = injectTimestamp("Is it the weekend?", { + timezone: "America/New_York", + timeFormat: "12", + }); + + expect(result).toMatch(/^\[.+\] Is it the weekend\?$/); + expect(result).toContain("Wednesday"); + expect(result).toContain("January 28"); + expect(result).toContain("2026"); + expect(result).toContain("8:30 PM"); + }); + + it("formats in 24-hour time when configured", () => { + const result = injectTimestamp("hello", { + timezone: "America/New_York", + timeFormat: "24", + }); + + expect(result).toContain("20:30"); + expect(result).not.toContain("PM"); + }); + + it("uses the configured timezone", () => { + const result = injectTimestamp("hello", { + timezone: "America/Chicago", + timeFormat: "12", + }); + + // 8:30 PM EST = 7:30 PM CST + expect(result).toContain("7:30 PM"); + }); + + it("defaults to UTC when no timezone specified", () => { + const result = injectTimestamp("hello", {}); + + // 2026-01-29T01:30:00Z + expect(result).toContain("January 29"); // UTC date, not EST + expect(result).toContain("1:30 AM"); + }); + + it("returns empty/whitespace messages unchanged", () => { + expect(injectTimestamp("", { timezone: "UTC" })).toBe(""); + expect(injectTimestamp(" ", { timezone: "UTC" })).toBe(" "); + }); + + it("does NOT double-stamp messages with channel envelope timestamps", () => { + const enveloped = "[Discord user1 2026-01-28 20:30 EST] hello there"; + const result = injectTimestamp(enveloped, { timezone: "America/New_York" }); + + expect(result).toBe(enveloped); + }); + + it("does NOT double-stamp messages with cron-injected timestamps", () => { + const cronMessage = + "[cron:abc123 my-job] do the thing\nCurrent time: Wednesday, January 28th, 2026 — 8:30 PM (America/New_York)"; + const result = injectTimestamp(cronMessage, { timezone: "America/New_York" }); + + expect(result).toBe(cronMessage); + }); + + it("handles midnight correctly", () => { + vi.setSystemTime(new Date("2026-02-01T05:00:00.000Z")); // midnight EST + + const result = injectTimestamp("hello", { + timezone: "America/New_York", + timeFormat: "12", + }); + + expect(result).toContain("February 1"); + expect(result).toContain("12:00 AM"); + }); + + it("handles date boundaries (just before midnight)", () => { + vi.setSystemTime(new Date("2026-02-01T04:59:00.000Z")); // 11:59 PM Jan 31 EST + + const result = injectTimestamp("hello", { + timezone: "America/New_York", + timeFormat: "12", + }); + + expect(result).toContain("January 31"); + expect(result).toContain("11:59 PM"); + }); + + it("accepts a custom now date", () => { + const customDate = new Date("2025-07-04T16:00:00.000Z"); // July 4, noon EST + + const result = injectTimestamp("fireworks?", { + timezone: "America/New_York", + timeFormat: "12", + now: customDate, + }); + + expect(result).toContain("July 4"); + expect(result).toContain("2025"); + }); +}); + +describe("timestampOptsFromConfig", () => { + it("extracts timezone and timeFormat from config", () => { + const opts = timestampOptsFromConfig({ + agents: { + defaults: { + userTimezone: "America/Chicago", + timeFormat: "24", + }, + }, + } as any); + + expect(opts.timezone).toBe("America/Chicago"); + expect(opts.timeFormat).toBe("24"); + }); + + it("falls back gracefully with empty config", () => { + const opts = timestampOptsFromConfig({} as any); + + expect(opts.timezone).toBeDefined(); // resolveUserTimezone provides a default + expect(opts.timeFormat).toBeUndefined(); + }); +}); diff --git a/src/gateway/server-methods/agent-timestamp.ts b/src/gateway/server-methods/agent-timestamp.ts new file mode 100644 index 000000000..e436f1ebb --- /dev/null +++ b/src/gateway/server-methods/agent-timestamp.ts @@ -0,0 +1,69 @@ +import { + formatUserTime, + resolveUserTimeFormat, + resolveUserTimezone, +} from "../../agents/date-time.js"; +import type { MoltbotConfig } from "../../config/types.js"; + +/** + * Envelope pattern used by channel plugins (Discord, Telegram, etc.): + * [Channel sender 2026-01-28 20:31 EST] message text + * + * Messages arriving through channels already have timestamps. + * We skip injection for those to avoid double-stamping. + */ +const ENVELOPE_PATTERN = /^\[[\w]+ .+ \d{4}-\d{2}-\d{2}/; + +/** + * Cron jobs inject "Current time: ..." into their messages. + * Skip injection for those too. + */ +const CRON_TIME_PATTERN = /Current time: /; + +export interface TimestampInjectionOptions { + timezone?: string; + timeFormat?: "12" | "24"; + now?: Date; +} + +/** + * Injects a timestamp prefix into a message if one isn't already present. + * + * Used by the gateway agent handler to give all agent contexts (TUI, web, + * spawned subagents, sessions_send, heartbeats) date/time awareness without + * modifying the system prompt (which is cached for stability). + * + * Channel messages (Discord, Telegram, etc.) already have timestamps via + * envelope formatting and take a separate code path — they never reach + * the agent handler, so there's no double-stamping risk. + * + * @see https://github.com/moltbot/moltbot/issues/3658 + */ +export function injectTimestamp(message: string, opts?: TimestampInjectionOptions): string { + if (!message.trim()) return message; + + // Already has a channel envelope timestamp + if (ENVELOPE_PATTERN.test(message)) return message; + + // Already has a cron-injected timestamp + if (CRON_TIME_PATTERN.test(message)) return message; + + const now = opts?.now ?? new Date(); + const timezone = opts?.timezone ?? "UTC"; + const timeFormat = opts?.timeFormat ?? "12"; + + const formatted = formatUserTime(now, timezone, resolveUserTimeFormat(timeFormat)); + if (!formatted) return message; + + return `[${formatted}] ${message}`; +} + +/** + * Build TimestampInjectionOptions from a MoltbotConfig. + */ +export function timestampOptsFromConfig(cfg: MoltbotConfig): TimestampInjectionOptions { + return { + timezone: resolveUserTimezone(cfg.agents?.defaults?.userTimezone), + timeFormat: cfg.agents?.defaults?.timeFormat as "12" | "24" | undefined, + }; +} diff --git a/src/gateway/server-methods/agent.test.ts b/src/gateway/server-methods/agent.test.ts index 149ab4a67..e5509b85f 100644 --- a/src/gateway/server-methods/agent.test.ts +++ b/src/gateway/server-methods/agent.test.ts @@ -8,6 +8,7 @@ const mocks = vi.hoisted(() => ({ updateSessionStore: vi.fn(), agentCommand: vi.fn(), registerAgentRunContext: vi.fn(), + loadConfigReturn: {} as Record, })); vi.mock("../session-utils.js", () => ({ @@ -32,7 +33,7 @@ vi.mock("../../commands/agent.js", () => ({ })); vi.mock("../../config/config.js", () => ({ - loadConfig: () => ({}), + loadConfig: () => mocks.loadConfigReturn, })); vi.mock("../../agents/agent-scope.js", () => ({ @@ -115,6 +116,62 @@ describe("gateway agent handler", () => { expect(capturedEntry?.claudeCliSessionId).toBe(existingClaudeCliSessionId); }); + it("injects a timestamp into the message passed to agentCommand", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-01-29T01:30:00.000Z")); // Wed Jan 28, 8:30 PM EST + mocks.agentCommand.mockReset(); + + mocks.loadConfigReturn = { + agents: { + defaults: { + userTimezone: "America/New_York", + timeFormat: "12", + }, + }, + }; + + mocks.loadSessionEntry.mockReturnValue({ + cfg: mocks.loadConfigReturn, + storePath: "/tmp/sessions.json", + entry: { + sessionId: "existing-session-id", + updatedAt: Date.now(), + }, + canonicalKey: "agent:main:main", + }); + mocks.updateSessionStore.mockResolvedValue(undefined); + mocks.agentCommand.mockResolvedValue({ + payloads: [{ text: "ok" }], + meta: { durationMs: 100 }, + }); + + const respond = vi.fn(); + await agentHandlers.agent({ + params: { + message: "Is it the weekend?", + agentId: "main", + sessionKey: "agent:main:main", + idempotencyKey: "test-timestamp-inject", + }, + respond, + context: makeContext(), + req: { type: "req", id: "ts-1", method: "agent" }, + client: null, + isWebchatConnect: () => false, + }); + + // Wait for the async agentCommand call + await vi.waitFor(() => expect(mocks.agentCommand).toHaveBeenCalled()); + + const callArgs = mocks.agentCommand.mock.calls[0][0]; + expect(callArgs.message).toMatch( + /^\[.*Wednesday.*January 28.*2026.*8:30 PM.*\] Is it the weekend\?$/, + ); + + mocks.loadConfigReturn = {}; + vi.useRealTimers(); + }); + it("handles missing cliSessionIds gracefully", async () => { mocks.loadSessionEntry.mockReturnValue({ cfg: {}, diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index d159d1f78..8888dcbc4 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto"; import { agentCommand } from "../../commands/agent.js"; import { listAgentIds } from "../../agents/agent-scope.js"; import { loadConfig } from "../../config/config.js"; +import { injectTimestamp, timestampOptsFromConfig } from "./agent-timestamp.js"; import { resolveAgentIdFromSessionKey, resolveExplicitAgentSessionKey, @@ -138,6 +139,13 @@ export const agentHandlers: GatewayRequestHandlers = { return; } } + + // Inject timestamp into messages that don't already have one. + // Channel messages (Discord, Telegram, etc.) get timestamps via envelope + // formatting in a separate code path — they never reach this handler. + // See: https://github.com/moltbot/moltbot/issues/3658 + message = injectTimestamp(message, timestampOptsFromConfig(cfg)); + const isKnownGatewayChannel = (value: string): boolean => isGatewayMessageChannel(value); const channelHints = [request.channel, request.replyChannel] .filter((value): value is string => typeof value === "string") From 3696532f040d7bca49dd5637828bbb5063af030e Mon Sep 17 00:00:00 2001 From: Conroy Whitney Date: Wed, 28 Jan 2026 21:34:53 -0500 Subject: [PATCH 2/7] feat(gateway): inject timestamps into chat.send (webchat/TUI) The chat.send handler (used by webchat and TUI) is a separate path from the agent handler. Inject timestamp into BodyForAgent (what the model sees) while keeping Body raw for UI display. This completes timestamp coverage for all non-channel paths: - agent handler: spawned subagents, sessions_send, heartbeats - chat.send: webchat, TUI --- src/gateway/server-methods/chat.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index 9010a6f21..53a992d3d 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -5,6 +5,7 @@ import path from "node:path"; import { CURRENT_SESSION_VERSION } from "@mariozechner/pi-coding-agent"; import { resolveSessionAgentId } from "../../agents/agent-scope.js"; import { resolveEffectiveMessagesConfig, resolveIdentityName } from "../../agents/identity.js"; +import { injectTimestamp, timestampOptsFromConfig } from "./agent-timestamp.js"; import { resolveThinkingDefault } from "../../agents/model-selection.js"; import { resolveAgentTimeoutMs } from "../../agents/timeout.js"; import { dispatchInboundMessage } from "../../auto-reply/dispatch.js"; @@ -443,9 +444,14 @@ export const chatHandlers: GatewayRequestHandlers = { ); const commandBody = injectThinking ? `/think ${p.thinking} ${parsedMessage}` : parsedMessage; const clientInfo = client?.connect?.client; + // Inject timestamp so agents know the current date/time. + // Only BodyForAgent gets the timestamp — Body stays raw for UI display. + // See: https://github.com/moltbot/moltbot/issues/3658 + const stampedMessage = injectTimestamp(parsedMessage, timestampOptsFromConfig(cfg)); + const ctx: MsgContext = { Body: parsedMessage, - BodyForAgent: parsedMessage, + BodyForAgent: stampedMessage, BodyForCommands: commandBody, RawBody: parsedMessage, CommandBody: commandBody, From 9d287637533e85c7e536a6b3b57d96fa8240dd85 Mon Sep 17 00:00:00 2001 From: Conroy Whitney Date: Wed, 28 Jan 2026 21:46:29 -0500 Subject: [PATCH 3/7] test: add DST boundary test for timestamp injection Verifies that America/New_York correctly resolves to midnight for both EST (winter, UTC-5) and EDT (summer, UTC-4) using the same IANA timezone. Intl.DateTimeFormat handles the DST transition. --- .../server-methods/agent-timestamp.test.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/gateway/server-methods/agent-timestamp.test.ts b/src/gateway/server-methods/agent-timestamp.test.ts index bce2d9435..eb24e02eb 100644 --- a/src/gateway/server-methods/agent-timestamp.test.ts +++ b/src/gateway/server-methods/agent-timestamp.test.ts @@ -97,6 +97,26 @@ describe("injectTimestamp", () => { expect(result).toContain("11:59 PM"); }); + it("handles DST correctly (same UTC hour, different local time)", () => { + // EST (winter): UTC-5 → 2026-01-15T05:00Z = midnight Jan 15 + vi.setSystemTime(new Date("2026-01-15T05:00:00.000Z")); + const winter = injectTimestamp("winter", { + timezone: "America/New_York", + timeFormat: "12", + }); + expect(winter).toContain("January 15"); + expect(winter).toContain("12:00 AM"); + + // EDT (summer): UTC-4 → 2026-07-15T04:00Z = midnight Jul 15 + vi.setSystemTime(new Date("2026-07-15T04:00:00.000Z")); + const summer = injectTimestamp("summer", { + timezone: "America/New_York", + timeFormat: "12", + }); + expect(summer).toContain("July 15"); + expect(summer).toContain("12:00 AM"); + }); + it("accepts a custom now date", () => { const customDate = new Date("2025-07-04T16:00:00.000Z"); // July 4, noon EST From ff713a41e01602339ea2fcf0ae8f863c1e39a41d Mon Sep 17 00:00:00 2001 From: Conroy Whitney Date: Wed, 28 Jan 2026 21:59:21 -0500 Subject: [PATCH 4/7] refactor: use compact formatZonedTimestamp for injection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace verbose formatUserTime (Wednesday, January 28th, 2026 — 8:30 PM) with the same formatZonedTimestamp used by channel envelopes (2026-01-28 20:30 EST). This: - Saves ~4 tokens per message (~7 vs ~11) - Uses globally unambiguous YYYY-MM-DD 24h format - Removes 12/24h config option (always 24h, agent-facing) - Anchors envelope detection to the actual format function — if channels change their timestamp format, our injection + detection change too - Adds test that compares injection output to formatZonedTimestamp directly Exported formatZonedTimestamp from auto-reply/envelope.ts for reuse. --- src/auto-reply/envelope.ts | 2 +- .../server-methods/agent-timestamp.test.ts | 93 ++++++++----------- src/gateway/server-methods/agent-timestamp.ts | 49 +++++----- src/gateway/server-methods/agent.test.ts | 5 +- 4 files changed, 64 insertions(+), 85 deletions(-) diff --git a/src/auto-reply/envelope.ts b/src/auto-reply/envelope.ts index 357ae3d1f..ae5bf1985 100644 --- a/src/auto-reply/envelope.ts +++ b/src/auto-reply/envelope.ts @@ -97,7 +97,7 @@ function formatUtcTimestamp(date: Date): string { return `${yyyy}-${mm}-${dd}T${hh}:${min}Z`; } -function formatZonedTimestamp(date: Date, timeZone?: string): string | undefined { +export function formatZonedTimestamp(date: Date, timeZone?: string): string | undefined { const parts = new Intl.DateTimeFormat("en-US", { timeZone, year: "numeric", diff --git a/src/gateway/server-methods/agent-timestamp.test.ts b/src/gateway/server-methods/agent-timestamp.test.ts index eb24e02eb..85398c4e4 100644 --- a/src/gateway/server-methods/agent-timestamp.test.ts +++ b/src/gateway/server-methods/agent-timestamp.test.ts @@ -1,10 +1,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { injectTimestamp, timestampOptsFromConfig } from "./agent-timestamp.js"; +import { formatZonedTimestamp } from "../../auto-reply/envelope.js"; describe("injectTimestamp", () => { beforeEach(() => { vi.useFakeTimers(); - // Wednesday, January 28, 2026 at 8:30 PM EST + // Wednesday, January 28, 2026 at 8:30 PM EST (01:30 UTC Jan 29) vi.setSystemTime(new Date("2026-01-29T01:30:00.000Z")); }); @@ -12,45 +13,43 @@ describe("injectTimestamp", () => { vi.useRealTimers(); }); - it("prepends a formatted timestamp to a plain message", () => { + it("prepends a compact timestamp matching formatZonedTimestamp", () => { const result = injectTimestamp("Is it the weekend?", { timezone: "America/New_York", - timeFormat: "12", }); - expect(result).toMatch(/^\[.+\] Is it the weekend\?$/); - expect(result).toContain("Wednesday"); - expect(result).toContain("January 28"); - expect(result).toContain("2026"); - expect(result).toContain("8:30 PM"); + expect(result).toMatch(/^\[2026-01-28 20:30 EST\] Is it the weekend\?$/); }); - it("formats in 24-hour time when configured", () => { - const result = injectTimestamp("hello", { - timezone: "America/New_York", - timeFormat: "24", - }); + it("uses the same format as channel envelope timestamps", () => { + const now = new Date(); + const expected = formatZonedTimestamp(now, "America/New_York"); + + const result = injectTimestamp("hello", { timezone: "America/New_York" }); + + expect(result).toBe(`[${expected}] hello`); + }); + + it("always uses 24-hour format", () => { + const result = injectTimestamp("hello", { timezone: "America/New_York" }); expect(result).toContain("20:30"); expect(result).not.toContain("PM"); + expect(result).not.toContain("AM"); }); it("uses the configured timezone", () => { - const result = injectTimestamp("hello", { - timezone: "America/Chicago", - timeFormat: "12", - }); + const result = injectTimestamp("hello", { timezone: "America/Chicago" }); - // 8:30 PM EST = 7:30 PM CST - expect(result).toContain("7:30 PM"); + // 8:30 PM EST = 7:30 PM CST = 19:30 + expect(result).toMatch(/^\[2026-01-28 19:30 CST\]/); }); it("defaults to UTC when no timezone specified", () => { const result = injectTimestamp("hello", {}); // 2026-01-29T01:30:00Z - expect(result).toContain("January 29"); // UTC date, not EST - expect(result).toContain("1:30 AM"); + expect(result).toMatch(/^\[2026-01-29 01:30/); }); it("returns empty/whitespace messages unchanged", () => { @@ -65,6 +64,13 @@ describe("injectTimestamp", () => { expect(result).toBe(enveloped); }); + it("does NOT double-stamp messages already injected by us", () => { + const alreadyStamped = "[2026-01-28 20:30 EST] hello there"; + const result = injectTimestamp(alreadyStamped, { timezone: "America/New_York" }); + + expect(result).toBe(alreadyStamped); + }); + it("does NOT double-stamp messages with cron-injected timestamps", () => { const cronMessage = "[cron:abc123 my-job] do the thing\nCurrent time: Wednesday, January 28th, 2026 — 8:30 PM (America/New_York)"; @@ -76,80 +82,59 @@ describe("injectTimestamp", () => { it("handles midnight correctly", () => { vi.setSystemTime(new Date("2026-02-01T05:00:00.000Z")); // midnight EST - const result = injectTimestamp("hello", { - timezone: "America/New_York", - timeFormat: "12", - }); + const result = injectTimestamp("hello", { timezone: "America/New_York" }); - expect(result).toContain("February 1"); - expect(result).toContain("12:00 AM"); + expect(result).toMatch(/^\[2026-02-01 00:00 EST\]/); }); it("handles date boundaries (just before midnight)", () => { - vi.setSystemTime(new Date("2026-02-01T04:59:00.000Z")); // 11:59 PM Jan 31 EST + vi.setSystemTime(new Date("2026-02-01T04:59:00.000Z")); // 23:59 Jan 31 EST - const result = injectTimestamp("hello", { - timezone: "America/New_York", - timeFormat: "12", - }); + const result = injectTimestamp("hello", { timezone: "America/New_York" }); - expect(result).toContain("January 31"); - expect(result).toContain("11:59 PM"); + expect(result).toMatch(/^\[2026-01-31 23:59 EST\]/); }); it("handles DST correctly (same UTC hour, different local time)", () => { // EST (winter): UTC-5 → 2026-01-15T05:00Z = midnight Jan 15 vi.setSystemTime(new Date("2026-01-15T05:00:00.000Z")); - const winter = injectTimestamp("winter", { - timezone: "America/New_York", - timeFormat: "12", - }); - expect(winter).toContain("January 15"); - expect(winter).toContain("12:00 AM"); + const winter = injectTimestamp("winter", { timezone: "America/New_York" }); + expect(winter).toMatch(/^\[2026-01-15 00:00 EST\]/); // EDT (summer): UTC-4 → 2026-07-15T04:00Z = midnight Jul 15 vi.setSystemTime(new Date("2026-07-15T04:00:00.000Z")); - const summer = injectTimestamp("summer", { - timezone: "America/New_York", - timeFormat: "12", - }); - expect(summer).toContain("July 15"); - expect(summer).toContain("12:00 AM"); + const summer = injectTimestamp("summer", { timezone: "America/New_York" }); + expect(summer).toMatch(/^\[2026-07-15 00:00 EDT\]/); }); it("accepts a custom now date", () => { - const customDate = new Date("2025-07-04T16:00:00.000Z"); // July 4, noon EST + const customDate = new Date("2025-07-04T16:00:00.000Z"); // July 4, noon ET const result = injectTimestamp("fireworks?", { timezone: "America/New_York", - timeFormat: "12", now: customDate, }); - expect(result).toContain("July 4"); - expect(result).toContain("2025"); + expect(result).toMatch(/^\[2025-07-04 12:00 EDT\]/); }); }); describe("timestampOptsFromConfig", () => { - it("extracts timezone and timeFormat from config", () => { + it("extracts timezone from config", () => { const opts = timestampOptsFromConfig({ agents: { defaults: { userTimezone: "America/Chicago", - timeFormat: "24", }, }, } as any); expect(opts.timezone).toBe("America/Chicago"); - expect(opts.timeFormat).toBe("24"); }); it("falls back gracefully with empty config", () => { const opts = timestampOptsFromConfig({} as any); expect(opts.timezone).toBeDefined(); // resolveUserTimezone provides a default - expect(opts.timeFormat).toBeUndefined(); }); }); diff --git a/src/gateway/server-methods/agent-timestamp.ts b/src/gateway/server-methods/agent-timestamp.ts index e436f1ebb..c8b397734 100644 --- a/src/gateway/server-methods/agent-timestamp.ts +++ b/src/gateway/server-methods/agent-timestamp.ts @@ -1,58 +1,56 @@ -import { - formatUserTime, - resolveUserTimeFormat, - resolveUserTimezone, -} from "../../agents/date-time.js"; +import { resolveUserTimezone } from "../../agents/date-time.js"; +import { formatZonedTimestamp } from "../../auto-reply/envelope.js"; import type { MoltbotConfig } from "../../config/types.js"; -/** - * Envelope pattern used by channel plugins (Discord, Telegram, etc.): - * [Channel sender 2026-01-28 20:31 EST] message text - * - * Messages arriving through channels already have timestamps. - * We skip injection for those to avoid double-stamping. - */ -const ENVELOPE_PATTERN = /^\[[\w]+ .+ \d{4}-\d{2}-\d{2}/; - /** * Cron jobs inject "Current time: ..." into their messages. - * Skip injection for those too. + * Skip injection for those. */ const CRON_TIME_PATTERN = /Current time: /; +/** + * Matches a leading `[... YYYY-MM-DD HH:MM ...]` envelope — either from + * channel plugins or from a previous injection. Uses the same YYYY-MM-DD + * HH:MM format as {@link formatZonedTimestamp}, so detection stays in sync + * with the formatting. + */ +const TIMESTAMP_ENVELOPE_PATTERN = /^\[.*\d{4}-\d{2}-\d{2} \d{2}:\d{2}/; + export interface TimestampInjectionOptions { timezone?: string; - timeFormat?: "12" | "24"; now?: Date; } /** - * Injects a timestamp prefix into a message if one isn't already present. + * Injects a compact timestamp prefix into a message if one isn't already + * present. Uses the same `YYYY-MM-DD HH:MM TZ` format as channel envelope + * timestamps ({@link formatZonedTimestamp}), keeping token cost low (~7 + * tokens) and format consistent across all agent contexts. * - * Used by the gateway agent handler to give all agent contexts (TUI, web, - * spawned subagents, sessions_send, heartbeats) date/time awareness without - * modifying the system prompt (which is cached for stability). + * Used by the gateway `agent` and `chat.send` handlers to give TUI, web, + * spawned subagents, `sessions_send`, and heartbeat wake events date/time + * awareness — without modifying the system prompt (which is cached). * * Channel messages (Discord, Telegram, etc.) already have timestamps via * envelope formatting and take a separate code path — they never reach - * the agent handler, so there's no double-stamping risk. + * these handlers, so there is no double-stamping risk. The detection + * pattern is a safety net for edge cases. * * @see https://github.com/moltbot/moltbot/issues/3658 */ export function injectTimestamp(message: string, opts?: TimestampInjectionOptions): string { if (!message.trim()) return message; - // Already has a channel envelope timestamp - if (ENVELOPE_PATTERN.test(message)) return message; + // Already has an envelope or injected timestamp + if (TIMESTAMP_ENVELOPE_PATTERN.test(message)) return message; // Already has a cron-injected timestamp if (CRON_TIME_PATTERN.test(message)) return message; const now = opts?.now ?? new Date(); const timezone = opts?.timezone ?? "UTC"; - const timeFormat = opts?.timeFormat ?? "12"; - const formatted = formatUserTime(now, timezone, resolveUserTimeFormat(timeFormat)); + const formatted = formatZonedTimestamp(now, timezone); if (!formatted) return message; return `[${formatted}] ${message}`; @@ -64,6 +62,5 @@ export function injectTimestamp(message: string, opts?: TimestampInjectionOption export function timestampOptsFromConfig(cfg: MoltbotConfig): TimestampInjectionOptions { return { timezone: resolveUserTimezone(cfg.agents?.defaults?.userTimezone), - timeFormat: cfg.agents?.defaults?.timeFormat as "12" | "24" | undefined, }; } diff --git a/src/gateway/server-methods/agent.test.ts b/src/gateway/server-methods/agent.test.ts index e5509b85f..a78131631 100644 --- a/src/gateway/server-methods/agent.test.ts +++ b/src/gateway/server-methods/agent.test.ts @@ -125,7 +125,6 @@ describe("gateway agent handler", () => { agents: { defaults: { userTimezone: "America/New_York", - timeFormat: "12", }, }, }; @@ -164,9 +163,7 @@ describe("gateway agent handler", () => { await vi.waitFor(() => expect(mocks.agentCommand).toHaveBeenCalled()); const callArgs = mocks.agentCommand.mock.calls[0][0]; - expect(callArgs.message).toMatch( - /^\[.*Wednesday.*January 28.*2026.*8:30 PM.*\] Is it the weekend\?$/, - ); + expect(callArgs.message).toBe("[2026-01-28 20:30 EST] Is it the weekend?"); mocks.loadConfigReturn = {}; vi.useRealTimers(); From a007ba7b1b3c89dcf75ae94d2aabad851e58cd9f Mon Sep 17 00:00:00 2001 From: Conroy Whitney Date: Wed, 28 Jan 2026 22:07:26 -0500 Subject: [PATCH 5/7] feat: add 3-letter DOW prefix to injected timestamps Changes [2026-01-28 20:30 EST] to [Wed 2026-01-28 20:30 EST]. Costs ~1 extra token but provides day-of-week for smaller models that can't derive DOW from a date. Frontier models already handle it, but this is cheap insurance for 7B-class models. --- .../server-methods/agent-timestamp.test.ts | 23 ++++++++++--------- src/gateway/server-methods/agent-timestamp.ts | 8 ++++++- src/gateway/server-methods/agent.test.ts | 2 +- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/gateway/server-methods/agent-timestamp.test.ts b/src/gateway/server-methods/agent-timestamp.test.ts index 85398c4e4..9ac3f7d13 100644 --- a/src/gateway/server-methods/agent-timestamp.test.ts +++ b/src/gateway/server-methods/agent-timestamp.test.ts @@ -18,16 +18,17 @@ describe("injectTimestamp", () => { timezone: "America/New_York", }); - expect(result).toMatch(/^\[2026-01-28 20:30 EST\] Is it the weekend\?$/); + expect(result).toMatch(/^\[Wed 2026-01-28 20:30 EST\] Is it the weekend\?$/); }); - it("uses the same format as channel envelope timestamps", () => { + it("uses channel envelope format with DOW prefix", () => { const now = new Date(); const expected = formatZonedTimestamp(now, "America/New_York"); const result = injectTimestamp("hello", { timezone: "America/New_York" }); - expect(result).toBe(`[${expected}] hello`); + // DOW prefix + formatZonedTimestamp format + expect(result).toBe(`[Wed ${expected}] hello`); }); it("always uses 24-hour format", () => { @@ -42,14 +43,14 @@ describe("injectTimestamp", () => { const result = injectTimestamp("hello", { timezone: "America/Chicago" }); // 8:30 PM EST = 7:30 PM CST = 19:30 - expect(result).toMatch(/^\[2026-01-28 19:30 CST\]/); + expect(result).toMatch(/^\[Wed 2026-01-28 19:30 CST\]/); }); it("defaults to UTC when no timezone specified", () => { const result = injectTimestamp("hello", {}); // 2026-01-29T01:30:00Z - expect(result).toMatch(/^\[2026-01-29 01:30/); + expect(result).toMatch(/^\[Thu 2026-01-29 01:30/); }); it("returns empty/whitespace messages unchanged", () => { @@ -65,7 +66,7 @@ describe("injectTimestamp", () => { }); it("does NOT double-stamp messages already injected by us", () => { - const alreadyStamped = "[2026-01-28 20:30 EST] hello there"; + const alreadyStamped = "[Wed 2026-01-28 20:30 EST] hello there"; const result = injectTimestamp(alreadyStamped, { timezone: "America/New_York" }); expect(result).toBe(alreadyStamped); @@ -84,7 +85,7 @@ describe("injectTimestamp", () => { const result = injectTimestamp("hello", { timezone: "America/New_York" }); - expect(result).toMatch(/^\[2026-02-01 00:00 EST\]/); + expect(result).toMatch(/^\[Sun 2026-02-01 00:00 EST\]/); }); it("handles date boundaries (just before midnight)", () => { @@ -92,19 +93,19 @@ describe("injectTimestamp", () => { const result = injectTimestamp("hello", { timezone: "America/New_York" }); - expect(result).toMatch(/^\[2026-01-31 23:59 EST\]/); + expect(result).toMatch(/^\[Sat 2026-01-31 23:59 EST\]/); }); it("handles DST correctly (same UTC hour, different local time)", () => { // EST (winter): UTC-5 → 2026-01-15T05:00Z = midnight Jan 15 vi.setSystemTime(new Date("2026-01-15T05:00:00.000Z")); const winter = injectTimestamp("winter", { timezone: "America/New_York" }); - expect(winter).toMatch(/^\[2026-01-15 00:00 EST\]/); + expect(winter).toMatch(/^\[Thu 2026-01-15 00:00 EST\]/); // EDT (summer): UTC-4 → 2026-07-15T04:00Z = midnight Jul 15 vi.setSystemTime(new Date("2026-07-15T04:00:00.000Z")); const summer = injectTimestamp("summer", { timezone: "America/New_York" }); - expect(summer).toMatch(/^\[2026-07-15 00:00 EDT\]/); + expect(summer).toMatch(/^\[Wed 2026-07-15 00:00 EDT\]/); }); it("accepts a custom now date", () => { @@ -115,7 +116,7 @@ describe("injectTimestamp", () => { now: customDate, }); - expect(result).toMatch(/^\[2025-07-04 12:00 EDT\]/); + expect(result).toMatch(/^\[Fri 2025-07-04 12:00 EDT\]/); }); }); diff --git a/src/gateway/server-methods/agent-timestamp.ts b/src/gateway/server-methods/agent-timestamp.ts index c8b397734..6cd24b797 100644 --- a/src/gateway/server-methods/agent-timestamp.ts +++ b/src/gateway/server-methods/agent-timestamp.ts @@ -53,7 +53,13 @@ export function injectTimestamp(message: string, opts?: TimestampInjectionOption const formatted = formatZonedTimestamp(now, timezone); if (!formatted) return message; - return `[${formatted}] ${message}`; + // Add 3-letter day-of-week for smaller models that can't derive DOW + // from a date. Costs ~1 token, cheap insurance. + const dow = new Intl.DateTimeFormat("en-US", { timeZone: timezone, weekday: "short" }).format( + now, + ); + + return `[${dow} ${formatted}] ${message}`; } /** diff --git a/src/gateway/server-methods/agent.test.ts b/src/gateway/server-methods/agent.test.ts index a78131631..669a8aa07 100644 --- a/src/gateway/server-methods/agent.test.ts +++ b/src/gateway/server-methods/agent.test.ts @@ -163,7 +163,7 @@ describe("gateway agent handler", () => { await vi.waitFor(() => expect(mocks.agentCommand).toHaveBeenCalled()); const callArgs = mocks.agentCommand.mock.calls[0][0]; - expect(callArgs.message).toBe("[2026-01-28 20:30 EST] Is it the weekend?"); + expect(callArgs.message).toBe("[Wed 2026-01-28 20:30 EST] Is it the weekend?"); mocks.loadConfigReturn = {}; vi.useRealTimers(); From 5ca1b96988b617c92f9aa8bfe8d0e647920a816f Mon Sep 17 00:00:00 2001 From: Conroy Whitney Date: Wed, 28 Jan 2026 22:28:48 -0500 Subject: [PATCH 6/7] feat: add "Current Date:" label to timestamp prefix Changes [Wed 2026-01-28 20:30 EST] to [Current Date: Wed 2026-01-28 20:30 EST]. Tested with qwen3-1.7B: even with DOW in the timestamp, the model ignored it and tried to compute the day using Zeller's Congruence. The "Current Date:" semantic label is widely present in training data and gives small models the best chance of recognizing the timestamp as authoritative context rather than metadata to parse. Cost: ~18 tokens per message. Prevents hallucination spirals that burn hundreds or thousands of tokens on date derivation. --- .../server-methods/agent-timestamp.test.ts | 20 +++++++++---------- src/gateway/server-methods/agent-timestamp.ts | 7 ++++--- src/gateway/server-methods/agent.test.ts | 2 +- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/gateway/server-methods/agent-timestamp.test.ts b/src/gateway/server-methods/agent-timestamp.test.ts index 9ac3f7d13..7aece7a31 100644 --- a/src/gateway/server-methods/agent-timestamp.test.ts +++ b/src/gateway/server-methods/agent-timestamp.test.ts @@ -18,7 +18,7 @@ describe("injectTimestamp", () => { timezone: "America/New_York", }); - expect(result).toMatch(/^\[Wed 2026-01-28 20:30 EST\] Is it the weekend\?$/); + expect(result).toMatch(/^\[Current Date: Wed 2026-01-28 20:30 EST\] Is it the weekend\?$/); }); it("uses channel envelope format with DOW prefix", () => { @@ -28,7 +28,7 @@ describe("injectTimestamp", () => { const result = injectTimestamp("hello", { timezone: "America/New_York" }); // DOW prefix + formatZonedTimestamp format - expect(result).toBe(`[Wed ${expected}] hello`); + expect(result).toBe(`[Current Date: Wed ${expected}] hello`); }); it("always uses 24-hour format", () => { @@ -43,14 +43,14 @@ describe("injectTimestamp", () => { const result = injectTimestamp("hello", { timezone: "America/Chicago" }); // 8:30 PM EST = 7:30 PM CST = 19:30 - expect(result).toMatch(/^\[Wed 2026-01-28 19:30 CST\]/); + expect(result).toMatch(/^\[Current Date: Wed 2026-01-28 19:30 CST\]/); }); it("defaults to UTC when no timezone specified", () => { const result = injectTimestamp("hello", {}); // 2026-01-29T01:30:00Z - expect(result).toMatch(/^\[Thu 2026-01-29 01:30/); + expect(result).toMatch(/^\[Current Date: Thu 2026-01-29 01:30/); }); it("returns empty/whitespace messages unchanged", () => { @@ -66,7 +66,7 @@ describe("injectTimestamp", () => { }); it("does NOT double-stamp messages already injected by us", () => { - const alreadyStamped = "[Wed 2026-01-28 20:30 EST] hello there"; + const alreadyStamped = "[Current Date: Wed 2026-01-28 20:30 EST] hello there"; const result = injectTimestamp(alreadyStamped, { timezone: "America/New_York" }); expect(result).toBe(alreadyStamped); @@ -85,7 +85,7 @@ describe("injectTimestamp", () => { const result = injectTimestamp("hello", { timezone: "America/New_York" }); - expect(result).toMatch(/^\[Sun 2026-02-01 00:00 EST\]/); + expect(result).toMatch(/^\[Current Date: Sun 2026-02-01 00:00 EST\]/); }); it("handles date boundaries (just before midnight)", () => { @@ -93,19 +93,19 @@ describe("injectTimestamp", () => { const result = injectTimestamp("hello", { timezone: "America/New_York" }); - expect(result).toMatch(/^\[Sat 2026-01-31 23:59 EST\]/); + expect(result).toMatch(/^\[Current Date: Sat 2026-01-31 23:59 EST\]/); }); it("handles DST correctly (same UTC hour, different local time)", () => { // EST (winter): UTC-5 → 2026-01-15T05:00Z = midnight Jan 15 vi.setSystemTime(new Date("2026-01-15T05:00:00.000Z")); const winter = injectTimestamp("winter", { timezone: "America/New_York" }); - expect(winter).toMatch(/^\[Thu 2026-01-15 00:00 EST\]/); + expect(winter).toMatch(/^\[Current Date: Thu 2026-01-15 00:00 EST\]/); // EDT (summer): UTC-4 → 2026-07-15T04:00Z = midnight Jul 15 vi.setSystemTime(new Date("2026-07-15T04:00:00.000Z")); const summer = injectTimestamp("summer", { timezone: "America/New_York" }); - expect(summer).toMatch(/^\[Wed 2026-07-15 00:00 EDT\]/); + expect(summer).toMatch(/^\[Current Date: Wed 2026-07-15 00:00 EDT\]/); }); it("accepts a custom now date", () => { @@ -116,7 +116,7 @@ describe("injectTimestamp", () => { now: customDate, }); - expect(result).toMatch(/^\[Fri 2025-07-04 12:00 EDT\]/); + expect(result).toMatch(/^\[Current Date: Fri 2025-07-04 12:00 EDT\]/); }); }); diff --git a/src/gateway/server-methods/agent-timestamp.ts b/src/gateway/server-methods/agent-timestamp.ts index 6cd24b797..0b09d34e7 100644 --- a/src/gateway/server-methods/agent-timestamp.ts +++ b/src/gateway/server-methods/agent-timestamp.ts @@ -53,13 +53,14 @@ export function injectTimestamp(message: string, opts?: TimestampInjectionOption const formatted = formatZonedTimestamp(now, timezone); if (!formatted) return message; - // Add 3-letter day-of-week for smaller models that can't derive DOW - // from a date. Costs ~1 token, cheap insurance. + // "Current Date:" label is unambiguous even for tiny models (1.7B+). + // 3-letter DOW included because small models can't derive it from a date. + // Total cost: ~18 tokens — saves thousands when it prevents hallucination. const dow = new Intl.DateTimeFormat("en-US", { timeZone: timezone, weekday: "short" }).format( now, ); - return `[${dow} ${formatted}] ${message}`; + return `[Current Date: ${dow} ${formatted}] ${message}`; } /** diff --git a/src/gateway/server-methods/agent.test.ts b/src/gateway/server-methods/agent.test.ts index 669a8aa07..e572d2abc 100644 --- a/src/gateway/server-methods/agent.test.ts +++ b/src/gateway/server-methods/agent.test.ts @@ -163,7 +163,7 @@ describe("gateway agent handler", () => { await vi.waitFor(() => expect(mocks.agentCommand).toHaveBeenCalled()); const callArgs = mocks.agentCommand.mock.calls[0][0]; - expect(callArgs.message).toBe("[Wed 2026-01-28 20:30 EST] Is it the weekend?"); + expect(callArgs.message).toBe("[Current Date: Wed 2026-01-28 20:30 EST] Is it the weekend?"); mocks.loadConfigReturn = {}; vi.useRealTimers(); From b603d9c6bac162e2396fe07e0ebd1a241cd15624 Mon Sep 17 00:00:00 2001 From: Conroy Whitney Date: Wed, 28 Jan 2026 22:40:48 -0500 Subject: [PATCH 7/7] revert: drop "Current Date:" label, keep [Wed YYYY-MM-DD HH:MM TZ] Small model testing showed the label did not meaningfully help: - Sub-3B models fail regardless of format - 8B models untested with label specifically - Frontier models never needed it The bracket convention [Wed 2026-01-28 22:30 EST] matches existing channel envelope format and is widely present in training data. Saves ~2-3 tokens per message vs the labeled version. --- .../server-methods/agent-timestamp.test.ts | 20 +++++++++---------- src/gateway/server-methods/agent-timestamp.ts | 7 +++---- src/gateway/server-methods/agent.test.ts | 2 +- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/gateway/server-methods/agent-timestamp.test.ts b/src/gateway/server-methods/agent-timestamp.test.ts index 7aece7a31..9ac3f7d13 100644 --- a/src/gateway/server-methods/agent-timestamp.test.ts +++ b/src/gateway/server-methods/agent-timestamp.test.ts @@ -18,7 +18,7 @@ describe("injectTimestamp", () => { timezone: "America/New_York", }); - expect(result).toMatch(/^\[Current Date: Wed 2026-01-28 20:30 EST\] Is it the weekend\?$/); + expect(result).toMatch(/^\[Wed 2026-01-28 20:30 EST\] Is it the weekend\?$/); }); it("uses channel envelope format with DOW prefix", () => { @@ -28,7 +28,7 @@ describe("injectTimestamp", () => { const result = injectTimestamp("hello", { timezone: "America/New_York" }); // DOW prefix + formatZonedTimestamp format - expect(result).toBe(`[Current Date: Wed ${expected}] hello`); + expect(result).toBe(`[Wed ${expected}] hello`); }); it("always uses 24-hour format", () => { @@ -43,14 +43,14 @@ describe("injectTimestamp", () => { const result = injectTimestamp("hello", { timezone: "America/Chicago" }); // 8:30 PM EST = 7:30 PM CST = 19:30 - expect(result).toMatch(/^\[Current Date: Wed 2026-01-28 19:30 CST\]/); + expect(result).toMatch(/^\[Wed 2026-01-28 19:30 CST\]/); }); it("defaults to UTC when no timezone specified", () => { const result = injectTimestamp("hello", {}); // 2026-01-29T01:30:00Z - expect(result).toMatch(/^\[Current Date: Thu 2026-01-29 01:30/); + expect(result).toMatch(/^\[Thu 2026-01-29 01:30/); }); it("returns empty/whitespace messages unchanged", () => { @@ -66,7 +66,7 @@ describe("injectTimestamp", () => { }); it("does NOT double-stamp messages already injected by us", () => { - const alreadyStamped = "[Current Date: Wed 2026-01-28 20:30 EST] hello there"; + const alreadyStamped = "[Wed 2026-01-28 20:30 EST] hello there"; const result = injectTimestamp(alreadyStamped, { timezone: "America/New_York" }); expect(result).toBe(alreadyStamped); @@ -85,7 +85,7 @@ describe("injectTimestamp", () => { const result = injectTimestamp("hello", { timezone: "America/New_York" }); - expect(result).toMatch(/^\[Current Date: Sun 2026-02-01 00:00 EST\]/); + expect(result).toMatch(/^\[Sun 2026-02-01 00:00 EST\]/); }); it("handles date boundaries (just before midnight)", () => { @@ -93,19 +93,19 @@ describe("injectTimestamp", () => { const result = injectTimestamp("hello", { timezone: "America/New_York" }); - expect(result).toMatch(/^\[Current Date: Sat 2026-01-31 23:59 EST\]/); + expect(result).toMatch(/^\[Sat 2026-01-31 23:59 EST\]/); }); it("handles DST correctly (same UTC hour, different local time)", () => { // EST (winter): UTC-5 → 2026-01-15T05:00Z = midnight Jan 15 vi.setSystemTime(new Date("2026-01-15T05:00:00.000Z")); const winter = injectTimestamp("winter", { timezone: "America/New_York" }); - expect(winter).toMatch(/^\[Current Date: Thu 2026-01-15 00:00 EST\]/); + expect(winter).toMatch(/^\[Thu 2026-01-15 00:00 EST\]/); // EDT (summer): UTC-4 → 2026-07-15T04:00Z = midnight Jul 15 vi.setSystemTime(new Date("2026-07-15T04:00:00.000Z")); const summer = injectTimestamp("summer", { timezone: "America/New_York" }); - expect(summer).toMatch(/^\[Current Date: Wed 2026-07-15 00:00 EDT\]/); + expect(summer).toMatch(/^\[Wed 2026-07-15 00:00 EDT\]/); }); it("accepts a custom now date", () => { @@ -116,7 +116,7 @@ describe("injectTimestamp", () => { now: customDate, }); - expect(result).toMatch(/^\[Current Date: Fri 2025-07-04 12:00 EDT\]/); + expect(result).toMatch(/^\[Fri 2025-07-04 12:00 EDT\]/); }); }); diff --git a/src/gateway/server-methods/agent-timestamp.ts b/src/gateway/server-methods/agent-timestamp.ts index 0b09d34e7..3dcb71834 100644 --- a/src/gateway/server-methods/agent-timestamp.ts +++ b/src/gateway/server-methods/agent-timestamp.ts @@ -53,14 +53,13 @@ export function injectTimestamp(message: string, opts?: TimestampInjectionOption const formatted = formatZonedTimestamp(now, timezone); if (!formatted) return message; - // "Current Date:" label is unambiguous even for tiny models (1.7B+). - // 3-letter DOW included because small models can't derive it from a date. - // Total cost: ~18 tokens — saves thousands when it prevents hallucination. + // 3-letter DOW: small models (8B) can't reliably derive day-of-week from + // a date, and may treat a bare "Wed" as a typo. Costs ~1 token. const dow = new Intl.DateTimeFormat("en-US", { timeZone: timezone, weekday: "short" }).format( now, ); - return `[Current Date: ${dow} ${formatted}] ${message}`; + return `[${dow} ${formatted}] ${message}`; } /** diff --git a/src/gateway/server-methods/agent.test.ts b/src/gateway/server-methods/agent.test.ts index e572d2abc..669a8aa07 100644 --- a/src/gateway/server-methods/agent.test.ts +++ b/src/gateway/server-methods/agent.test.ts @@ -163,7 +163,7 @@ describe("gateway agent handler", () => { await vi.waitFor(() => expect(mocks.agentCommand).toHaveBeenCalled()); const callArgs = mocks.agentCommand.mock.calls[0][0]; - expect(callArgs.message).toBe("[Current Date: Wed 2026-01-28 20:30 EST] Is it the weekend?"); + expect(callArgs.message).toBe("[Wed 2026-01-28 20:30 EST] Is it the weekend?"); mocks.loadConfigReturn = {}; vi.useRealTimers();