fix(x): show chat timestamps in the user's local timezone (#779)

* fix(x): show chat timestamps in the user's local timezone

The per-message "Current date and time" context was built with toISOString(), giving the model a UTC time frame with no zone. It then quoted email Date: headers (sent in +0000) in UTC — an email shown as 1:06 PM IST in the inbox was described as 7:36 AM in chat.

Use local wall-clock via toLocaleString with an explicit timeZoneName and the IANA zone (matching what the legacy engine already did) so the model converts offset-bearing timestamps to local time.

* fix(x): convert email timestamps to local time for the assistant

The "now" fix gave the model a local time frame, but email dates still reached it as raw RFC 2822 Date: headers (marketing mail is sent in +0000), so it kept quoting them in UTC — a 1:06 PM IST email described as 7:36 AM in chat.

Add formatEmailDateLocal() and apply it at the three model-facing serialization points: the read-view email tool, the thread-summary line, and the gmail-sync knowledge markdown (**Date:**). The structured date field on snapshots/messages is left raw, since the renderer parses it with new Date() and formats it client-side.

* fix(x): generalize local-time formatting for model-facing timestamps

Move formatEmailDateLocal out of sync_gmail into a shared
formatTimestampForModel() util — email is just the first source of
external timestamps; Slack/calendar serialization can now reuse the
same one-line call instead of growing per-source formatters. Also add
a system-prompt instruction to always express times in the user's
local timezone, as a fallback for raw dates inside email bodies, web
content, and third-party tool output that pre-conversion can't reach.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Harshvardhan Vatsa 2026-07-24 16:26:04 +05:30 committed by GitHub
parent f8f5c8614f
commit 15d484980d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 67 additions and 17 deletions

View file

@ -3054,7 +3054,18 @@ function App() {
...(chatMaxModelCalls !== undefined ? { maxModelCalls: chatMaxModelCalls } : {}),
}
const userMessageContextFor = (middlePane: Awaited<ReturnType<typeof buildMiddlePaneContext>>) => ({
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 },
})

View file

@ -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);

View file

@ -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.

View file

@ -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.

View file

@ -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<typeof BuiltinToolsSchema> = {
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,
}));

View file

@ -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 };

View file

@ -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',
});
}