diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 711c5c27..57fd36b5 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -3054,7 +3054,18 @@ function App() { ...(chatMaxModelCalls !== undefined ? { maxModelCalls: chatMaxModelCalls } : {}), } const userMessageContextFor = (middlePane: Awaited>) => ({ - currentDateTime: new Date().toISOString(), + // Local wall-clock with explicit timezone, never toISOString: the model + // adopts this as its time frame, so a UTC "now" makes it quote email + // timestamps (which carry their own offsets) in UTC instead of local. + currentDateTime: `${new Date().toLocaleString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + timeZoneName: 'short', + })} (${Intl.DateTimeFormat().resolvedOptions().timeZone})`, middlePane: middlePane ?? { kind: 'empty' as const }, }) diff --git a/apps/x/packages/core/src/knowledge/sync_gmail.ts b/apps/x/packages/core/src/knowledge/sync_gmail.ts index e4dd956b..2de211e5 100644 --- a/apps/x/packages/core/src/knowledge/sync_gmail.ts +++ b/apps/x/packages/core/src/knowledge/sync_gmail.ts @@ -13,6 +13,7 @@ import { classifyThread, getUserEmail, type EmailCategory } from './classify_thr import { recordImportanceCorrection } from './email_importance_feedback.js'; import { recordCategoryCorrection } from './email_category_feedback.js'; import { notifyIfEnabled } from '../application/notification/notifier.js'; +import { formatTimestampForModel } from '@x/shared/dist/time.js'; // Configuration const SYNC_DIR = path.join(WorkDir, 'gmail_sync'); @@ -1157,7 +1158,7 @@ async function parseThreadSnapshot( const earlier = visibleMessages.slice(0, -1); const earlierSummary = earlier .map((msg) => { - const date = msg.date ? ` (${msg.date})` : ''; + const date = msg.date ? ` (${formatTimestampForModel(msg.date)})` : ''; const body = msg.body.replace(/\s+/g, ' ').slice(0, 500).trim(); return `${msg.from}${date}: ${body}`; }) @@ -1330,7 +1331,7 @@ async function processThread(auth: OAuth2Client, threadId: string, syncDir: stri const date = headers.find(h => h.name === 'Date')?.value || 'Unknown'; mdContent += `### From: ${from}\n`; - mdContent += `**Date:** ${date}\n\n`; + mdContent += `**Date:** ${formatTimestampForModel(date)}\n\n`; if (msg.payload) { const body = getBody(msg.payload); diff --git a/apps/x/packages/core/src/runtime/assembly/__snapshots__/compose-instructions.test.ts.snap b/apps/x/packages/core/src/runtime/assembly/__snapshots__/compose-instructions.test.ts.snap index 4a3dadc5..3099a208 100644 --- a/apps/x/packages/core/src/runtime/assembly/__snapshots__/compose-instructions.test.ts.snap +++ b/apps/x/packages/core/src/runtime/assembly/__snapshots__/compose-instructions.test.ts.snap @@ -6,7 +6,7 @@ exports[`composeSystemInstructions golden bytes > agent notes 1`] = ` # Hidden User Context User messages may include a hidden "# User Context" section before "# User Message". Treat it as runtime metadata captured when that specific user message was sent. The actual user-authored text starts under "# User Message". -Use "Current date and time" for temporal reasoning. +Use "Current date and time" for temporal reasoning; it reflects the user's local timezone. Always express dates and times in that local timezone: timestamps inside emails, web content, or tool output may carry other offsets (often UTC) — convert those to local time before repeating them. If Middle pane context is present, it reflects what the user had open at the time of that specific message and overrides earlier middle-pane references. If the conversation history references a different note or browser page, the user had since closed or navigated away from it. Do not treat earlier context as current. @@ -27,7 +27,7 @@ exports[`composeSystemInstructions golden bytes > base: no modes active 1`] = ` # Hidden User Context User messages may include a hidden "# User Context" section before "# User Message". Treat it as runtime metadata captured when that specific user message was sent. The actual user-authored text starts under "# User Message". -Use "Current date and time" for temporal reasoning. +Use "Current date and time" for temporal reasoning; it reflects the user's local timezone. Always express dates and times in that local timezone: timestamps inside emails, web content, or tool output may carry other offsets (often UTC) — convert those to local time before repeating them. If Middle pane context is present, it reflects what the user had open at the time of that specific message and overrides earlier middle-pane references. If the conversation history references a different note or browser page, the user had since closed or navigated away from it. Do not treat earlier context as current. @@ -44,7 +44,7 @@ exports[`composeSystemInstructions golden bytes > coach mode 1`] = ` # Hidden User Context User messages may include a hidden "# User Context" section before "# User Message". Treat it as runtime metadata captured when that specific user message was sent. The actual user-authored text starts under "# User Message". -Use "Current date and time" for temporal reasoning. +Use "Current date and time" for temporal reasoning; it reflects the user's local timezone. Always express dates and times in that local timezone: timestamps inside emails, web content, or tool output may carry other offsets (often UTC) — convert those to local time before repeating them. If Middle pane context is present, it reflects what the user had open at the time of that specific message and overrides earlier middle-pane references. If the conversation history references a different note or browser page, the user had since closed or navigated away from it. Do not treat earlier context as current. @@ -72,7 +72,7 @@ exports[`composeSystemInstructions golden bytes > code mode claude with cwd 1`] # Hidden User Context User messages may include a hidden "# User Context" section before "# User Message". Treat it as runtime metadata captured when that specific user message was sent. The actual user-authored text starts under "# User Message". -Use "Current date and time" for temporal reasoning. +Use "Current date and time" for temporal reasoning; it reflects the user's local timezone. Always express dates and times in that local timezone: timestamps inside emails, web content, or tool output may carry other offsets (often UTC) — convert those to local time before repeating them. If Middle pane context is present, it reflects what the user had open at the time of that specific message and overrides earlier middle-pane references. If the conversation history references a different note or browser page, the user had since closed or navigated away from it. Do not treat earlier context as current. @@ -105,7 +105,7 @@ exports[`composeSystemInstructions golden bytes > code mode codex without cwd 1` # Hidden User Context User messages may include a hidden "# User Context" section before "# User Message". Treat it as runtime metadata captured when that specific user message was sent. The actual user-authored text starts under "# User Message". -Use "Current date and time" for temporal reasoning. +Use "Current date and time" for temporal reasoning; it reflects the user's local timezone. Always express dates and times in that local timezone: timestamps inside emails, web content, or tool output may carry other offsets (often UTC) — convert those to local time before repeating them. If Middle pane context is present, it reflects what the user had open at the time of that specific message and overrides earlier middle-pane references. If the conversation history references a different note or browser page, the user had since closed or navigated away from it. Do not treat earlier context as current. @@ -138,7 +138,7 @@ exports[`composeSystemInstructions golden bytes > everything on 1`] = ` # Hidden User Context User messages may include a hidden "# User Context" section before "# User Message". Treat it as runtime metadata captured when that specific user message was sent. The actual user-authored text starts under "# User Message". -Use "Current date and time" for temporal reasoning. +Use "Current date and time" for temporal reasoning; it reflects the user's local timezone. Always express dates and times in that local timezone: timestamps inside emails, web content, or tool output may carry other offsets (often UTC) — convert those to local time before repeating them. If Middle pane context is present, it reflects what the user had open at the time of that specific message and overrides earlier middle-pane references. If the conversation history references a different note or browser page, the user had since closed or navigated away from it. Do not treat earlier context as current. @@ -285,7 +285,7 @@ exports[`composeSystemInstructions golden bytes > search enabled 1`] = ` # Hidden User Context User messages may include a hidden "# User Context" section before "# User Message". Treat it as runtime metadata captured when that specific user message was sent. The actual user-authored text starts under "# User Message". -Use "Current date and time" for temporal reasoning. +Use "Current date and time" for temporal reasoning; it reflects the user's local timezone. Always express dates and times in that local timezone: timestamps inside emails, web content, or tool output may carry other offsets (often UTC) — convert those to local time before repeating them. If Middle pane context is present, it reflects what the user had open at the time of that specific message and overrides earlier middle-pane references. If the conversation history references a different note or browser page, the user had since closed or navigated away from it. Do not treat earlier context as current. @@ -305,7 +305,7 @@ exports[`composeSystemInstructions golden bytes > video mode 1`] = ` # Hidden User Context User messages may include a hidden "# User Context" section before "# User Message". Treat it as runtime metadata captured when that specific user message was sent. The actual user-authored text starts under "# User Message". -Use "Current date and time" for temporal reasoning. +Use "Current date and time" for temporal reasoning; it reflects the user's local timezone. Always express dates and times in that local timezone: timestamps inside emails, web content, or tool output may carry other offsets (often UTC) — convert those to local time before repeating them. If Middle pane context is present, it reflects what the user had open at the time of that specific message and overrides earlier middle-pane references. If the conversation history references a different note or browser page, the user had since closed or navigated away from it. Do not treat earlier context as current. @@ -346,7 +346,7 @@ exports[`composeSystemInstructions golden bytes > voice input 1`] = ` # Hidden User Context User messages may include a hidden "# User Context" section before "# User Message". Treat it as runtime metadata captured when that specific user message was sent. The actual user-authored text starts under "# User Message". -Use "Current date and time" for temporal reasoning. +Use "Current date and time" for temporal reasoning; it reflects the user's local timezone. Always express dates and times in that local timezone: timestamps inside emails, web content, or tool output may carry other offsets (often UTC) — convert those to local time before repeating them. If Middle pane context is present, it reflects what the user had open at the time of that specific message and overrides earlier middle-pane references. If the conversation history references a different note or browser page, the user had since closed or navigated away from it. Do not treat earlier context as current. @@ -368,7 +368,7 @@ exports[`composeSystemInstructions golden bytes > voice output full 1`] = ` # Hidden User Context User messages may include a hidden "# User Context" section before "# User Message". Treat it as runtime metadata captured when that specific user message was sent. The actual user-authored text starts under "# User Message". -Use "Current date and time" for temporal reasoning. +Use "Current date and time" for temporal reasoning; it reflects the user's local timezone. Always express dates and times in that local timezone: timestamps inside emails, web content, or tool output may carry other offsets (often UTC) — convert those to local time before repeating them. If Middle pane context is present, it reflects what the user had open at the time of that specific message and overrides earlier middle-pane references. If the conversation history references a different note or browser page, the user had since closed or navigated away from it. Do not treat earlier context as current. @@ -430,7 +430,7 @@ exports[`composeSystemInstructions golden bytes > voice output summary 1`] = ` # Hidden User Context User messages may include a hidden "# User Context" section before "# User Message". Treat it as runtime metadata captured when that specific user message was sent. The actual user-authored text starts under "# User Message". -Use "Current date and time" for temporal reasoning. +Use "Current date and time" for temporal reasoning; it reflects the user's local timezone. Always express dates and times in that local timezone: timestamps inside emails, web content, or tool output may carry other offsets (often UTC) — convert those to local time before repeating them. If Middle pane context is present, it reflects what the user had open at the time of that specific message and overrides earlier middle-pane references. If the conversation history references a different note or browser page, the user had since closed or navigated away from it. Do not treat earlier context as current. @@ -495,7 +495,7 @@ exports[`composeSystemInstructions golden bytes > work directory 1`] = ` # Hidden User Context User messages may include a hidden "# User Context" section before "# User Message". Treat it as runtime metadata captured when that specific user message was sent. The actual user-authored text starts under "# User Message". -Use "Current date and time" for temporal reasoning. +Use "Current date and time" for temporal reasoning; it reflects the user's local timezone. Always express dates and times in that local timezone: timestamps inside emails, web content, or tool output may carry other offsets (often UTC) — convert those to local time before repeating them. If Middle pane context is present, it reflects what the user had open at the time of that specific message and overrides earlier middle-pane references. If the conversation history references a different note or browser page, the user had since closed or navigated away from it. Do not treat earlier context as current. diff --git a/apps/x/packages/core/src/runtime/assembly/compose-instructions.ts b/apps/x/packages/core/src/runtime/assembly/compose-instructions.ts index 2bfe37b0..89281177 100644 --- a/apps/x/packages/core/src/runtime/assembly/compose-instructions.ts +++ b/apps/x/packages/core/src/runtime/assembly/compose-instructions.ts @@ -25,7 +25,7 @@ const PROMPT_CAPABILITIES = [ const USER_CONTEXT_SYSTEM_INSTRUCTIONS = `# Hidden User Context User messages may include a hidden "# User Context" section before "# User Message". Treat it as runtime metadata captured when that specific user message was sent. The actual user-authored text starts under "# User Message". -Use "Current date and time" for temporal reasoning. +Use "Current date and time" for temporal reasoning; it reflects the user's local timezone. Always express dates and times in that local timezone: timestamps inside emails, web content, or tool output may carry other offsets (often UTC) — convert those to local time before repeating them. If Middle pane context is present, it reflects what the user had open at the time of that specific message and overrides earlier middle-pane references. If the conversation history references a different note or browser page, the user had since closed or navigated away from it. Do not treat earlier context as current. diff --git a/apps/x/packages/core/src/runtime/tools/domains/app.ts b/apps/x/packages/core/src/runtime/tools/domains/app.ts index 4af10d1c..c91abf8b 100644 --- a/apps/x/packages/core/src/runtime/tools/domains/app.ts +++ b/apps/x/packages/core/src/runtime/tools/domains/app.ts @@ -11,6 +11,7 @@ import { WorkDir } from "../../../config/config.js"; import { RowboatAppManifestSchema } from "@x/shared/dist/rowboat-app.js"; import { listApps } from "../../../apps/indexer.js"; import { listImportantThreads, searchThreads } from "../../../knowledge/sync_gmail.js"; +import { formatTimestampForModel } from "@x/shared/dist/time.js"; import { listTasks as listBackgroundTasks } from "../../../background-tasks/fileops.js"; import type { ISessions } from "../../sessions/api.js"; import { BuiltinToolsSchema } from "../types.js"; @@ -113,7 +114,9 @@ export const appNavigationTools: z.infer = { threadId: t.threadId, subject: t.subject ?? '(no subject)', from: t.from ?? '', - date: t.date ?? '', + // Local-timezone string for the model, not the raw + // header — otherwise it quotes email times in UTC. + date: formatTimestampForModel(t.date), unread: t.unread ?? false, summary: t.summary ? t.summary.slice(0, 200) : undefined, })); diff --git a/apps/x/packages/shared/src/index.ts b/apps/x/packages/shared/src/index.ts index 6661420e..6841e705 100644 --- a/apps/x/packages/shared/src/index.ts +++ b/apps/x/packages/shared/src/index.ts @@ -22,5 +22,6 @@ export * as notificationSettings from './notification-settings.js'; export * as turnLimits from './turn-limits.js'; export * as codeSessions from './code-sessions.js'; export * as channels from './channels.js'; +export * as time from './time.js'; export * as rowboatApp from './rowboat-app.js'; export { PrefixLogger }; diff --git a/apps/x/packages/shared/src/time.ts b/apps/x/packages/shared/src/time.ts new file mode 100644 index 00000000..691fc7d2 --- /dev/null +++ b/apps/x/packages/shared/src/time.ts @@ -0,0 +1,34 @@ +/** + * Format a timestamp into the user's local timezone for any text shown to the + * LLM (tool output, knowledge markdown, thread summaries). External sources + * carry their own offsets — email `Date:` headers are typically `+0000`, Slack + * uses epoch seconds, calendar APIs return UTC ISO strings. Handed to the model + * raw, it quotes them verbatim instead of converting — e.g. a 1:06 PM IST email + * described as "7:36 AM" in chat. Call this at every model-facing serialization + * boundary; the model's tendency to quote timestamps verbatim then produces + * correct output. The weekday is included because models are unreliable at + * day-of-week arithmetic. Uses the system timezone of the process it runs in. + * + * NOT for structured `date` fields that code later parses with `new Date(...)` + * — those must stay raw parseable values. + * + * Accepts a raw date string, epoch milliseconds, or a Date. Unparseable + * strings (e.g. 'Unknown') are returned unchanged; null/undefined and invalid + * Date/number inputs return ''. + */ +export function formatTimestampForModel(raw: string | number | Date | undefined | null): string { + if (raw === undefined || raw === null || raw === '') return ''; + const parsed = raw instanceof Date ? raw : new Date(raw); + if (Number.isNaN(parsed.getTime())) { + return typeof raw === 'string' ? raw : ''; + } + return parsed.toLocaleString('en-US', { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + timeZoneName: 'short', + }); +}