From 9d2d43855c345c59fa8b6c8f57ba0e63c4ca2758 Mon Sep 17 00:00:00 2001 From: RainbowBird Date: Sat, 9 May 2026 00:38:50 +0800 Subject: [PATCH] refactor(chat): update datetime prefixing logic for user messages and improve formatting --- packages/stage-ui/src/stores/chat.ts | 20 +++++----- .../src/stores/chat/datetime-prefix.ts | 40 ++++++++----------- 2 files changed, 27 insertions(+), 33 deletions(-) diff --git a/packages/stage-ui/src/stores/chat.ts b/packages/stage-ui/src/stores/chat.ts index 6e804e2d0..925027128 100644 --- a/packages/stage-ui/src/stores/chat.ts +++ b/packages/stage-ui/src/stores/chat.ts @@ -350,28 +350,28 @@ export const useChatOrchestratorStore = defineStore('chat-orchestrator', () => { ], }) - // Per-message datetime injection (replaces the old `` XML block): - // every user/assistant message gets a `[YYYY-MM-DD HH:MM]` prefix - // derived from its persisted `createdAt`. The full date appears on every - // turn so the model can read "today" from the most recent message; the - // system prompt itself stays 100% static for permanent KV-cache reuse. - // Legacy entries without a persisted `createdAt` fall back to "now" - // rather than a fabricated older timestamp. + // Inject `[YYYY-MM-DD HH:MM]` prefix only into user messages, derived + // from their persisted `createdAt`. Assistant messages stay clean — + // otherwise the model learns the format and mirrors it back into its + // own replies (e.g. `[2026-05-09 00:23] > ...`). The model still reads + // "today" from the latest user message, and the system prompt stays + // 100% static for permanent KV-cache reuse. Legacy entries without a + // persisted `createdAt` fall back to "now" rather than a fabricated + // older timestamp. // See `./chat/datetime-prefix.ts` for the rationale. const nowTs = Date.now() const newMessages = sessionMessagesForSend.map((msg) => { const { context: _context, id: _id, createdAt, ...withoutContext } = msg const rawMessage = toRaw(withoutContext) - const ts = createdAt ?? nowTs if (rawMessage.role === 'user') { - return prependTextToContent(rawMessage, formatTimePrefix(ts)) + return prependTextToContent(rawMessage, formatTimePrefix(createdAt ?? nowTs)) } if (rawMessage.role === 'assistant') { const { slices: _slices, tool_results: _toolResults, categorization: _categorization, ...rest } = rawMessage as ChatAssistantMessage - return prependTextToContent(toRaw(rest), formatTimePrefix(ts)) + return toRaw(rest) } return rawMessage diff --git a/packages/stage-ui/src/stores/chat/datetime-prefix.ts b/packages/stage-ui/src/stores/chat/datetime-prefix.ts index bdee72b1b..f3f9edaa3 100644 --- a/packages/stage-ui/src/stores/chat/datetime-prefix.ts +++ b/packages/stage-ui/src/stores/chat/datetime-prefix.ts @@ -6,14 +6,16 @@ * invalidated KV-cache prefixes on every send). * * Strategy: - * - Each user/assistant message is prefixed with `[YYYY-MM-DD HH:MM]` derived - * from its persisted `createdAt`. Stored timestamps never change, so the - * prefixed history stays byte-stable across turns and accumulates KV-cache - * prefix matches. - * - The full date is included on every message so the model can infer "today" - * from the most recent message — there is no separate system-prompt date - * anchor, which keeps the system prompt 100% static and permanently - * cacheable across turns and across day boundaries. + * - Only user messages are prefixed with `[YYYY-MM-DD HH:MM]` derived from + * their persisted `createdAt`. Assistant messages stay clean — prefixing + * them caused models to learn the format and emit `[date] > ...` in their + * own replies. + * - Stored timestamps never change, so the prefixed user history stays + * byte-stable across turns and accumulates KV-cache prefix matches. + * - The full date is included on every user message so the model can infer + * "today" from the most recent user turn — there is no separate + * system-prompt date anchor, which keeps the system prompt 100% static and + * permanently cacheable across turns and across day boundaries. * * Format choice: * - `[YYYY-MM-DD HH:MM]` is ISO-like, structurally compact (~17 chars), and @@ -26,23 +28,18 @@ * correlates with verbatim copy-back. */ -const DATE_TIME = new Intl.DateTimeFormat('en-CA', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - hour12: false, -}) +import { format } from 'date-fns' /** * Formats a timestamp as `[YYYY-MM-DD HH:MM] ` in the user's local timezone. * * Use when: - * - Annotating user/assistant messages so the model has a concrete time - * anchor on every turn — historic and current alike use the same shape so - * that prefix-cache stays valid when a "current" turn becomes "historic" on + * - Annotating user messages so the model has a concrete time anchor on + * every turn — historic and current user turns use the same shape so that + * prefix-cache stays valid when a "current" turn becomes "historic" on * the next send. + * - Not used for assistant messages — that caused the model to mirror the + * prefix into its own output. * * Returns: * - String including a trailing space, e.g. `"[2026-04-25 18:47] "`. @@ -54,8 +51,5 @@ const DATE_TIME = new Intl.DateTimeFormat('en-CA', { * - "[2026-04-25 18:47] " */ export function formatTimePrefix(createdAt: number): string { - // Intl en-CA locale uses ISO-style `YYYY-MM-DD, HH:MM`. Strip the comma to - // produce the bracketed `YYYY-MM-DD HH:MM` form. - const formatted = DATE_TIME.format(new Date(createdAt)).replace(', ', ' ') - return `[${formatted}] ` + return `[${format(createdAt, 'yyyy-MM-dd HH:mm')}] ` }