TL;DR
The Captured: line returned by search_thoughts, list_thoughts, and thought_stats (plus the date prefix in stats and the email-import display) is rendered server-side via Date.toLocaleDateString(). Because the Supabase Edge Functions run in UTC, users in negative-offset zones see the next calendar day for any capture made after their local ~17:00–20:00. A 21:25 capture in Chile (UTC-04) becomes 01:25 UTC next day → displays as "tomorrow".
Reproduction
Any timezone with non-zero offset from UTC, capturing close to local midnight or earlier-evening for negative offsets. Specific case:
- Wall-clock at capture: 2026-05-29 21:25 Chile (UTC-04:00)
- Stored
created_at (Postgres timestamptz): 2026-05-30 01:25:00+00
toLocaleDateString() on the Edge Function runtime (UTC) → "5/30/2026"
- User sees
Captured: 5/30/2026 (the day after they captured)
Root cause
Storage is fine — timestamptz preserves the instant correctly. The truncation to date-only happens in the output formatter:
server/index.ts:38 — datePrefix for stats header
server/index.ts:256 — Captured: line in search_thoughts response
server/index.ts:342 — [date] prefix in list_thoughts response
server/index.ts:407-409 — stats date range
integrations/kubernetes-deployment/index.ts:75, 301, 401, 475-477 — same pattern duplicated in the K8s deployment variant
recipes/email-history-import/pull-gmail.ts:883 — date column in email import display
All call new Date(t.created_at).toLocaleDateString() server-side in a UTC runtime → loses both time-of-day and the user's local offset.
Why this likely hasn't been flagged before
Hypotheses (happy to be wrong):
- Most reported users seem to be on US East/Central, where the off-by-one only triggers for captures between ~20:00–midnight local. Easy to not notice if your typical capture pattern is daytime.
- The dashboard parses
Captured: ... back into createdAt via dashboards/open-brain-dashboard/src/lib/api.ts:179-180 as a date string. So even if the format changes, the dashboard would silently break unless updated in sync.
- Date-only is "good enough" for most semantic recall — the bug only really bites when grouping by year (autobiography/wiki synthesis) or when users in far-from-UTC zones notice the day mismatch.
Fix paths (ordered by invasiveness)
| # |
Change |
Backwards-compat |
Notes |
| 1 |
Add optional captured_at (ISO 8601 with offset) parameter to capture_thought / ingest-thought |
Yes (additive) |
Lets clients with timezone knowledge (Outlook plugin, mobile capture, backfill scripts) preserve local time. Defaults to now() server-side. Zero behavioral change for existing callers. |
| 2 |
Change output formatter to emit full ISO 8601 instead of toLocaleDateString() |
Breaks dashboard (parses date string) and any external consumer of the Captured: line. Needs coordinated PR across server + dashboard + recipes. |
Resolves the display bug for all timezones. |
| 3 |
Store with explicit client-provided offset (captured_at from #1) so future formatters can honor original local time |
Yes (depends on #1) |
The cleanest long-term fix — now() in the Edge Function always loses the client's timezone. |
#1 alone fixes the underlying data-loss problem without changing any display contract. #2 fixes the visible symptom but is the biggest blast radius.
Bonus: fork-relevant gotcha
Not relevant on Postgres, but worth flagging for anyone considering a SQL Server port: pyodbc + ODBC Driver 18 has no default output converter for DATETIMEOFFSET (SQL type -155). Inputs work, but the first SELECT returns 'ODBC SQL type -155 is not yet supported'. Fix is to register conn.add_output_converter(-155, ...) with a 20-byte struct unpack — see pyodbc issue #134.
Happy to open a PR for fix path #1 if it would be welcome — it's the smallest, most reversible change of the three.
TL;DR
The
Captured:line returned bysearch_thoughts,list_thoughts, andthought_stats(plus the date prefix in stats and the email-import display) is rendered server-side viaDate.toLocaleDateString(). Because the Supabase Edge Functions run in UTC, users in negative-offset zones see the next calendar day for any capture made after their local ~17:00–20:00. A 21:25 capture in Chile (UTC-04) becomes01:25 UTCnext day → displays as "tomorrow".Reproduction
Any timezone with non-zero offset from UTC, capturing close to local midnight or earlier-evening for negative offsets. Specific case:
created_at(Postgrestimestamptz):2026-05-30 01:25:00+00toLocaleDateString()on the Edge Function runtime (UTC) →"5/30/2026"Captured: 5/30/2026(the day after they captured)Root cause
Storage is fine —
timestamptzpreserves the instant correctly. The truncation to date-only happens in the output formatter:server/index.ts:38—datePrefixfor stats headerserver/index.ts:256—Captured:line insearch_thoughtsresponseserver/index.ts:342—[date]prefix inlist_thoughtsresponseserver/index.ts:407-409— stats date rangeintegrations/kubernetes-deployment/index.ts:75, 301, 401, 475-477— same pattern duplicated in the K8s deployment variantrecipes/email-history-import/pull-gmail.ts:883— date column in email import displayAll call
new Date(t.created_at).toLocaleDateString()server-side in a UTC runtime → loses both time-of-day and the user's local offset.Why this likely hasn't been flagged before
Hypotheses (happy to be wrong):
Captured: ...back intocreatedAtviadashboards/open-brain-dashboard/src/lib/api.ts:179-180as a date string. So even if the format changes, the dashboard would silently break unless updated in sync.Fix paths (ordered by invasiveness)
captured_at(ISO 8601 with offset) parameter tocapture_thought/ingest-thoughtnow()server-side. Zero behavioral change for existing callers.toLocaleDateString()Captured:line. Needs coordinated PR across server + dashboard + recipes.captured_atfrom #1) so future formatters can honor original local timenow()in the Edge Function always loses the client's timezone.#1 alone fixes the underlying data-loss problem without changing any display contract. #2 fixes the visible symptom but is the biggest blast radius.
Bonus: fork-relevant gotcha
Not relevant on Postgres, but worth flagging for anyone considering a SQL Server port:
pyodbc+ ODBC Driver 18 has no default output converter forDATETIMEOFFSET(SQL type-155). Inputs work, but the firstSELECTreturns'ODBC SQL type -155 is not yet supported'. Fix is to registerconn.add_output_converter(-155, ...)with a 20-byte struct unpack — see pyodbc issue #134.Happy to open a PR for fix path #1 if it would be welcome — it's the smallest, most reversible change of the three.