Repository navigation
feat(telemetry): deterministic claude code token measurement, release 1 - #993
Merged
Merged
Conversation
The contract for replacing the previous telemetry: every billed Claude Code call counted once, broken down by person, session, model, period, repository, task and ticket, the task coming only from a declaration. Carries the measurements, the common usage record and the prototype readers with their mutation-proved fixtures. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp AIDD-Session-Id: 2c21d903-3a7e-47ac-83f8-d8b7ae3aa579
Removal of the previous telemetry first, then count, store, declare, ask, attribute and report, opt-in and cleanup, and the end-to-end proof. Pins the consent key, the binding snapshots, the hook guards and a mutation per rule. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp AIDD-Session-Id: 2c21d903-3a7e-47ac-83f8-d8b7ae3aa579
The previous measurement is replaced by a deterministic, declaration-attributed one in the following commits of this branch. This commit removes the old one so nothing half-describes the new one. cli: delete the telemetry context, its wiring, command, displays and tests, and everything that existed only to serve it: the measurement vocabulary, the per-tool telemetry declarations and transcript locations, the git adapter that installed the commit-trailer hook, the identity and sink resolvers, the FileWriter chmodExecutable method and the run-journal gitignore entry. The OpenCode hooks bridge no longer names the deleted plugin module in its generated comments; the framework build golden is re-baselined for that one line. Architecture guards, biome overrides, mutation scope, smoke steps and the help golden describe the CLI that remains. plugins: delete the aidd-telemetry hooks and skills (README reduced to a stub, manifest description updated). Remove the backlog-link.json step, reference and template from aidd-pm:04-spec and aidd-dev:01-plan, the backlog-link paragraph from the SDLC frame, and every aidd:step-end echo with the SDLC, async-dev and backlog "Say when this orchestration is over" sections. scripts and CI: delete the telemetry script tests, their fixtures, the two telemetry scripts, the generated prompts documentation and its lefthook job, the prepare-commit-msg trailer job, the journal replay, telemetry check and telemetry e2e exclusion in cli-ci.yml, and the identifier-join probe. Tests that used the telemetry plugin as a byte fixture read aidd-context instead. docs: delete the cost-report and metrics contracts and aidd_docs/runs/, rewrite every memory, README and docs line that claimed the previous telemetry, and mark the pending telemetry task folders of 2026_09 as superseded. The previous version's on-disk locations are recorded in aidd_docs/tasks/2026_10/2026_10_09_deterministic-tokens/context/previous-version-leftovers.md for the opt-in and forget phases. BREAKING CHANGE: the aidd telemetry commands, the aidd-telemetry hooks and skills, backlog-link.json and the AIDD-Session-Id commit trailer are removed; measurement returns, for Claude Code first, later on this branch. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Read Claude Code transcripts into one usage record per billed model call. - key message.id:requestId, or message.id:sessionId:timestamp without a request id - drop the synthetic model; isSidechain or agentId makes the call a subagent - cache_write and cache_write_1h are summed over the message iterations; the one-hour part is unknown when any iteration lacks its split - each advisor iteration is its own record, keyed <key>#advisor<n> - an unrecognised shape is reported and never guessed; an unknown counter is null, never 0 - global fold across every file: largest total, then earliest time, then lowest session id, independent of input order - source: recursive walk of the projects root including subagents and *.jsonl.superseded-* set-asides, byte-offset reads, an unterminated last line is left unconsumed, a shrunk or replaced file is re-read whole Re-adds the telemetry context to the architecture guards (public modules, biome layer overrides, codebase map, mutation scope, tool-name baseline). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Add `aidd telemetry ingest`: read what every Claude Code transcript gained and store each billed call of the projects that opted in. - ledger: <telemetry dir>/ledger/<YYYY-MM>.jsonl by the record's own UTC time; a changed partition is rewritten by temp file and rename, so a better snapshot of a call replaces the held one even across months and no reader meets two live records or a torn line - idempotent: held records fold first, so an equal record changes nothing; the phase 3 fold decides every replacement, in any order - lock: ledger/.lock holds pid and creation time, created exclusively; a dead pid or a lock older than ten minutes is cleared, a live recent one is waited for and released only by its owner - repository: git rev-parse per working directory, id = sha256 of the normalised host/owner/repo of origin (https, ssh, scp-like, credentials, port and .git dropped), else the lowest root commit; a linked worktree resolves to its main repository - consent: .aidd/config.json telemetry.enabled with version 2; the previous bare enabled is not consent; an unparseable file is counted; the main working tree answers for a worktree without its own file; AIDD_TELEMETRY=0 refuses before anything is read or written - roots.json remembers each directory seen alive, so lines from a deleted one still resolve; one never seen alive is counted, not stored - bindings/branches.jsonl snapshots branch.<name>.aiddTask|Ticket|DeclaredAt with the branch's creation time from its reflog, only when it changed - write order under the lock: ledger, roots, snapshots, offsets last - owner-only storage: 0700/0600 on POSIX, icacls on Windows - coverage counters in the ingest result, by reason not stored Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
aidd telemetry task <name> [--ticket <ref>] | --none binds the running Claude session from now on and, on a working branch, the branch itself through its git config, then snapshots the declaration at once. The default branch and a detached head are left untouched. A project that has not opted in to version 2, or AIDD_TELEMETRY=0, is refused. With no argument the command shows what the work is bound to and where that comes from. The format the CLI writes and the plugin hooks read is pinned by one shared fixture under scripts/__tests__/fixtures/telemetry-bindings/. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Three Claude-only hooks, inert unless the project opted in: a prompt gate that answers a typed declaration without the model and asks once on an unbound working branch when a person is present, a session-start that keeps the task across /clear and /branch, and an async catch-up that runs the ingest. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
aidd telemetry report reads new transcripts, then splits what the stored
calls consumed along total, person, session, model, day (UTC), repository,
task or ticket, four counters apart, every axis summing to the same
totals. An unknown counter is left out of its sum and counted, never added
as zero.
Attribution is one pure function: the session's declaration at the call's
time, else its carry, else the branch's declaration from the snapshots
(from the branch's creation, so declaring late moves earlier work), else
unattributed with the reason. The first declaration in a carried session
applies to the whole session. Sub-agent and advisor calls follow their
session.
aidd telemetry identity <id> writes {person_id} to identity.json in the
telemetry directory, owner-only; --off removes it. Nothing is carried over
from the previous version's file.
Ingest also reports when the oldest transcript still on disk was last
written, for the coverage line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Attribution read only the latest snapshot of a repository and branch name, so a branch deleted and created again under the same name took the older generation's calls to the new task, or to none. Snapshots are now read as generations told apart by branch_created_at: a call belongs to the youngest generation created strictly before it, and within that generation to the latest snapshot, so declaring late still moves its earlier work. A snapshot with no creation time speaks only when no dated generation matches. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
`on` writes consent at version 2, removes the previous version's commit hook line with its script, run journal and ignore entry, and resets the ledger offsets and remembered consent so earlier history is stored once. `off` switches off and keeps the version. `forget` previews, and with --yes erases both versions' files and declaration keys under the lock. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Two thin skills over the CLI: 00-init opts in after consent, sets or removes the identity and forgets with a preview; 01-usage maps a question to report flags and reads the JSON without computing a figure. A test checks every command they name against the help golden. The usage contract, the plugin README and the CLI memory describe the record, the version 1 envelope, the stores and the ask and carry rules. Stale mentions of the replaced measurement go, the smoke harness covers the telemetry commands, and the bundle budget is set to the measured build plus 2 %. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Opt in, get asked on an unbound branch, declare by typing it, clear and keep the task, run a headless session, then check that every report axis adds up to the total and lands on the declared task and ticket. The real hooks run through an aidd shim, a .cmd on Windows and a node script elsewhere, with no AI tool binary on PATH. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
The built CLI and the prototype's independent reader counted the same projected history: 2,746 files, 262,328 records, 68 days, no day differing. The usage record's branch comment now says what attribution does with it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
C1: partitioning is linear, an ingest rewrites only the months that changed, declarations and snapshots take a bindings lock and never wait on the ledger lock. C2: on lifts a remembered refusal for every directory of the repository. W1: ordinary prompts that look like a declaration pass the gate. W2: a branch redeclaration applies from its own time. W3: consent is the clone's git config aidd.telemetry, never committed. W4: cmd.exe /s command line, and the hook home matches the CLI's. W5: mutation proofs recorded. Minor: stale context docs, superseded old plan, report wording, dead attribution reasons, V1 config block removal, forget wording, next step. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
…re remembered as Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
A call is stored only if its directory was seen alive in clone X, X is the clone answering now (or the remembered X once it is gone), and X's consent covered the call's time. A clone is its git common dir's dev, ino, birth time and path; consent is a history of intervals kept beside the ledger, opened by on, closed by off or by an ingest that finds the key changed or the clone gone. roots.json becomes format 2: one entry per directory and clone, no consented flag. forget names only gone clones that had consented. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
The identity comment now cites the Node Stats documentation and the libuv sources it was read from, and drops a claim about FAT and network shares that no source gave. The proofs file states what was observed: the damaged-month mutation at its stated scope and without the new assertion, and row 12 at the old code. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
The report must equal what the API billed, call for call and counter for counter, on the declared task and ticket. The API is a local server whose usage is the expected answer, so no key and no quota are spent, and the job runs on Linux, Windows and macOS. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
…the task The real claude runs in a pseudo-terminal against the fake API: the first prompt is blocked before any call, the declaration typed as a prompt is answered by the hook, and /clear carries the declared session. Each step waits on what it causes on disk or at the API, not on the screen. Linux and macOS; Windows has no POSIX pty and keeps the headless check. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
The same relay through pywinpty, with an aidd.cmd the gate can find, so Windows is asked, answered and keeps the task across /clear like the rest. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
…ndows Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
…adless Under ConPTY on the runner Claude never got past its terminal setup, even with the cursor and identity queries answered. Windows keeps the headless check, which passes there. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
blafourcade
marked this pull request as ready for review
October 15, 2026 06:31
This was referenced Oct 15, 2026
Closed
Merged
1 of 7 tasks
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
Replaces the previous telemetry with a deterministic token count for Claude Code. Every billed call is counted exactly once and stored locally. The report breaks the calls down by person, session, model, agent, day, repository, task and ticket, and every axis sums to the same total. The task comes only from an explicit declaration, and nothing is sent anywhere.
Contract: spec.md. Plan: plan.md. Product contract: usage-contract.md.
🛠️ How it works
message.id:requestIdand duplicates are folded globally: the largest total wins, then the earliest time, then the lowest session id. Synthetic messages are dropped. Each advisor iteration becomes its own record. Cache writes are summed over the message iterations, and an unknown counter stays null, never 0. (contexts/telemetry/domain)aidd telemetry task <name> [--ticket <ref>] | --nonebinds the session and the working branch; branch bindings live in local git config. The plugin hooks ask once per unbound working branch, only for Claude, only when a person is present, and only where consent is open. A declaration typed as a prompt is parsed by a strict tokenizer and run without a shell, so it never reaches the model. A/clearor/branchkeeps the task. (hooks)aidd telemetry onwritesaidd.telemetry=2:<token>to local git config and opens an interval. A call is stored only if its clone had an open interval at the call's time. A clone is identified by its common dir's dev, ino, birth time and realpath. There is no backfill beforeon; that is a user decision, because backfill was the source of every consent hole the reviews found. Nothing is committed, so a teammate is never measured without runningon.aidd telemetry statusshows whether this clone is measured, since when, and its task.onis not read: on a real 6.2 GB history the first ingest goes from over 22 minutes (never finished) to 246 s, the next takes 1.1 s, and the stored calls match an independent count (5 calls, 217,337 tokens).offcloses the interval.forgetpreviews first; with--yesit erases both versions' data and the git keys. The previous version's leftovers are cleaned onon.🧪 How to verify
cd cli && pnpm test(6,449 tests),pnpm test:mutation:telemetry(96.2, floor 96),bash scripts/smoke-tools.sh;node --test 'scripts/__tests__/**/*.test.js'(434).RUNNER~1) was not tried, and neither was thecmd /slaunch of an npm shim on a real install.git configoff is not seen if no hook or ingest runs before the clone is deleted;statx(seccomp, kernel before 4.11), the birth time Node reports is the change time, so the identity moves at each entry added to the git dir. The clone stops being measured;offalso closes the interval its key names at its path, so it fails closed. Simulated (statxmade to fail understrace): 0 of 36 rows stored more, 3 runs; before that change one row stored a call made afteroff.identifier-joinruns the installedclaude -pagainst a dead API and checks, through the shipped hook predicates, that the hook's payload, its environment and every transcript line name one session, thatCLAUDE_PIDis set, and that a headless run never reads as a person present (probe-identifier-join.cjs). It holds on 2.1.295.✅ I certify
Ticked by Claude on the author's explicit instruction.
🤖 Generated with Claude Code
https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp