mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-24 21:41:08 +02:00
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:
parent
f8f5c8614f
commit
15d484980d
7 changed files with 67 additions and 17 deletions
|
|
@ -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 },
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
34
apps/x/packages/shared/src/time.ts
Normal file
34
apps/x/packages/shared/src/time.ts
Normal 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',
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue