-
Notifications
You must be signed in to change notification settings - Fork 15
Description
Ran into an issue while building a feature for td today. Basically, it looks like sidecar and td are (maybe?) using different formats to write to the same column (action_log.timestamp)
Claude Code helped me write it up so apologies if it's a bit wordy ...
I'm happy to tackle this problem if you feel it makes sense ... let me know!
Problem
The action_log.timestamp column (type DATETIME, stored as TEXT in SQLite) contains timestamps written in two incompatible string formats depending on which process wrote the row:
td commands (sessions like ses_c54218) produce Go's time.Time.String() format:
2026-02-18 12:48:02.073352 -0500 EST m=+0.111433043
sidecar (session sidecar) produces RFC3339 UTC:
2026-02-18T15:22:14Z
Because SQLite stores DATETIME as text and compares lexicographically, any query using WHERE timestamp >= ? or ORDER BY timestamp produces wrong results when the table contains rows from both writers.
Root Cause
td passes raw time.Time values to SQLite query parameters:
// internal/db/activity.go — LogAction
action.Timestamp = time.Now()
_, err = db.conn.Exec(`INSERT INTO action_log (..., timestamp, ...) VALUES (..., ?, ...)`,
..., action.Timestamp)The modernc.org/sqlite driver serializes time.Time by calling Go's .String() method, which produces a format that:
- Uses a space separator between date and time (not
T) - Includes the timezone abbreviation (
EST,UTC, etc.) - Includes the monotonic clock reading (
m=+0.111433043) - Is not parseable by SQLite's built-in
datetime()/strftime()functions
The sidecar, by contrast, explicitly formats timestamps as RFC3339 UTC strings before writing.
Why This Matters
SQLite compares these as raw text. At string position 10:
- td format:
' '(space, ASCII 32) - Sidecar format:
'T'(ASCII 84)
Since 'T' > ' ', every sidecar-written row compares as "newer" than every td-written row, regardless of actual chronological order. This means:
WHERE timestamp >= ?with a td-origin parameter returns all sidecar rowsORDER BY timestampinterleaves rows incorrectly- Any future code that compares timestamps across writers will silently produce wrong results
Observed Impact
Webhook dispatch used GetActionsSince(preRunTimestamp) to detect actions created during a command. Due to the format mismatch, every td command's webhook delivery included all sidecar-written actions (not just new ones), producing duplicate webhook payloads. This was fixed by switching to rowid-based comparison (commit 50042a0), but the underlying format problem remains.
Evidence
-- td-written rows:
SELECT timestamp FROM action_log WHERE session_id LIKE 'ses_%' LIMIT 3;
-- 2026-02-18 12:48:02.073352 -0500 EST m=+0.111433043
-- 2026-02-18 12:48:02.027095 -0500 EST m=+0.065175793
-- 2026-02-18 12:47:58.909085 -0500 EST m=+0.071497459
-- sidecar-written rows:
SELECT timestamp FROM action_log WHERE session_id = 'sidecar' LIMIT 3;
-- 2026-02-18T15:22:14Z
-- 2026-02-18T15:22:20Z
-- 2026-02-18T15:22:30ZSuggested Fix
All action_log writers in td should explicitly format timestamps as RFC3339 UTC strings before passing them to SQLite, instead of relying on the driver's serialization of time.Time:
// Before (driver calls time.Time.String()):
db.conn.Exec(`INSERT INTO action_log (..., timestamp) VALUES (..., ?)`, time.Now())
// After (explicit RFC3339 UTC):
db.conn.Exec(`INSERT INTO action_log (..., timestamp) VALUES (..., ?)`,
time.Now().UTC().Format(time.RFC3339Nano))This matches what the sidecar already does, making all rows comparable.
Affected files (non-test, INSERT INTO action_log):
internal/db/activity.go—LogAction,AddLog,AddHandoff,AddCommentinternal/db/issues_logged.go— 4 insertsinternal/db/boards_logged.go— 6 insertsinternal/db/relations_logged.go— 4 insertsinternal/db/work_sessions.go— 4 insertsinternal/db/notes.go— 3 inserts