feat(queue): reclaim the 84% of the queue nothing ever reclaimed - #117
Merged
Conversation
Addresses #103. `sync --compact`, and the column invariant that lived only in prose now runs in `doctor`. Measured on a real install: 34,492 lines, 5,595 unique keys, 83.8% superseded, 11 MB — and readQueueData re-reads and re-dedups ALL of it on every endpoint call, 6-8 per dashboard refresh, auto-refresh every 30s. The design risk is near zero because THE READERS ALREADY DEFINE THE OUTPUT. readQueueData keeps the last row per `source|model|hour_start`, so compaction keeps the last RAW LINE per that key — not a re-serialised row. Byte-identical API responses follow by construction, not by luck: the surviving bytes are the exact bytes the reader would have kept. There is a test that reproduces the reader's dedup and asserts its output is identical before and after. Unparseable lines are KEPT. They are invisible to every reader already, so dropping them changes no response — but a partial write worth investigating should survive a routine maintenance command. THREE THINGS FOUND WHILE BUILDING THIS. 1. The invariant as written is WRONG, and my check inherited the error. Running it against a real 34,922-row queue reported 8,236 violations — every one source=codex, and in every case the difference was exactly reasoning_output_tokens. The rows were right: codex and every-code fold reasoning INTO output_tokens, and computeRowCost (pricing/index.js:309) already made that distinction by charging their reasoning at zero. CLAUDE.md said "sum of all columns" with no exception. Corrected there, and expectedTotal() is now the single rule shared by doctor and the #104 parser harness, which had copied the same too-strict version. After the fix: 13 findings, all in 2 real codex buckets whose total exceeds its columns by 5,337. Left as a WARN — genuine data, not a check artefact. 2. AUTOMATIC compaction is NOT shipped, though #103 proposed it. Wired up first, it immediately rewrote an 11 MB production queue (34,924 -> 5,636 lines) during `npm test`. Sync now RECOMMENDS `--compact` past the threshold and does not act. Rewriting the user's data file unasked turns every path that reaches sync into a data-mutating path. 3. The reason it could: test/init-uninstall.test.js spawned the notify handler in a CHILD PROCESS with `env: {...process.env}`, so overriding process.env.HOME in the parent never reached it and the child resolved ~/.tokentracker. Pre-existing — it had been appending rows to the maintainer's real queue all along; compaction only made it visible. The helper now requires an isolated `home` and throws without one, with a test for that. Verified: ci:local no longer changes the real queue's mtime. Interrupt safety is write-then-rename, so the file is either entirely the old one or entirely the new one; a test forces the rename to fail and asserts the original is intact and no temp file is left behind. ci:local exit 0: 914 root tests, 256 dashboard.
This was referenced Jul 25, 2026
pitimon
pushed a commit
that referenced
this pull request
Jul 25, 2026
A flake I introduced in #117 and did not see until the hermes fixture run. Giving the notify handler an isolated HOME was the right fix — it stopped the child resolving the developer's real ~/.tokentracker. But the handler deliberately spawns a DETACHED background sync, and that sync now writes inside the test's own temp directory. The write can land after execFile's callback has fired, so the `fs.rm` in `finally` races it and fails with ENOTEMPTY. Cleanup retries instead of waiting on a process the handler detaches on purpose. 16 call sites, one helper. Verified by running the file 5 times: 19 pass, 0 fail, every run. A single green run proves nothing about a race. Also recording the process error that let this reach a commit: I ran `ci:local` and chained the commit after `echo "exit=$?"`, which always succeeds, so the `&&` did not gate on the red suite. The commit landed on a failing gate. Fixed by capturing the exit code into a variable and testing it. ci:local exit 0: 933 root tests, 262 dashboard.
4 tasks
pitimon
added a commit
that referenced
this pull request
Jul 25, 2026
…120) * test(parsers): the hermes fixture the allowlist was standing in for Closes the last DoD item on issue 104. The allowlist entry said a fixture "needs a real schema, not a JSONL line" — so this builds one. node:sqlite ships with Node 22+, already the floor here, and readHermesSessions falls back to it when the sqlite3 CLI is absent. If neither is available the harness reports the fixture produced no rows rather than passing quietly — the same guard that caught the empty kimi fixture. Schema taken from the parser's own query (rollout.js:3216), not guessed, and started_at/ended_at are epoch SECONDS with the bucket coming from `ended_at ?? started_at`. Three sessions across two databases, because the profile walk is what is unique to this parser: a default state.db plus profiles/work/state.db, and a profiles/empty/ with no database that must be skipped rather than throw. Also a session with cache and reasoning at zero, which still has to satisfy the column sum rather than be excused from it. Verified by reading the emitted rows, not just the exit code: 3 sessions -> 2 buckets grouped by model, 4290+820+15000+300+120 = 20530 and 500+60+0+0+40 = 600, and claude-opus-5 present proves the profile database was read. Allowlist 21 -> 20, max_size lowered in the same commit as the ratchet requires. ci:local exit 0: 933 root tests, 262 dashboard. * test: cleanup raced the background sync the HOME fix made land in-tree A flake I introduced in #117 and did not see until the hermes fixture run. Giving the notify handler an isolated HOME was the right fix — it stopped the child resolving the developer's real ~/.tokentracker. But the handler deliberately spawns a DETACHED background sync, and that sync now writes inside the test's own temp directory. The write can land after execFile's callback has fired, so the `fs.rm` in `finally` races it and fails with ENOTEMPTY. Cleanup retries instead of waiting on a process the handler detaches on purpose. 16 call sites, one helper. Verified by running the file 5 times: 19 pass, 0 fail, every run. A single green run proves nothing about a race. Also recording the process error that let this reach a commit: I ran `ci:local` and chained the commit after `echo "exit=$?"`, which always succeeds, so the `&&` did not gate on the red suite. The commit landed on a failing gate. Fixed by capturing the exit code into a variable and testing it. ci:local exit 0: 933 root tests, 262 dashboard. --------- Co-authored-by: itarun.p <itarun.p@somapait.com>
2 tasks
pitimon
added a commit
that referenced
this pull request
Jul 25, 2026
The last unticked box: "Compaction is safe against a concurrent append (test with the sync lock held)." It is safe BECAUSE of the lock, not on its own, so a test that only asserted the happy path would be claiming a property the code does not have. Both halves are demonstrated instead. First, the hazard is real and shown to be real: patching writeFileSync to append to the queue in the window between the temp write and the rename, the appended row IS lost. It was on the old inode and went with it. If that test ever starts failing because the append survives, compaction grew a merge step and the lock requirement should be re-examined — a finding, not a broken test. Second, the lock genuinely excludes a second holder, and releases. Without that "compaction runs inside the lock" would guarantee nothing. Third, the positive case in the arrangement sync actually uses — lock, compact, release — with a would-be concurrent writer refused mid-operation. Fourth, a source fact pinning the ORDER in sync.js. Reordering those lines would leave the other three green while reopening the window, which is exactly the kind of regression the other three cannot see. ci:local exit 0: 956 root tests, 302 dashboard. Co-authored-by: itarun.p <itarun.p@somapait.com>
This was referenced Jul 25, 2026
Merged
pitimon
added a commit
that referenced
this pull request
Jul 26, 2026
15 commits since v0.39.43. Tracker: #125. User-facing: - The Projects panel honours the date range. It read no query parameter at all, so picking "24h" narrowed every other card while Projects kept showing all-time totals with nothing on screen saying so (#118). - Per-repo cost, with rows written before the change named as unpriced rather than shown as a confident $0 (#121). - Sources with no per-repo attribution are named, instead of their absence reading as "that tool cost nothing here" (#118). - A plan-value card answering what README:32 already promised, labelled as list-price-equivalent rather than a saving (#122). - The Codex quota chip no longer vanishes on a 401 (#119). - `sync --compact`, which reclaimed 34,924 lines to 5,636 on a real install (#117). Behind the scenes: outbound-privacy validator hardening (#111, #114), avatar proxy per-hop revalidation, port-aware allowlisting and a real download cap (#109, #112), a parser conformance ratchet (#116, #120), scripts/graph out of the product gate (#115), and a usage-limits fixture capture tool (#124). Version bumped in all four lockstep locations by the `version` hook: package.json, package-lock.json, both TokenTrackerBar targets, and the Windows csproj. validate:version-lockstep passes. WATCH AFTER SHIPPING: #121 migrates cursors.json on every user's first sync after upgrading, re-keying project buckets from project|source|hour to project|source|model|hour. Without that the old bucket strands and its usage is counted twice. Verified read-only against a real 2,015-bucket state — every key migrated, total_tokens preserved exactly at 6,281,653,062, and all 2,015 queuedKey markers survived, the loss of which would re-append every row. That is one machine; a differently-shaped state is the residual risk. ci:local exit 0: 972 root tests, 302 dashboard. Co-authored-by: itarun.p <itarun.p@somapait.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Addresses #103:
sync --compact, plus the column invariant that lived only in prose now running indoctor.Measured on a real install: 34,492 lines, 5,595 unique keys, 83.8% superseded, 11 MB — and
readQueueDatare-reads and re-dedups all of it on every endpoint call, 6-8 per dashboard refresh, auto-refresh every 30s.Why the design risk is near zero
The readers already define the output.
readQueueDatakeeps the last row persource|model|hour_start, so compaction keeps the last raw line per that key — not a re-serialised row. Byte-identical API responses follow by construction rather than by luck: the surviving bytes are the exact bytes the reader would have kept. A test reproduces the reader's dedup and asserts its output is identical before and after.Unparseable lines are kept. They are invisible to every reader already, so dropping them changes no response — but a partial write worth investigating should survive a routine maintenance command.
Three things found while building this
1. The invariant as documented is wrong, and my check inherited the error
Running it against a real 34,922-row queue reported 8,236 violations. Every one was
source=codex, and in every case the difference was exactlyreasoning_output_tokens.The rows were right. Codex and every-code fold reasoning into
output_tokens, andcomputeRowCost(src/lib/pricing/index.js:309) already made that distinction by charging their reasoning at zero.CLAUDE.mdsaid "sum of all columns" with no exception — the prose was the defect.Corrected there, and
expectedTotal()is now the single rule shared bydoctorand the #104 parser harness, which had copied the same too-strict version.After the fix: 13 findings, all inside 2 real codex buckets whose total exceeds its columns by 5,337. Left as a
warn— genuine data, not a check artefact.2. Automatic compaction is NOT shipped, though #103 proposed it
Wired up first, it immediately rewrote an 11 MB production queue — 34,924 lines down to 5,636 — during
npm test.Sync now recommends
--compactpast the threshold and does not act. Rewriting the user's data file unasked turns every path that reaches sync into a data-mutating path, and that is a decision for the person whose data it is.3. The reason it could reach production data
test/init-uninstall.test.jsspawned the notify handler in a child process withenv: { ...process.env }. Overridingprocess.env.HOMEin the parent never reaches a child, so it resolved~/.tokentrackerand ran a real sync.Pre-existing — it had been appending rows to the maintainer's real queue all along. Compaction only made it visible. The helper now requires an isolated
homeand throws without one, with a test for that.Verified empirically:
ci:localno longer changes the real queue's mtime.Test plan
ci:localexit 0 — 914 root tests (+12), 256 dashboarddoctorflags a column-sum violation, a negative count, a non-numeric column and a misaligned bucket; a codex row that folds reasoning is not flagged, and the same numbers fromclaudearesync --compactend-to-end on an isolated HOME: 6 lines → 1, keeping the latestqueue-compact.jsis 220 linesNot in this PR
Automatic compaction (see above) and the concurrent-append test. The operation runs inside the existing sync lock, which is where a concurrent writer is already excluded.