You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Wire up the timezone: seed it, honour it in the UI, and stop subordinate_status emitting raw UTC #345
c4394ad made agents speak the user's local time, sourced from users.preferences.timezone (the account blob, reused from #211; no migration; validated by isValidTimeZone from lib/cron-time.ts, so the platform has one timezone vocabulary). It also chose local-zone prose, which means SystemMessage.tsx needs no change — #336's local rendering and the agent's prose agree for free.
Three loose ends remain, each small and each in a lane that was closed at the time.
1. Nothing seeds the timezone, so "unset" is the standing case
preferences.timezone starts empty and there is no prompt to fill it. Until an owner discovers the field, every agent still says UTC — the exact symptom #329 was filed for, just now with a fix that nobody has switched on.
Seed it from Intl.DateTimeFormat().resolvedOptions().timeZone on first load via PUT /v1/preferences. That makes unset a transient state rather than a permanent one.
Seed, do not bind.#329 deliberately rejected the browser value as the source of truth — it changes when you travel, is absent on cron/pump/MCP paths, and cannot be corrected by the owner. Writing it once when empty is the useful half; re-asserting it on every load would reintroduce all three problems.
2. The UI ignores an explicit override
lib/messageStamp.ts's stampTitle uses toLocaleString(undefined, …) — the browser's zone, not the stored one. So an owner who sets a timezone different from their machine sees it honoured in the agent's prose and ignored in the UI, which is a two-clock defect again, one layer down.
Pass preferences.timezone from GET /v1/preferences as the timeZone option.
3. subordinate_status still emits raw UTC
lib/connectors/supervision.ts:254 emits asOf: new Date().toISOString() — this is the actual source of the "22:33:34 UTC" in the incident that prompted #329. formatInZone and utcOffsetLabel are exported from lib/agent-clock.ts for exactly this.
Worth checking the same pattern elsewhere: anywhere a tool payload carries a bare ISO string, a model will read it aloud verbatim.
Why together
All three are "the fix landed but the value never reaches the user". Individually each looks cosmetic; together they are the difference between #329 being done and #329 being available.
Acceptance
A new owner gets a correct local time without knowing the setting exists.
An owner who overrides their machine's zone sees the override in both prose and UI.
No tool payload hands a model a bare UTC timestamp to read out.
Shipped in c039562 — CI green (run 31139852805), API + Host workers deployed.
1. Seed.store/console/src/lib/accountTimezone.ts. AuthProvider fires loadAccountTimeZone() at the app's single "we know who this is" moment; it GETs /v1/preferences and PUTs the machine's zone only when timezoneSeed(stored, machine) returns non-null. That function returns null the instant anything is stored, so travelling never rewrites a set zone — seed, not bind. It also declines a browser that resolves to UTC/Etc/UTC/GMT: that is an unconfigured machine, not an answer, and recording it would collapse the undefined vs "UTC" distinction parseAccountPreferences keeps on purpose (the two render the same hour anyway, so declining costs nothing and keeps the honest hedge). Memoised on the in-flight promise, so StrictMode's double effect cannot double-PUT. Preferences gains a Timezone card — override, live "it is 08:34 there" confirmation, and "Not set" to clear back to unset.
2. UI override.stampTitle(iso, timeZone?) now takes the stored zone and SystemMessage passes it via useAccountTimeZone(). Unresolvable zone falls back to the browser's rather than to an empty title.
3. ISO sweep.localStamp(at, zone) added to the pure lib/agent-clock.ts — renders in the owner's zone, or an explicitly-labelled UTC wall clock when unset, never a bare ISO/epoch. Applied to:
check_delegation: ...run was spreading raw epoch milliseconds for startedAt/finishedAt/lastProgressAt — the same defect in different clothes.
renderTerminalLine's "last saved snapshot (as of …)", which put a raw D1 timestamp straight into the chat prompt.
get_activity (storage-tools.ts) — an activity log is a timeline the model narrates back, so its stamps are the most likely of any tool's to be read aloud.
Plus a TIMES_LEGEND in the payload so a model does not helpfully convert an already-local time back to UTC. Formatting happens at the payload boundary, not inside summarizeSubordinates, which sorts and trims on updatedAt — a test pins that ordering.
Left alone deliberately: timestamps written into stored records (tools.ts), and list_files' date-only column.
accountTimeZone(env, userId) moved to its own leaf module (lib/account-timezone.ts) — supervision cannot import tool-registry without closing a cycle.
Unset vs UTC stayed distinguishable end to end: parseAccountPreferences untouched, the route still 400s an unresolvable zone and still clears on null/"", and the seed refuses to manufacture a UTC answer.
Activity
fix(api,console): the timezone reaches the user, not just the schema (#…
serge-ivo commentedon Aug 7, 2026
Shipped in c039562 — CI green (run 31139852805), API + Host workers deployed.
1. Seed.
store/console/src/lib/accountTimezone.ts.AuthProviderfiresloadAccountTimeZone()at the app's single "we know who this is" moment; it GETs/v1/preferencesand PUTs the machine's zone only whentimezoneSeed(stored, machine)returns non-null. That function returns null the instant anything is stored, so travelling never rewrites a set zone — seed, not bind. It also declines a browser that resolves toUTC/Etc/UTC/GMT: that is an unconfigured machine, not an answer, and recording it would collapse theundefinedvs"UTC"distinctionparseAccountPreferenceskeeps on purpose (the two render the same hour anyway, so declining costs nothing and keeps the honest hedge). Memoised on the in-flight promise, so StrictMode's double effect cannot double-PUT. Preferences gains a Timezone card — override, live "it is 08:34 there" confirmation, and "Not set" to clear back to unset.2. UI override.
stampTitle(iso, timeZone?)now takes the stored zone andSystemMessagepasses it viauseAccountTimeZone(). Unresolvable zone falls back to the browser's rather than to an empty title.3. ISO sweep.
localStamp(at, zone)added to the purelib/agent-clock.ts— renders in the owner's zone, or an explicitly-labelled UTC wall clock when unset, never a bare ISO/epoch. Applied to:subordinate_status:asOf(the literal source of the "22:33:34 UTC" in the Agents report timestamps in UTC because nothing ever told them the user's timezone — and no prompt has a clock #329 incident),work[].updatedAt,connectivity.lastSeenAt,acts[].at.check_delegation:...runwas spreading raw epoch milliseconds forstartedAt/finishedAt/lastProgressAt— the same defect in different clothes.renderTerminalLine's "last saved snapshot (as of …)", which put a raw D1 timestamp straight into the chat prompt.get_activity(storage-tools.ts) — an activity log is a timeline the model narrates back, so its stamps are the most likely of any tool's to be read aloud.Plus a
TIMES_LEGENDin the payload so a model does not helpfully convert an already-local time back to UTC. Formatting happens at the payload boundary, not insidesummarizeSubordinates, which sorts and trims onupdatedAt— a test pins that ordering.Left alone deliberately: timestamps written into stored records (
tools.ts), andlist_files' date-only column.accountTimeZone(env, userId)moved to its own leaf module (lib/account-timezone.ts) — supervision cannot importtool-registrywithout closing a cycle.Unset vs UTC stayed distinguishable end to end:
parseAccountPreferencesuntouched, the route still 400s an unresolvable zone and still clears onnull/"", and the seed refuses to manufacture a UTC answer.fix(console): the message header reserves the corner its action butto…
fix(supervision): the roster fits, and the count rides in front of it…