refactor(chat): update datetime prefixing logic for user messages and improve formatting

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