Skip to content

Wire up the timezone: seed it, honour it in the UI, and stop subordinate_status emitting raw UTC #345

Closed

Description

@serge-ivo

Three wiring gaps left by #329

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.

Activity

  1. serge-ivo commented on Aug 7, 2026

    @serge-ivo
    ContributorAuthor

    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:

    • 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: ...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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions