Repository navigation
Audit scenarios 6, 8e and 11d — all three were fixture defects - #2246
Conversation
…2242) Three agent-matrix scenarios failed for every model measured, including one passing 17/20 overall. The audit ran both winnability checks the issue asked for — are the facts present, and is the ideal call accepted — and found no harness bug and no capability gap. All three were the fixture measuring itself. The leading hypothesis is REFUTED. `role="tool"` history really is dropped by `node_history_from_messages`, but the writes those messages carried are re-rendered as terse "Fact: ..." lines plus a system-role write record. So the turn going into 11d could see both endpoint ids, the `mentions` edge, and the decision's own text. The failure was the opposite of a missing fact: the history HANDED the model the answer, and TOOL_STRATEGY_RULES tells it to answer such a turn in text. Every model returning `tools: []` was following orders. - 11d asked "what did we settle on", answerable from history without touching the graph. Now asks which records point at the decision, and a new 11c2 records a second edge so the answer is an aggregation history does not state. - 6 required the [resolve_query, update_node] subsequence to catch an INDIRECT reference. The rendered history replays scenario 4's write with its property values and id inline, so "the five-day one" was already a direct string match and there was no indirection left to resolve. Now asserts the outcome (update_node once, minProperties 1). The cost is stated in the fixture: indirect-reference decomposition is no longer measured here. - 8e's "Run through those calls for me" pointed three turns back, past 8d's Planning Cycle — the nearest antecedent was the wrong type, which is why one model answered the previous scenario's prompt. "Calls" was also a pun that never appears as the stored type's name, and "run through" reads as a meta question. Reworded to name the type and use an unambiguous retrieval verb. Also fixes a real bug the audit surfaced. `completed_writes_from` read a schema write's id from `r.result["schema_id"]`, but CreateSchemaOutput and SchemaUpdateOutput both carry rename_all = "camelCase", so the wire key is `schemaId` and the lookup matched nothing. Every schema write silently degraded to terse_write_fact's id-less phrasing, dropping from history the one string a later create_node must copy into node_type. Evidence is pinned as tests rather than left in the issue: two history- rendering tests in local_agent_service.rs, and a new matrix_scenario_winnability.rs that hand-executes each ideal call against a live SQLite-backed NodeService and confirms it is accepted — including that one traversal enumerates both of 11d's inbound edges, so its noRetry half cannot red out a correct model. Co-Authored-By: Claude <noreply@anthropic.com>
Pragmatic Code Review — PR #2246Review Type: Initial review (no prior reviews/comments on the PR) Requirements Check (issue #2242)
Verification I re-ran independently:
Code Review SummaryThis is a genuinely good piece of investigative work, and the standard of evidence is well above what an eval-fixture change normally carries. The central discipline — pin the finding as an executable test rather than as prose in a closed issue — is exactly right, and it directly serves the codebase's recorded history of eval results that turned out to be measuring stale or self-defeating fixtures. I set out to break the refutation and could not. Three of the four substantive claims verify end-to-end against the real source; the fourth (11d's new prompt) is honestly self-labelled as a weaker guarantee. There is one real bug fix here that is worth more than the fixture work, and it is correctly diagnosed. Findings below are all non-blocking. My one genuine reservation is a coverage question on scenario 6, discussed at length rather than dismissed. Findings🟢 Verified: the refutation is soundI traced the mechanism rather than trusting the narrative. 📁 📁 📁 The conclusion holds: 11d's old prompt was answerable from history without touching the graph, and the 🟢 Verified: the
|
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review: APPROVE (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
…d fixture Review feedback on PR #2246. Deleting the `schema_id` fallback (per CLAUDE.md's no-backward-compat rule) surfaced that there WAS one producer of the snake_case spelling: the hand-written result fixture in golden_scenario6_real_pipeline.rs. That fixture and the old lookup agreed with each other and disagreed with production, so `real_pipeline_renders_terse_facts_not_narrative_prose` passed on main while the real pipeline silently rendered the id-less phrasing of the fact — the test was green for the wrong reason. Fixed the fixture to the real wire shape (`schemaId`/`isCore`) rather than restoring the fallback. Its assertion that the schema id reaches the terse fact was always correct; only the key was stale. This makes the test validate the renderer against a shape production actually emits, and it independently corroborates that the bug was real. Other review items: - Moved completed_create_schema_write_captures_the_schema_id below `use super::*;` so it sits with the other tests rather than above the imports. - agent-matrix.ts: "11c records a second edge" -> 11c2, in the one place a reader consults to check 11d's justification. - Tightened `ids.len() >= 2` to `assert_eq!(.., 2)` so the traversal test also catches spurious nodes, not just missing ones. - Filed #2248 for the indirect-reference decomposition coverage scenario 6 gave up, and pointed the fixture's cost-statement at it so the gap has an owner. Skipped, with reasons: - Redesigning 11d around edge state at read time, and adding a runner note distinguishing prose-correct from prose-wrong failures. Both are framed in the review as "if this reds out again"; acting now would be scope creep on a scenario whose behavior has not been re-measured since the reword. - Moving the audit narrative out of the fixture into nodespace-docs. The review explicitly declines to argue for it, and that repo holds decisions and architecture, not audit findings. Co-Authored-By: Claude <noreply@anthropic.com>
Review addressed — commit
|
Three sibling PRs landed on main touching the same files. They compose rather than conflict, but the seams needed real resolution: #2247 (--runs / pass^k) restructured the runner around reps: the flat `summary`/`results` on EvalResults became `aggregate`/`reps`, and the scenario loop moved into a per-rep function. My per-run counts moved to RepSummary, and the report sums them across reps rather than quoting the first. The seam that actually mattered: `aggregateReps` filtered only `excludedAsEmptyGeneration`, so setup scenarios would have counted toward pass^k — putting the cascade back one level up, where a flaky setup turn reads as an unreliable SCENARIO rather than as its successors' reps not being comparable. ScenarioReliability now carries `setup`, the aggregate excludes it, and the reliability table renders it as ⊙ setup rather than as a degenerate empty generation (both have scoredReps === 0). Four tests pin it; verified they fail if the filter is reverted. #2246 (scenario audit) added scenario 11c2, which had no `end` clause — caught by my own fixture invariant rather than by the merge. It asserts both halves (the note exists AND the edge reaches it), because it is the create-and-link-in-one-turn shape: asserting only the edge would pass on a turn that linked the wrong node, and only the node would pass on the prose-instead-of-an-edge failure 11c exists to catch. It is also the shape the asymmetric `after` walk was built to see. Notably #2246 reached the same conclusion for scenario 6 independently — assert the outcome, not the route — and relaxed its trajectory expectation for the reason this PR exists. test:all green: 227 frontend files / 4794 tests, 214 scripts, 24 skill, 53 Rust binaries including #2246's new winnability suite. Co-Authored-By: Claude <noreply@anthropic.com>
Closes #2242.
Ran both winnability checks the issue specified against all three scenarios. No harness bug, no capability gap — all three were the fixture measuring itself.
The leading hypothesis is refuted
The issue's strongest candidate was that the prompt template drops
role="tool"history, making 11d unwinnable by construction. That is not what happens.Tool-role messages are dropped by
node_history_from_messages(the_ => return Vec::new()arm). But the writes they carried are re-rendered as terseFact: ...lines plus a system-role write record. Rendering the real 11a→11c history shows the turn going into 11d could see:Both endpoint ids, the edge, and the decision's own text — stated twice. The failure was the opposite of a missing fact: the history handed the model the answer, and
TOOL_STRATEGY_RULES' first bullet ("CONVERSATIONAL TURNS USE NO TOOLS… answer directly in text") tells it to answer such a turn in prose. Every model returningtools: []was following its instructions.Prose tuning provably cannot fix a missing fact — but there was no missing fact to fix.
Classification and fix, per scenario
[resolve_query, update_node]to catch an indirect reference, but history replays scenario 4's write with its property values and id inline, so "the five-day one" was already a direct string match. Now asserts the outcomeOn 8e specifically, three problems compounded: the nearest antecedent to the bare demonstrative was the wrong type (which is why one model answered the previous scenario's prompt); "calls" was a pun that never appears as the stored type's name; and "run through" reads as much like a meta question as a retrieval request.
On 6, the cost is stated in the fixture rather than hidden: indirect-reference decomposition is no longer measured there. It needs a chain whose referent is not recoverable from history, which this one cannot provide — every write it makes is replayed with its particulars.
A real bug the audit surfaced
completed_writes_fromread a schema write's id fromr.result["schema_id"]. BothCreateSchemaOutputandSchemaUpdateOutputcarry#[serde(rename_all = "camelCase")], so the wire key isschemaIdand the lookup matched nothing. Every schema write silently degraded toterse_write_fact's id-less phrasing — dropping from history the one string a latercreate_nodemust copy intonode_type, leaving the model to guess the normalized spelling from the display name.This affects every multi-turn chain that creates a type then records an instance against it, which is most of the matrix.
Evidence is pinned as tests, not left in the issue
packages/agent/tests/matrix_scenario_winnability.rs(new) — hand-executes each scenario's ideal call against a live SQLite-backedNodeServiceand asserts acceptance. Covers step 2 of the issue for all three. Includes a check that one traversal enumerates both of 11d's inbound edges, so itsnoRetryhalf cannot red out a correct model.scenario_11d_history_already_contains_the_link_it_asks_aboutandscenario_6_history_resolves_its_indirect_reference_directlyinlocal_agent_service.rs— render the real history and pin the refutation, so the next reader finds it rather than re-deriving it.completed_create_schema_write_captures_the_schema_id— asserts against the real serialization rather than a hand-written key, so a future serde-policy change re-breaks the test rather than silently re-breaking history.Verification
bun run test:all— exit 0; frontend 227 files / 4794 tests, matching baseline exactlybun run quality:fix— clean (0 errors, 0 warnings, clippy-D warningsclean)guidance_is_not_contaminated_by_eval_promptspasses with the new promptstest:e2e(25 tests)Note on ids
11c2is added; every pre-existing id is preserved, so baseline joins are unaffected and the runner reports the new one asNEWrather than silently dropping anything.🤖 Generated with Claude Code
https://claude.ai/code/session_01QaGX2LndVMehjJh4rXxQe5