feat(facts): compute a deterministic session-facts sheet from the spans - #112
Merged
Merged
Conversation
`traces ask` and the exported `runTraceQuestions` run one or many questions over one or many sessions. Each question is its own `runTraceAnalyst` call, so the engine's prose answer survives; the analyst registry keeps only findings and runs analysts one at a time. - Questions run concurrently through a worker pool with a configurable limit. Unlike the import pool, one failed question never stops the others. - One shared `CostLedger` bounds the whole run, so `--budget` means the same thing whatever the number of questions; `--question-budget` bounds one question. A budget below one call's reservation refuses the run before any model call, and a budget that serializes the pool warns instead. - Every `trace://` citation in an answer is resolved against the store; an unresolvable citation fails that question. - A question may fix its answer's shape with a small JSON Schema subset. An unsupported keyword is rejected rather than ignored. - The question layout fits the DSPy input preview: the question stays whole and the answer rules sit in the first 500 characters of the instructions. - Output is `answers.json` and `report.md`, written before the exit code is decided; exit 1 when any question failed. Also surface what the evidence gate refused, and stop reporting a failed external analyzer as success: - `finding rejected` log lines now name the reason and the offending URI, and rejection counts by reason reach the analyst table's Detail cell, `TraceInvestigationResult.findingRejections`, and the ask JSON. - `analyze` exits 1 when a requested `--analyzer` fails, after writing the report that holds its error. The `--llm` engine construction moves to `analysisEngineFromEnv` in `analyst-model-call.ts`, and the shared trace-file and store setup to `analysis-store.ts`, so `ask` reuses both instead of duplicating them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bounded trace tools cannot return a whole-session fact. `viewTrace` degrades to a 20-entry name histogram above its 150,000-byte ceiling, `countTraces` counts traces rather than spans, `viewSpans` needs span ids the reader does not have, and `searchTrace` stops at 500 hits. A model asked for a tool-call total therefore adds up a capped histogram and decides by eye which names count. Measured over twelve private audit sessions, the model-backed analyst arm scored a deterministic mean of 0.389. Extracting the same answers mechanically from the OTLP spans those runs already wrote scores 0.858: the facts were present and exact the whole time. This adds that extraction as `src/session-facts.ts`, a `traces facts` command that prints it at $0, and the same sheet as prepared context for the built-in analyst kinds and every `traces ask` question. Two rules hold for every field: it names the span ids it was computed from, so a reader can check it; and a fact the spans cannot support is null with a stated reason, never a guess and never a silent zero. The sheet is not a span and cannot be cited — citations still resolve against the raw spans. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
Author
|
Landed on The conflict resolution against main's reviewed #110 is 2ee1e63; |
drewstone
added a commit
that referenced
this pull request
Sep 10, 2026
`main` landed this PR's first commit (`55044df`) through the audit-facts stack (#115) and, in #112, extended `preparedContext` with the deterministic session-facts sheet. This PR's two review commits — `185f975` (an interrupted run keeps every bought answer) and `541e2eb` (a citation the model wrapped in Markdown emphasis resolves) — were not in that stack and are still only here. One conflict, in `ASK_RULES`. Both intents kept: rules 2-4 take main's wording, which tells the model the prepared context now ends with a SESSION FACTS sheet and that the sheet itself is not citable; the comment above rule 3 stays, because it records why that rule does not also legislate citation formatting — `traceCitationsInText` reads an emphasised citation. Verified: install, check:source, typecheck, 994 tests, build and check:package green. The deterministic facts arm still reproduces dev 1.000 and holdout 0.996 over the 13 Codex bench sessions, byte-identical to the measurement taken from #115's merge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.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.
Stacked on #108 (
feat/ask-questions), because this wires the sheet into theaskpath that PR introduces. Base it onmainonce #108 merges.What it implements
Ranked change 1 of
analysis.md, the largest measured item: a deterministic session-facts sheet computed from the spans, with no model call.Measured on twelve private audit sessions, over the OTLP files those runs already wrote:
Expected lift for this change alone: +0.414 deterministic mean (0.389 → 0.803), against an Opus subagent fleet at 0.906 dev / 0.917 holdout.
Nothing was missing from the spans. No tool returned a whole-session fact —
viewTracedegrades to a 20-entry name histogram above its 150,000-byte ceiling,countTracescounts traces rather than spans,viewSpansneeds span ids the reader does not have, andsearchTracestops at 500 hits — so a model asked for a tool-call total added up a capped histogram and decided by eye which names counted.The benchmark harness that produced those numbers is private and lives outside this repo. No session content from it appears here: every fixture is synthetic inline JSONL.
What is in the change
src/session-facts.ts.computeSessionFacts(spans)returns, per trace: tool calls excluding synthesized spans (with the excluded count kept separately, and a by-name histogram), everyspawn_agentwith its task name, humanuser.promptturns in order with actor and timestamp (plus every turn by actor), the last message of the session's own agent and of each subagent task kept apart, changed paths from patch headers and file-editing tool arguments, first and last record times, unread records from the session integrity receipt, and the harness token total when a span carriestraces.session.total_tokens.traces facts. JSON by default,--format textfor the short readable form,--outto write it, for one session or many. No model call, no engine, no budget: $0.askquestion throughTraceAnalystDefinition.prepareContext, bounded at 30,000 bytes — a fifth of the documentedperCallByteCeilingof 150,000 — with the shed reported inomitted_fieldsrather than applied silently.Two rules hold for every field: it names the span ids it came from, so a reader can open them and check the number; and a fact the spans cannot support is
nullwith a stated reason, never guessed and never a silent zero. The sheet is not a span and cannot be cited —trace://citations still resolve against the raw spans, which is why each fact carries its span ids.factsexits non-zero when a selected session produced no record spans, rather than printing a sheet of zeros for a session it could not read.Schema and behavior changes
SESSION_TOKEN_TOTAL_ATTR = 'traces.session.total_tokens', the span attribute the sheet reads for the harness token total. No adapter emits it yet, sotokenTotalis null with its reason; ranked change 2 emits it, and that is worth a further +0.125 on its own.SPAN_SYNTHESIZED_ATTR = 'traces.codex.span_synthesized', honored when present. Until ranked change 5 sets it, a synthesized subagent span is identified by thetraces.codex.subagent_thread_id+traces.codex.subagent_pathpair only that span carries.traces.session_factsandtraces.session_facts_report, bothschemaVersion: 1.+session-facts.1appended to its version.createTraceAnalystrecordsprepare_contextin the exact-run identity, so a changed prepared context must not hide behind an unchanged version.analyzeSpans({ sessionFactsContext: false })runs an analyst without it.ask's rules text now names the sheet and stays inside the 500-character DSPy preview head;--otlpis now a reading flag onfacts, and--current/--workfloware accepted.agent-eval.Tests
17 new tests in
tests/session-facts.test.tsover a synthetic Codex rollout (tests/session-facts-fixture.ts) whose gold is written by hand:tool.Agentspan does not inflate the tool count — 5 TOOL spans, 4 tool calls, 1 excluded;tokenTotalis null with its reason, and reports the value once a span carries the attribute;analyzeSpanshands the sheet to the built-in kinds and omits it undersessionFactsContext: false;traces factsprints both forms, keeps the facts and names the unread records for a partly corrupt session, and exits non-zero for a session that cannot be read and for an unknown--format.pnpm check:source,typecheck,test(845 passed),buildandcheck:packageare green locally.Documented in
README.md(Session facts) anddocs/trace-analysts.md(Session facts, including that the sheet is deterministic and free).🤖 Generated with Claude Code