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):
// 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
@@ -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')}] `
}