cc-explorer: make failure a queryable axis (errors_only + survey_failures) - #64
Open
coryking wants to merge 4 commits into
Open
cc-explorer: make failure a queryable axis (errors_only + survey_failures)#64coryking wants to merge 4 commits into
coryking wants to merge 4 commits into
Conversation
Failure was reachable only by guessing what an error said and regexing for the prose, even though every tool result carries a structured `is_error` flag. This lays the foundation for querying it directly. models.py — the failure taxonomy, sited next to UserOrigin as its exact parallel: the ONE place that answers "did this tool call fail, and how?" FailureKind classifies, FailureCategory says whose problem it is (agent / policy / environment / cascade), and `ToolResultContent.failure` is the only entry point. `is_error` is the GATE; prose only classifies. Measured on the live corpus, a marker heuristic promotes ~1400 SUCCESSFUL results to failures — npm logs and ssh banners that merely contain "not found" — which would bury real breakage, so the old `_result_is_error` heuristic is kept under its true name (`looks_like_no_match`) as the narrower zero-hit question audit_session_tools actually asks. `collect_tool_calls` lifts the tool_use -> tool_result pairing out of subagents.py so the single-session audit and the corpus-wide survey share one walk. `extract_agent_tool_audit` is rebuilt on it, with its broader "error" definition (failure OR zero-hit) preserved and now documented. `ToolResultContent.text` replaces the third copy of the content-flattening logic. corpus.py — `matching_files` exposes the rg prefilter at FILE granularity and without the `rg_safe` gate, which exists to protect USER regexes written for extracted text from JSON string escaping; a pattern authored against the raw encoding has no such gap. File granularity is what stops one failing subagent body from dragging its session's other twenty transcripts through the parser. `candidate_refs` is now expressed on top of it. search.py — `errors_only` restricts matches to failed tool results, so the pattern narrows the TOPIC rather than the error vocabulary. `session_sources` takes a Path instead of a SessionInfo: the expansion is a filesystem walk and must be reachable from a SessionRef without paying for a parse. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kor6NdZegwKxn55hdvZiot
…eryable axis (#57) Two surfaces over the classification foundation. `errors_only` on search_projects / grep_session / grep_sessions restricts hits to failed tool calls. On the real corpus, patterns=["ssh"] over home-it-services drops 6193 hits to the 74 that actually failed — which is the whole point: two surveyed episodes had agents fire ~30 guessed vocabulary patterns and pre-write ~25 literal candidate error strings into a dispatch prompt because no primitive existed for "give me every failed call." search_projects narrows the corpus by the raw `is_error` literal before the pattern prefilter runs. errors_only with hide='outputs' is refused rather than confidently returning nothing. `survey_failures` is the landing page: every failure in a window, classified and rolled up by kind, tool and session. Cost scales with the answer — rg names the ~43% of files that can contribute before anything is parsed, and `after` additionally prunes by mtime. A full unwindowed pass over 4.6 GB runs in ~33s at 463 MB peak RSS. Three things the payload gets right: - Cascade suppression is on by default. One failure in a parallel batch cancels its siblings and each cancellation is recorded as its own error; counting them multiplies that batch. 444 suppressed corpus-wide, reported as `cascade_suppressed` and recoverable with include_cascade. - `unclassified` is not a junk drawer at the bottom. It sits directly after by_kind, grouped by leading text and ranked by recurrence, because the taxonomy exists to subtract KNOWN noise so novel breakage stands out. - Nothing is unconditional. The default response is counts only (~1.2k tokens for a 7-day window); error text is opt-in via `examples`, every list is capped by `limit`, and any capped list carries an explicit overflow line naming what was dropped — a silently truncated list reads as "that's all of them" and ends the investigation. No existing response model gained a field. Verified against ~/.claude/projects with an independent raw-JSONL oracle: 8369 of 8535 in-corpus flagged errors captured (98.1%), per-kind counts matching exactly for 6 of 16 kinds and within 4% for the rest. The residual is genuine — 52 tool_results whose tool_use is absent from the file, plus conversion-artifact copies that are deliberately skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kor6NdZegwKxn55hdvZiot
Applies the consolidated review of the failures-axis diff. Every corpus number below was re-measured against ~/.claude/projects (7,494 transcripts, 11,583 flagged errors) before and after. Correctness - Anchor the bare uppercase error codes in the rule table. `ENOTFOUND` under `re.I` with no word boundary substring-matches `ModuleNotFoundError` and `FileNotFoundError`, and `network` runs before `bad_path`, so a real FileNotFoundError could never reach its own kind. The `network` bucket goes 175 -> 76, of which Python tracebacks go 96 -> 0. `ECONNREFUSED`, `ENOENT`, `EISDIR`, `ETIMEDOUT` anchored the same way. - Require a filesystem noun in front of `bad_path`'s `does not exist`. Bare, it is also every SQL error's phrasing: 244 Postgres `column "x" does not exist` results were filed as category `agent`, the category the docs tell surveyors to filter OUT. bad_path 1592 -> 1364; the moved rows land in exit_code (environment). Real paths, including long quoted ones, still classify. - Pair tool calls in two passes. `tool_use_id` is the causal link; file order is not. 740 of 258k tool_result blocks are written before the tool_use they answer, and the single forward pass dropped every one — the failure vanished from survey_failures while errors_only still found it. Recovers 44 failures across 39 transcripts (11,532 -> 11,576). - Make `kinds=['cascade']` answerable. Suppression ran first, so it always returned total=0 and the MCP layer then offered three remedies, none of them the real one. Naming cascade in `kinds` now implies include_cascade, and the zero-result error names include_cascade whenever cascades were suppressed. - Bound the text before normalizing it. `" ".join(text.split())[:LIMIT]` normalized whole 540 KB results to read a 1000-char window; `collapse_ws` slices first. FailureExample construction is deferred to the two places that can keep one (first of a kind, first of a shape) instead of being built for every failure and discarded. - Include the assistant side in `errors_only` context. The match set narrows to ToolResultEntry but context was still composed from `role`, so at the default role='user' the tool_use that caused the failure was structurally unreachable. Measured: the causing assistant turn is a median of 1 entry back; the nearest human turn is a median of 8 (p90 81, max 677) and for 24 failures does not exist at all. - Anchor the cascade rule to the head of the window. All 1246 in the corpus start there, so this is free — and it stops a result that merely QUOTES "Sibling tool call errored" from being suppressed, which in a repo that greps transcripts for a living is a real path. Design - Add `has_failure` (the flag, no regex) beside `failure` (the classifier). errors_only asked `failure is None` once per pattern per entry and threw the kind away; the survey and the audit walk did the same. - Move NO_MATCH_MARKERS / looks_like_no_match back to subagents.py, its one caller. models.py keeps the explanatory note pointing at it. - Justify session_sources' Path signature by using it: failures.py maps its rg-matched paths through it instead of re-deriving agent ids from filenames and re-reading provenance. Two walks answering the same question, now one. - One error-prefilter degrade path (`failures.error_candidate_files`) instead of the same try/except/warn/fall-back in two modules. - parse_kinds joins parse_hide in models.py; the MCP boundary keeps only the ValueError -> ToolError wrapper. Judgment calls - `by_tool` keys on the FULL tool name and resolves a display name that collapses to the short form only when unambiguous. 17 short names in the corpus are shared by two or three MCP servers; merging them blends the error_rate denominator that is the whole point of the section. - parse_error is category `agent`, not `environment`. All 163 sampled: python -c heredocs with broken f-string quoting, malformed jq filters, injected JS with `await` outside async. That is the agent getting its own code wrong. - FailureKindRow.kind now explains all 16 values instead of 9. Tests 507 pass, up from 480. New: out-of-order tool_result; multi-block tool_result entry; an equivalence oracle pinning survey_failures.total to the failed ToolResultEntry blocks over the same fixtures; a negative-collision table (ModuleNotFoundError is not network, `column "x" does not exist` is not bad_path, a quoted cascade marker is not cascade); kinds x include_cascade; ScannerError degradation asserting both the flag and completeness; errors_only context composition across all three roles; and the survey_failures MCP tool at the registration layer. test_every_kind_has_a_category asserted `isinstance(kind.category, FailureCategory)`, which cannot fail — it now asserts map membership, and a sibling asserts every kind is producible by a rule. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kor6NdZegwKxn55hdvZiot
…ilures axis Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kor6NdZegwKxn55hdvZiot
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.
Closes #57.
Failure was not a queryable axis of the transcript corpus. You could slice by project,
session, agent, role, pattern, and date — but not by "this call failed," even though
11,583 tool results carry
is_error: true. The only path to them was guessing the error'sprose and regexing for it.
Two surveyed episodes measured that tax: one agent fired ~30 guessed vocabulary
patterns to approximate "show me failed connections near host X," and another required
its orchestrator to pre-write ~25 literal candidate error strings into the dispatch
prompt before it could start.
What this adds
errors_onlyonsearch_projects/grep_session/grep_sessions— narrows hits totool results that actually failed. On the issue's own scenario,
patterns=["ssh"]overhome-it-services goes from 6,193 hits to 74 actual failures, surfacing
Connection refused/No route to host/Could not resolve hostnamewith nothingguessed.
survey_failures— a cross-project rollup.audit_session_tools' error machinerypromoted out of single-session, single-agent scope, plus a 16-kind taxonomy over a
5-value category axis (
agent/policy/environment/cascade/unknown). Cascadeerrors are suppressed from headline counts by default;
unclassifiedsits third in thepayload because that bucket is where novel breakage lives.
FailureKind/FailureCategoryinmodels.py, sited as a deliberate parallel to theexisting
UserOriginenum, withToolResultContent.failureas the single classificationentry point.
collect_tool_calls— the tool_use↔tool_result pairing walk — was lifted outof
subagents.pyso the single-session audit and the corpus survey share oneimplementation.
Payload economy
Default output is counts only — roughly 1,180 tokens for a 7-day window. Error text is
opt-in via
examples=true. All four list sections are capped bylimit(default 10) withan explicit
*_overflowline naming what was dropped; nothing truncates silently. Noexisting response model gained a field.
Design decision worth reading
The issue prescribed reusing
_result_is_erroras the error gate. That was overridden withmeasurement: of tool results carrying "not found"/"no matches" in their first 200 chars,
1,095 are not flagged as errors versus 567 that are. Letting prose promote successes to
failures would bury real breakage in the one tool whose entire value is precision. So
is_erroris the gate and prose only classifies; the old heuristic survives under its truename,
looks_like_no_match, used only byaudit_session_tools, whose question ("did thiscome back empty?") is genuinely different.
Review
Three reviewers (correctness, Python, project-standards). Seven correctness fixes, five
design cleanups, and eight new tests targeting the gaps that let the bugs through. The
material ones, all re-verified corpus-wide after fixing:
ENOTFOUNDmatchedModuleNotFoundError— no word boundary,re.I. Thenetworkbucket was 175 entries, 96 of them Python tracebacks. That poisoned the
environmentcategory the docs point surveyors at. After anchoring: network 175 → 76, Python
tracebacks 96 → 0.
collect_tool_callsassumed file order is causal order — JSONL flushes out of order,so a
tool_resultwritten before itstool_usewas silently dropped. 740 of 257,986blocks corpus-wide; failures visible to the survey went 11,532 → 11,576. This also had
the two halves of the feature disagreeing:
errors_onlyfound these, the survey did not.An equivalence-oracle test now pins them together.
kinds='cascade'was unreachable — suppression ran before the kinds filter, so italways returned zero and the error told you to widen the window. Naming
cascadenowimplies
include_cascade.bad_path's baredoes not existfiled 244 Postgres schema errors under categoryagent— the category the triage workflow tells you to filter out.errors_onlydrill-in showed the wrong context — at the defaultrole=userthecausing assistant turn (median 1 entry back) was structurally never shown, while the
nearest human turn was a median of 8 away, p90 81, and 24 failed results had no preceding
human turn at all.
in a package with a 5.5 GB RSS incident in its history.
short_namecollapsed distinct MCP tools — 17 of 153 short names are shared by 2–3servers, blending their error rates in a denominator the issue calls load-bearing.
Tests: 480 → 507 passed, 1 xfailed. No existing test weakened.
Known scope limit
Filed #62 separately:
Corpus.discoverenumeratesenc.glob("*.jsonl")— top-level maintranscripts only — so 430 session directories whose parent transcript is gone strand
2,373 subagent files (~32% of transcripts) outside every search path. Pre-existing, not
caused by this work, but it bounds this tool the same way it already silently bounds
search_projectsandgrep_session.🤖 Generated with Claude Code
https://claude.ai/code/session_01Kor6NdZegwKxn55hdvZiot