Repository navigation
fix(telemetry): nothing measured is lost silently - #1046
Merged
Merged
Conversation
Each telemetry command repeated the same try, dependency wiring and error handling; the nine filter flags were listed twice, by the command and by the envelope. One helper each, same behaviour, room in the bundle for the next change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
A partition rewritten by settle or ingest dropped every line that was not a record, so an older CLI erased a newer one's records and a torn line vanished unsaid. A rewrite now keeps each such line in its day, after the records, in text order and once; a monthly file's go to the first day of its month. A day holding only such lines stays, and skipped_ledger_lines counts them on every read. The byte-identity property of a full settle now holds with such lines present, and a second one checks that no ingest or settle, bounded or not, ever loses one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
…and report The ingest and the report each printed the shapes not recognised, the calls not stored, a damaged consent log and the ledger lines kept, with their own wording of the counts; both now print the same lines, so `ingest` groups thousands as the report does and prints its snapshot count before them. The identity rule and the consent-log recovery sentence are written once; the four counters are listed once by the report and its envelope; the commands share one wiring path; the absent-path test is one function. Same output otherwise: room in the bundle for the next change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
The catch-up hook runs `aidd telemetry ingest --quiet`, which printed nothing and moved the offsets on, so the calls it did not store and the lines that were no usage line were counted for that run only: a report run later read nothing new and printed 0. An ingest now keeps what it leaves out under ledger/left-out/: a call with a day by its key, agent, day and reason, in the ledger's own form, fold and settle; an undated call and a line that is no usage line as a hash in no-day.txt. Nothing else of a call is kept, and no transcript text. A report counts the left-out calls of its period that no read stored since, and every entry with no day. Keyed, so a re-read or an offsets reset counts nothing twice; a call is dated by its earliest copy, so the bytes do not depend on how many ingests read it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
The shared left-out lines sent every one of them to stderr, so a call not stored stopped reaching stdout. It is information again; a line that could not be read stays a warning, each level now pinned by a test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
The ledger directory now holds left-out/; three ingests started together leave one line in it for the one call not stored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
A line that is neither a call nor a shape not recognised (a prompt) is never kept as dropped; two undated calls are two; a telemetry directory that cannot be looked into fails instead of reading as empty. The days a save writes need no order: every write comes before any removal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
A call with no requestId is keyed by message id, session id and time, so its left-out entry holds both: the contract, the usage contract and the code now say so instead of "no session". The reach and the unmeasured read cost of left-out/ are stated as limits, and the gate evidence names the commit it was taken on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Review round 1 found the left-out store kept the key of every call not stored, a clone that never opted in included: the product rule is that such a clone, or work after `off`, leaves nothing on disk. What consent or scope excludes is no longer kept: it is counted by the run that read it, as before. Only what was measured and cannot be stored is kept, in ledger/left-out.txt, by a 32-hex hash: an undated call of a clone that consents, and a line that is no usage line naming such a clone's directory. Reports print it as coverage.left_out, the same in every report; not_stored and unrecognised_shapes keep their run-only meaning, so ingest and report count shapes alike. The store is its own small adapter; the per-day left-out partitions, the earliest-copy dating and the report-time matching are gone. Ingest groups `updated` and picks each line's level from its type, not its text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
A call made before its clone consented is not kept though it consents now; an undated call naming no directory is never resolved, wherever git would look; a line with an unusable time is judged at the read; the kept counts say one and many. A line that gives a call names nothing, so the reader's guard on it is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Review round 2: a call judged unreadable-consent (git refusing the .git once, a config that does not parse) was counted by the hook's quiet run only, then the offsets moved on, so a repaired clone never got it back and no report said it. That is not a no: ingest now keeps the position of each transcript holding such a call from before the read, so the next ingest, a report's included, reads it again; it is stored once readable, and every run says it until then. Nothing is written. A damaged consent log still moves on: forget is its way out. A call of a directory gone before any ingest saw it alive stays a stated loss: its consent is unknown and reading it again would never end. Run-only report lines now say "by this read". The usage contract quotes the current wording, names the undated exception in Consent, and notes roots.json beside its row. Bytes for it: the lock and the report destructure what they use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Measured 695.3 KB with the left-out store, kept ledger lines and the bounded re-read; 2.0% headroom, recorded in the registry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Review round 3: a directory git refuses for good (a repository owned by another user that never opted in, an unmounted share) held every transcript with one call there, re-read by each ingest without end. A transcript is now held only when a clone roots.json remembers at that directory consented at the call's time: repairing that clone ends the hold. Anything else moves on and only that read says it. An undated call and a line that is no usage line follow the same rule, so a consenting clone's are kept once it can be read. A damaged consent log moves on because it stays damaged until forget, not because forget reads again. P3 now has a consenting clone git refuses until the last ingest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
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.
🎯 What & why
This PR closes two gaps (M1 and M2 from the R1 telemetry audit) where telemetry lost data without saying so.
aidd telemetry ingest --quiet. What that ingest measured but could not store was counted for its own run only, then the offsets moved on, so a report run later showed0.🛠️ How it works
savekeeps every non-empty line that is not a record.skipped_ledger_linescounts these lines on every read.off, calls outside any repository, and directories never seen alive.ledger/left-out.txtas<reason> <32-hex sha-256>, one line each (41 or 46 bytes), each once, and never as a key or text in clear.leftOut..gitonce, or the config doesn't parse.unreadable-consentis not a refusal when a clone thatroots.jsonremembers at that directory consented at the call's time: that clone may consent again once it can be read. Undated calls and unrecognised lines follow the same rule.forgetremoves it.version: 1, with one field added:coverage.left_out: {undated, unrecognised}. It counts everything kept by every ingest so far, so it has the same value in every report, bounded or not.files_read,unrecognised_shapesandnot_storedkeep their meaning: the current run's.refactorcommits. The budget was then raised in its own commit,chore(cli): raise the bundle budget to 709 KB, as the policy incheck-bundle-size.mjsrequires, and recorded in its registry. Measured size: 712,014 B (695.3 KB), 2.0% headroom.🧪 How to verify
cd cli && pnpm test:Tests 6721 passed | 1 skipped (6722)node --test 'scripts/__tests__/**/*.test.js':pass 460,fail 0pnpm test:mutation:telemetry: score 96.6 (floor 96). The survivors on changed lines are explained in evidence.md, which also records the red runs, the hand mutations, smoke, and the token end-to-end check in both modes.left_outmatches an independent oracle, is the same bounded or unbounded, and is never written for a clone that never opted in.left-out.txtpresent, a bounded report equals the full one filtered.left-out.txt.left_out.left_outis cumulative. It counts everything every ingest has kept so far, and does not reset per run or per period, so do not sum it across reports.not_storedandunrecognised_shapesstill describe the current run only.ingestoutput changed. Counts are grouped by thousands, the snapshot line comes first, and each line's output level comes from its type.never-seen-alive, for example a worktree created and removed between two ingests) is reported only by the run that read it, and the hook's run prints nothing. Its consent is unknown, so nothing may be kept, and the directory never comes back, so re-reading it would never end. Aclaude --worktreesession's start sees its directory alive; a subagent's worktree has no such recovery. ASubagentStartorSubagentStophook might cover it; that is unverified and out of scope.roots.jsonmay name a non-opted clone. Resolving the directory of an undated call or an unrecognised line records the directory inroots.jsonwhen it is alive, in any clone. Dated calls already did this before this PR. It records a directory, never a call. The guarantee "a clone that never opted in leaves nothing" therefore applies toleft-out.txt, not toroots.json.review.md, and every finding from all three is addressed in this push. No checker has reviewed the result yet.forgetpluralises "repository" as "repositorys".🔗 Linked issue
None. Found by the R1 telemetry audit (M1, M2).
✅ I certify
🤖 Generated with Claude Code
https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp