Commit c4a723f
committed
ingest: fix tailer schema/idempotency/timestamp bugs; add Cowork coverage + reimport recovery
The passive tailer (Tier 1a) has been silently non-functional since it was
written: every usage_events row in the live DB had source='hook', zero from
source='tailer', even though the tailer was running and reading every
tracked file to EOF. Three separate bugs, found while investigating why
desktop-app Cowork sessions weren't showing up:
1. internal/ingest/parser.go read usage/session_id/message_id/model off the
top level of each JSON line. Real Claude Code transcripts nest usage/id/
model under a "message" object with camelCase "sessionId" as a sibling —
the same shape cmd/clusage-cli/hook.go already parses correctly, with a
comment noting the original parser was written against a mock schema
that never matched a real transcript. parser_test.go and tailer_test.go
were fixtured against the same wrong flat schema, which is exactly what
let this hide: the parser and its tests agreed with each other, just not
with reality.
2. The tailer's INSERT lacked the idempotent-duplicate handling
server.go's handleLog already has for hook re-posts. Once (1) was fixed,
every message the hook got to first collided on the
UNIQUE(session_id, message_id) constraint and was logged as an ERROR
plus a permanent parse_errors row, on every restart's catch-up poll.
Moved the constraint-violation check into store.IsUniqueConstraintViolation
(shared by both ingest paths) and made the tailer treat a collision as
idempotent re-delivery: debug-log, skip, still advance the offset.
3. The tailer's INSERT passed occurred_at as a raw time.Time instead of
store.FormatTime(...), the one documented invariant every other insert
site in the codebase already follows. modernc.org/sqlite serializes an
unconverted time.Time via Go's default String() ("2026-07-09 23:02:59.894
+0000 UTC", sometimes with a monotonic-clock "m=+..." suffix from a bare
time.Now()), which SQLite's strftime() silently fails to parse (NULL, not
an error). The dashboard's session/weekly burn-down charts bucket events
with strftime('%s', occurred_at), so every tailer-sourced row broke that
query for its bucket — the "converting NULL to int64" errors and the
"No active window" dashboard symptom. Fixed the insert; TestTailerStores
QueryableTimestamp pins the failure mode directly against strftime.
Cowork ("local agent mode") session coverage:
Each Cowork session runs against its own private, sandboxed .claude home
nested several directories under %APPDATA%\Claude\local-agent-mode-sessions,
completely separate from the user's real ~/.claude/projects that the
primary tailer watches — same JSONL schema, just invisible to it. Added
claude.cowork_sessions_dir (default resolved via APPDATA) and a second
Tailer instance rooted there; its own poll's recursive filepath.Walk
discovers new sessions automatically as they're created, no hook needed.
cmd/trayapp/main.go's tailerGroup aggregates CaughtUp()/Stop() across both
tailers so /healthz and graceful shutdown treat them uniformly (shutdown
previously only knew about the first tailer, a latent bug in the same area
this change touches).
Bulk reimport recovery tool:
A tailer's byte offset records how far a file was *read*, not whether the
lines in that range actually produced a usage_event — bug (1) above left
every pre-existing session's offset sitting at EOF for content that was
never successfully recorded, with no per-line marker to say which lines
those were. There's no way to selectively repair "just the missing ones"
without already knowing which files/sessions are affected. Added:
- store.DeleteAllTailerOffsets: wipes every persisted offset.
- ingest.Tailer.Reimport: wipes offsets (in-memory + DB) and re-walks
the tailer's root from byte 0.
- POST /admin/reimport: runs Reimport in the background (202 immediately,
409 if one's already in flight), for every tailer via tailerGroup.
- clusage-cli reimport [--wait] [--wait-timeout 5m]: triggers it, with
--wait polling /healthz's tailer_caught_up.
Deliberately slow and wasteful — re-reads every tracked transcript
byte-for-byte and leans entirely on UNIQUE(session_id, message_id) dedup to
discard anything already recorded. Recovery-only, not routine maintenance.
Verified live: recovered 2,426 tailer-sourced events with timestamps
predating the fix, with zero duplication.
Tests added at every layer (parser, tailer, store, server) including
TestTailerReimportRecoversPastOffset, which reproduces the exact bug class
Reimport exists to fix: an offset already at EOF with one message genuinely
missing, verifying recovery without duplicating what's already there.
Docs: docs/data-sources.md (Cowork second root, recovery section),
docs/container-cli.md (reimport subcommand), docs/configuration.md
(cowork_sessions_dir).1 parent b2107f2 commit c4a723f
15 files changed
Lines changed: 783 additions & 68 deletions
File tree
- cmd
- clusage-cli
- trayapp
- docs
- internal
- config
- ingest
- server
- store
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
| 40 | + | |
| 41 | + | |
40 | 42 | | |
41 | 43 | | |
42 | 44 | | |
| |||
56 | 58 | | |
57 | 59 | | |
58 | 60 | | |
| 61 | + | |
59 | 62 | | |
60 | 63 | | |
61 | 64 | | |
| |||
333 | 336 | | |
334 | 337 | | |
335 | 338 | | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
336 | 409 | | |
337 | 410 | | |
338 | 411 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
34 | 68 | | |
35 | 69 | | |
36 | 70 | | |
| |||
108 | 142 | | |
109 | 143 | | |
110 | 144 | | |
111 | | - | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
112 | 159 | | |
113 | 160 | | |
114 | | - | |
115 | | - | |
116 | | - | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
117 | 164 | | |
118 | 165 | | |
119 | 166 | | |
| |||
205 | 252 | | |
206 | 253 | | |
207 | 254 | | |
208 | | - | |
| 255 | + | |
209 | 256 | | |
210 | 257 | | |
211 | 258 | | |
212 | 259 | | |
213 | 260 | | |
214 | 261 | | |
215 | 262 | | |
216 | | - | |
| 263 | + | |
217 | 264 | | |
218 | 265 | | |
219 | 266 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
24 | 30 | | |
25 | 31 | | |
26 | 32 | | |
| |||
112 | 118 | | |
113 | 119 | | |
114 | 120 | | |
115 | | - | |
116 | | - | |
117 | | - | |
118 | | - | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
119 | 126 | | |
120 | 127 | | |
121 | 128 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
85 | 85 | | |
86 | 86 | | |
87 | 87 | | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
88 | 120 | | |
89 | 121 | | |
90 | 122 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
74 | 74 | | |
75 | 75 | | |
76 | 76 | | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
77 | 120 | | |
78 | 121 | | |
79 | 122 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
26 | 34 | | |
27 | 35 | | |
28 | 36 | | |
| |||
71 | 79 | | |
72 | 80 | | |
73 | 81 | | |
| 82 | + | |
74 | 83 | | |
75 | 84 | | |
76 | 85 | | |
| |||
107 | 116 | | |
108 | 117 | | |
109 | 118 | | |
| 119 | + | |
110 | 120 | | |
111 | 121 | | |
112 | 122 | | |
| |||
146 | 156 | | |
147 | 157 | | |
148 | 158 | | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
149 | 174 | | |
150 | 175 | | |
151 | 176 | | |
| |||
0 commit comments