Repository navigation
fix(telemetry): an agent row says who could be named - #770
Merged
Merged
Conversation
`agentKeyOf` read `agent_name === undefined` as the main thread, whatever the tool. Only Claude Code's reader ever sets that field — the Codex, Copilot and OpenCode readers set it nowhere — so on those three tools every record was reported as the main thread. Not a small error: 100% of the axis, asserted on no evidence. The contract said so out loud and called it correct. `claude-code-transcript.ts` already states the right rule twelve lines below, for `step`: its absence yields no step at all rather than asserting no skill ran. A record with no agent name now joins one of two rows, and only the tool's own declaration says which: `main-thread` is a tool that names agents saying this record belongs to none of them, `not-stated` is a tool whose route never names one. `TelemetryRouteSupply` gains `agentName`, declared beside `toolStatedStep` and checked against real captures by the same honesty guard — a route claiming an agent name its reader never sets fails there. The declaration is read rather than the record because the record cannot carry the absence: a tool that never names an agent writes exactly what a main-thread line writes. Still unfixed, and stated on the row instead: a line marked as a subagent's that carries no agent name reads as the main thread. Measured across 1,852 transcripts, 157 of 122,637 subagent lines, 0.07%. Closing it needs a field no already-stored record could ever gain. `accumulate`'s per-record parameters became a `RecordContext` — nine positional arguments in a fixed order is a call nobody checks by eye, and the linter's own function-length rule made the point at the ninth. Bundle budget raised 593 -> 596 KB, measured 593.8, the same 2.2 KB headroom the raise before it left. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Merged
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
agentKeyOfreadagent_name === undefinedas the main thread, whatever the tool:Only Claude Code's reader ever sets that field (
isSidechain+attributionAgent,claude-code-transcript.ts). The Codex, Copilot and OpenCode readers set it nowhere. So on those three tools every record was reported as the main thread — 100% of the axis, on no evidence at all.The contract said so out loud and called it correct:
The same source file already states the right rule twelve lines below, for
step: "its absence here yields nostepat all … rather than asserting 'no skill ran'."Change
A record with no agent name joins one of two rows, and only the tool's own declaration says which.
attributiontool-statedmain-threadnot-statedTelemetryRouteSupplygainsagentName, declared per route besidetoolStatedStepand checked against real captures by the existing honesty guard —telemetry-route-supply.unit.test.tsobserves what each capture actually yields, so a route claiming an agent name its reader never sets fails there.The declaration is read rather than the record because the record cannot carry the absence: a tool that never names an agent writes exactly what a main-thread line writes.
What this does not fix
A line marked as a subagent's that carries no agent name still reads as the main thread. Measured 2026-09-05 across 1,852 transcripts: 157 of 122,637 subagent lines name no agent, 0.07%. Closing it needs a new field on the record, which no record already stored could ever gain (
storeNewCandidatesfreezes a record's field set at first sight, #769). Stated onCostReportAgentRowrather than captured.Proof
Two binaries against one copy of the sink,
--days 30, 30,222 requests on both. This corpus is almost entirely Claude Code, so it can barely show the fault:by_agentrowaee5721bThat one record is the sink's only Codex request. The magnitude is not in this corpus — for a Codex-only reader the same change moves the whole axis — so the guards are the proof and the record is the demonstration the path runs end to end. All eleven breakdowns reconcile to 30,222 on both.
Guards
main-threadrownot-stated— 3not-statedmain-thread, the old reading — 3agentName: true— 1Reconciliation is covered by a three-row case: named + main thread + not stated must still sum to the period total.
Envelope
No bump. The agent row gains
attributionand the route supply gainsagent_name— fields a consumer may ignore, no existing field changing meaning, the rule atcost-report-envelope.ts. A consumer that readby_agentas "one row per agent plus one for the main thread" now sees a third row; that is the same reasoning #768 applied toby_flowand CI accepted, stated here rather than implied.Consumers updated in the same commit: the envelope type and mapper, the route-supply envelope shape, the
--axis agenttable, the terminal rendering, the contract, and the cost skill's reading instructions.Also in this commit
accumulate's per-record parameters became aRecordContext. Nine positional arguments in a fixed order is a call nobody checks by eye, andbiome's function-length rule made the point at the ninth.593→596KB, measured 593.8 — the same 2.2 KB headroom theby_promptraise left, recorded incheck-bundle-size.mjsbeside the two raises before it.Gates: 3449 tests / 308 files,
tsc,biome ci(2 pre-existing warnings, 0 errors), knip, jscpd, bundle within budget, 369 repository script tests, 0 broken links, cli layering clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp