Skip to content

action_log.timestamp inconsistency? #40

@boozedog

Description

@boozedog

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 rows
  • ORDER BY timestamp interleaves 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:30Z

Suggested 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.goLogAction, AddLog, AddHandoff, AddComment
  • internal/db/issues_logged.go — 4 inserts
  • internal/db/boards_logged.go — 6 inserts
  • internal/db/relations_logged.go — 4 inserts
  • internal/db/work_sessions.go — 4 inserts
  • internal/db/notes.go — 3 inserts

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions