a digital entity named phi that roams bsky phi.zzstoatzz.io
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: relative_when tolerates tz-naive timestamps

legacy memory rows (observations, interactions) were written with
`datetime.now().isoformat()` — no tz info. relative_when used
`datetime.now(UTC) - parsed` which raises TypeError on tz-naive operands:

can't subtract offset-naive and offset-aware datetimes

consequence: build_user_context partially rendered the OBSERVATIONS
block header, then threw inside the render loop, landing in the except
branch that appends the "no previous interactions" fallback. phi saw
both the header and the fallback — confusing, and per-user memory was
effectively silenced.

fix: when fromisoformat returns a naive datetime, assume UTC. matches
how the legacy rows were written (the code calling datetime.now() was
running in UTC on fly). new writes should migrate to tz-aware over time
but that's a separate concern — stale rows will keep existing for
months and the parser needs to handle them.

test: new relative_when case for tz-naive input.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>

+15 -1
+6 -1
src/bot/utils/time.py
··· 17 17 paginate ages: seconds → minutes → hours (with one decimal under 10) → 18 18 days (one decimal under 10) → months → years. 19 19 20 - Returns '' on parse failure or future timestamps. 20 + Returns '' on parse failure or future timestamps. Handles both tz-aware 21 + ('2026-04-20T15:00:00+00:00' / '...Z') and tz-naive ('2026-04-20T15:00:00') 22 + inputs — naive values are treated as UTC (which is how legacy rows in 23 + turbopuffer were written: `datetime.now().isoformat()` without tz info). 21 24 """ 22 25 try: 23 26 ts = datetime.fromisoformat(iso_ts.replace("Z", "+00:00")) 24 27 except (ValueError, TypeError): 25 28 return "" 29 + if ts.tzinfo is None: 30 + ts = ts.replace(tzinfo=UTC) 26 31 delta = (datetime.now(UTC) - ts).total_seconds() 27 32 if delta < 0: 28 33 return ""
+9
tests/test_relative_when.py
··· 81 81 ) 82 82 s = relative_when(z_form) 83 83 assert s.endswith("m ago") 84 + 85 + 86 + def test_tz_naive_handled_as_utc(): 87 + # Legacy memory rows used `datetime.now().isoformat()` with no tz info. 88 + # Those tz-naive strings must not raise; should be treated as UTC. 89 + naive = datetime.now(UTC).replace(tzinfo=None, microsecond=0).isoformat() 90 + s = relative_when(naive) 91 + # Should parse cleanly; age is "0s ago" or near-zero. 92 + assert s.endswith("s ago") or s.endswith("m ago")