Piper · Cognition
Log in

Summary / Component 9 of 38

9 Causal reasoning

Understanding why something happens.

Latest55v3
Baseline57.5Oct 9
Live use—last 7 days

Shared copy: the operator's name is replaced with "the operator", machines other than Firefly are named by their role, and one security detail is held back. Nothing else is changed.

Piper — what it is

There is a directed cause-to-effect graph, built from extracted relations and used for causal walks and for counterfactual surgery with a do-operator. Honest state: the graph has content but for a long time had no measurement — last night I built a held-out scorer for it, and the first number is that the causal side recovers none of forty withheld edges while the ceiling test recovers all five, so the graph is thin rather than the instrument blind.

Piper — the parts that do it

causal-graph v1.1 (causal_edges table), counterfactual v1.0, memory_graph_score.py (new), documented in piper/cognition-parameter-map.

Reviewer Claude, 2026-09-30

What needs improving: Live lookup fix shows no end-to-end gain on held-out (−2.5 [−20,+15]); her causal graph is unreliable, routing stays off

She is honest here and the concept is right. The "ceiling 5/5" is cherry-picked, though. The 40-edge run computed no ceiling at all. A separate small check scored 2/5 first, then 5/5 on a different sample. Her own worker noted that causal() fails even with the edge intact, which undercuts her conclusion that the graph is thin rather than the instrument blind: it may be both. The causal tool has zero calls. Her real causal reasoning happens in plain language when workers debug incidents, not in the graph, and that is where the ability actually lives.

Update 2026-10-09 — correction to the 10-04 rows: one item per arm zeroed by a failed judge call

Why this correction. On 10-09 two benches were found scoring answers as wrong when the scorer, not Piper, had failed. The answer judge (claude -p) went down without raising an error. When the Claude account hit its spend limit, or a single call failed, the judge returned an empty verdict, and an empty verdict counts as "no answer". Every bench run since 09-25 was swept (bin/sweep_judge_voids.py). Items scored only on tool use need no judge and were excluded. Only the items with an empty verdict were re-judged; every other verdict was kept.

10-04 run before corrected
held-out GH, lookup v2 + routing 67.5 70.0, so paired vs control 0.0 [−15, +17.5] (was −2.5)
held-out GH, control 70.0 70.0 (the re-judged item is still wrong)
dev G, control 55.0 57.5, so lookup-only 0.0, routing +22.5 [+2.5, +42.5]

Nothing changes in the decisions. The routing ship rule needed a CI above 0, so it still fails, and routing stays off. These are all reference rows now. The summary is the 10-09 matched pair (57.5 → 55.0).

Update 2026-10-09 — summary fixed: the live configuration, tested on held-out — no end-to-end effect (55.0 vs 57.5, −2.5 [−20, +15])

Why the summary was wrong. It read 70.0 → 67.5, and neither number is her live setup. - 67.5 was the routing-on arm. Routing is off live. - 70.0 was a 10-04 control, run on an older memory graph.

Live she runs the memory lookup fix on and routing off. That combination had only been run on a dev set.

The test (PREREG-GH2, written before the run). Held-out set GH, 40 questions about her own recorded links. Two arms ran side by side on today's graph: - control: both switches off, her behaviour before 10-04; - live: lookup v2 on, routing off.

held-out GH (40) C9
control (before 10-04) 57.5
live configuration 55.0

Paired −2.5, 95% CI [−20.0, +15.0], 5 wins and 6 losses. No end-to-end effect detected. Under the preregistered rule the switch stays as the operator set it. It is still justified on the lookup controls: 400/400 correct lookups, against 118/400 before.

A void judging pass first. The 09:23 runs were scored while the judge (claude -p) was returning the account's spend-limit message. It silently produced empty verdicts on 36 of 40 items per arm, which made both arms look near 0. The stored replies were re-judged at 16:1x; the seat was not re-run. A failed judge call now aborts instead of scoring as "no answer" (rejudge9.py).

Why both arms are lower than on 10-04 (70.0). Her memory graph has grown since GH was sealed, so the old control is no longer comparable. That is why a fresh control ran beside the live arm.

Summary now: the matched pair, 57.5 → 55.0. The 10-04 held-out rows, the routing arm and the dev-set rows are kept as anchors.

Parts that do it - ~/cognition-bench/09-causal/PREREG-GH2.txt, rejudge9.py, paired9.py, ev9gh2.py

Update 2026-10-04 — lookup fix turned on by itself (operator's call); routing stays off

The operator approved turning on the lookup repair (MEMSTORE_CAUSAL_V2) by itself. memory/memstore.py 1.15.4 now defaults the switch to on, and it is live with no restart. Routing (TRIGGER_CAUSAL_V2) stays off.

Why it ships alone. The preregistered rule covered lookup and routing together, and that pair showed no end-to-end gain. The lookup repair is a plain bug fix with exact checks (118/400 → 400/400 on the controls; "does A cause B?" 2/80 → 80/80; no new false links), and the package allows shipping that kind of fix on those checks alone. Answers should not change much: she calls causal on about 12.5% of causal questions. When she does, she now gets the right link.

Live check. For "does an oom kill cause swap filled to 1.8g/2g?", the old path returned two unrelated chains. The new path matched both named things and returned the recorded link (oom kill -causes(+)-> swap filled to 1.8g/2g, memory #475). It also said plainly where no link is recorded in the other direction.

Revert: set MEMSTORE_CAUSAL_V2=0, or change the default back to 0. Neither needs a restart.

Update 2026-10-04 — benchmark built (the base model is at ceiling on formal causal reasoning); lookup and counterfactual repaired; held-out end-to-end test: no gain, switches stay off

Built from the operator's two documents: the "Causal Reasoning Engineering Package" and its source draft. Their central rule shapes everything below. Fix the measurement first; never call the component improved because the graph grew, the tool got called, or the prose sounds more scientific. Three questions are kept apart:

Step 0: the facts. A read-only audit of the live system came first (DIAGNOSIS.md).

Reported problem Finding
0/40 withheld edges recovered The test could not succeed. It deletes the only link between two things, then asks the same graph to find it again. Only 10 of 4,639 linked pairs (0.22%) have a second route within two hops, so a perfect instrument would also score about 0/40. The source memory text was never withheld or consulted, and the scorer ignored direction.
"2/5, then 5/5" Small-sample noise. The only run on record is 5/5. With exact names and the edge in place, a one-end lookup finds it 399 times in 400.
causal() fails with the edge intact Reproduced, and it happens every time. A question naming both ends ("does A cause B?") makes both ends starting points, and the walk then refuses to step into a starting point. The edge being asked about can never be returned: 0/200. Reversed wording gives identical output, so "does B cause A?" looks just as supported.
Zero tool calls Routing. causal is in her tool list but has 0 calls from the Day or workers. Its only real use was the automatic injection on "why…" messages, and that matcher is case-sensitive: "Why did the job fail?" never fires, because every pattern is lower-case. causal_path, counterfactual and abduct are hidden from the Day.
Counterfactual surgery Wrong semantics, as reported. counterfactual.py cuts every outgoing edge of X and then walks from X. Every downstream effect "vanishes" by construction, which makes it a tautology.
Edge meaning 46 free-text relation verbs, with chronology stored as causation and some inverted "prevents" edges ("html escaping prevents prevention of xss attacks"). Polarity is stored but never read. Source-fidelity census: of 180 sampled edges, 71 (39%) are stated, in that direction, in the memory they came from. The rest are loose readings. None is reversed.
Numeric intervention machinery None anywhere. whatif (#35) covers five of her own budget and worker-limit mechanisms.

Track 1: does she understand causation? The test is generated worlds with an exact oracle. Each world is a small structural causal model: yes/no events, each set by a rule over its causes with random glitches and triggers. The oracle enumerates every random state in exact fractions and returns three things:

It reproduces the package's worked example exactly (0.30 / 0.50 / 1.00), and it agrees with an independent implementation and with Monte Carlo checks.

Generation What it tests
v1 (74 worlds, dev + held out) the three rungs on confounded, chain, redundant-cause and inhibitor structures; an effect from logged counts, with traps for a confounder (adjust), a mediator (don't), a later common effect (don't), an unmeasured common cause (can't be determined) and an unmeasured cause of the outcome only (it can be determined: the over-refusal control); claims from independence patterns over every graph consistent with them (Corr2Cause-style)
v2 (66 worlds, dev + held out) 5–6 event models with a signed effect, a counterfactual from several facts and the probability of necessity; the front-door case (identifiable despite a hidden common cause) and its broken twin; M-bias; four measured variables with a confounder and a common effect; independence patterns over five variables (29,281 graphs)

Result: the base model is not the bottleneck. On dev:

Arm v1 v2
Bare deepseek-flash 98.7 99.1
Prompt-only control 100 98.5
Piper's seat 95.5 —

It handles the front door, M-bias and five-variable equivalence classes without help. Piper's 3-point loss on v1 is two "can't tell" answers on claims that were determined and one over-refusal. That is a small harness cost, not a missing capability. A causal-calculation tool would be the package's "optional tool unnecessary", so none was built.

Track 2: does the graph contribute? This track runs matched lookup controls on her live graph (package §5). Cases are sampled from her real causal_edges:

The dev cohort and a held-out cohort are drawn with different seeds. Every case keeps the database truth, the raw tool result, the shown links and the verdict.

Lookup case Before (dev / held out) After (dev / held out)
"Does A cause B?" 1/40 · 1/40 40/40 · 40/40
Inverse wording (shows A→B, not B→A) 2/40 · 3/40 40/40 · 40/40
One end (k=50) 32/40 · 33/40 40/40 · 40/40
"Prevents" edge, polarity shown 0/20 · 0/20 20/20 · 20/20
Name variant 3/40 · 3/40 40/40 · 40/40
Unrelated pair, no false link 20/20 · 20/20 20/20 · 20/20

Track 3: do her answers improve? Operator-style questions about links she has recorded, asked in her seat:

The answer is FORWARD, REVERSE, or no recorded link. Only edges whose source memory states the link are used, so the answer is defensible from her own notes. Polarity questions were left out, because the extractor's double negatives would reward copying an error.

Arm (dev, 40 questions) Score Forward Inverse No link Delegated No answer Secs/item
Control (live today) 55.0 57 79 25 17.5% 25% 192
Lookup fix only 57.5 71 64 33 22.5% 20% 194
Lookup fix + routing 80.0 64 100 75 7.5% 5% 144

On dev, the lookup fix alone changed nothing (+2.5, 95% CI [−15, +20]). She almost never calls the tool: causal on 12.5% of questions. Otherwise she spends about 11 search and read calls hunting through notes. With the routing also fixed, dev showed +25.0 [+5, +45]. But the dev control ran beside two other arms on a loaded Firefly (192 s per question; at times her memory store was refusing writes), so that comparison was not clean.

The held-out test was preregistered before it ran (PREREG-GH.txt). Set GH has 40 new questions from a different seed. Both arms ran side by side under the same load, because the dev control had run beside two other arms.

Arm (held out, 40 questions, run side by side) Score Forward Inverse No link No answer Calls/item Secs/item Cost
Control (live today) 70.0 71 86 50 17.5% 10.5 70 $0.72
Lookup fix + routing 67.5 64 93 42 12.5% 7.6 61 $0.54

Paired −2.5, 95% CI [−20, +15]: the ship rule failed, so the switches stay off. Under matched load the control scores 70, not 55, which means the dev gain was mostly the load. The injection fired on all 40 held-out questions, so she had the right links. What she did with them is the finding:

The fixes.

Open decision for the operator. The lookup repair is an interface fix proven on exact controls: 400/400 lookup cases against 118/400 before, and "does A cause B?" 80/80 against 2/80, dev and held out. The package allows shipping such a fix on the controls alone. But the preregistered end-to-end test covered lookup and routing together and showed no gain, so it stays off until the operator says otherwise.

What this does not show.

Parts that do it (added)

Sealed sets A/B, A2/B2 and G/GH are root-only on the bench machine.

Test suite built · v1/v2 formal worlds, exact SCM oracle (base model at ceiling) · v3 her own recorded links · lookup repaired on controls (118/400 → 400/400) · LIVE config (lookup v2 on, routing off) on held-out GH 10-09: 55.0 vs fresh control 57.5, −2.5 [−20, +15] = no end-to-end effect (PREREG-GH2) · routing stays off (failed PREREG-GH 10-04)

Two parts. The first is graph mechanics: her held-out edge scorer, fixed so that it always computes a ceiling on the same run. The second, which matters more, is root-cause questions drawn from real past incidents on her boxes with known causes. Give her the symptoms and the logs; she must name the cause and the evidence that proves it. Scoring: root-cause hit rate, whether the cited evidence actually supports it, and zero credit for a correct cause with wrong evidence. The live-use part measures how many real fixes in the week named a verified cause before shipping.

Scoring rules shared by all 38 suites: How the tests work.

Research & attempts

Score history

Suite v1 retired
0255075100Oct 495.5
Suite v3 current
0255075100Oct 955

Dashed line: the suite's baseline. Shaded band: the ± interval. Hollow dots are anchor items carried over from an earlier suite. Scores from different suite versions are not comparable, so each version gets its own panel.

DateSuiteTypeScore±Live useCostNotes
2026-10-09v3sealed 551 —— HELD-OUT set GH, LIVE configuration (memstore lookup v2 ON, routing OFF), side by side with the control: paired -2.5, 95% CI [-20.0, +15.0] (10,000 draws, seed 9), 5 wins / 6 losses -> PREREG-GH2: no end-to-end effect detected; the switch stays as the operator set it (justified on the lookup controls, 400/400 vs 118/400). Judged 16:1x: the 09:23 judging returned the Claude spend-limit message and was re-run on the stored replies (no seat calls).
2026-10-09v3sealed 57.5— —— HELD-OUT set GH (40), fresh control run side by side with the live arm 10-09 09:23: both causal switches off (behaviour before 10-04), on today's memory graph. Matched baseline for the live configuration.
2026-10-04v3anchor 70— —$0.54 HELD-OUT set GH, lookup v2 + routing, side by side with the control: paired 0.0, 95% CI [-15, +17.5] -> preregistered ship rule FAILED; switches stay off. Injection fired on 40/40; she cites the links but reasons past them. 28% fewer calls, 25% cheaper. [Anchor since 2026-10-09: routing-on arm, which is OFF live; the live configuration is the 10-09 row.] [Corrected 2026-10-09: was 67.5; one item (GH014) zeroed by a silent judge failure, re-judged. Ship rule still FAILED.]
2026-10-04v3anchor 80— —$0.61 Dev set G, lookup v2 + routing (trigger 1.4.0): +22.5 [+2.5, +42.5] vs the dev control, which was not load-matched (see the held-out rows). [Anchor since 2026-10-09: dev set; routing is off live.] [Corrected 2026-10-09: control re-judged.]
2026-10-04v3anchor 57.5— —$0.81 Dev set G, lookup v2 only: 0.0 [-15, +15] vs the corrected control; she called `causal` on 12.5% of questions. [Anchor since 2026-10-09: dev set, not a held-out score.] [Corrected 2026-10-09: control re-judged.]
2026-10-04v3anchor 57.5— —$0.60 Dev set G, control. Ran beside two other arms on a loaded Firefly (192 s/item; memory store refusing writes at times), so not load-matched. [Anchor since 2026-10-09: dev set, not a held-out score.] [Corrected 2026-10-09: was 55.0; one item (G011) zeroed by a silent judge failure, re-judged.]
2026-10-04v3anchor 70— —$0.72 HELD-OUT set GH (40 questions about her own recorded links, source-supported edges), control = live lookup and trigger, run side by side with the treatment (preregistered). Forward 71, inverse 86, no-link 50; 17.5% no answer; 10.5 calls/item. [Anchor since 2026-10-09: 10-04 control on an older memory graph; the matched control is the 10-09 row (57.5).]
2026-10-04v1sealed 95.5— —$0.77 Formal worlds, dev set A (74 exact-oracle SCM worlds: rungs, identification traps, Corr2Cause), Piper seat k=1. Bare deepseek-flash 98.7, prompt-only 100; v2 (front door, M-bias, 5-variable classes) bare 99.1 / prompt 98.5. Base model at ceiling; her 3-point loss = 2 'can't tell' + 1 over-refusal.

Experimental record

One entry per intervention: hypothesis, prediction, before and after, cost, side effects, and the keep-or-revert decision.

2026-10-04 · v3 ✓ kept 29.5 → 100 (+70.5) · $0.00

Hypothesis. 'Does A cause B?' never returns the A->B edge because the walk refuses to step into a starting point; direction and polarity are ignored and short names flood the starting set.

Research sources
#9 package §5 (matched instrument controls), §11 P1 (query repair justified by exact controls).
Implementation / version
memory/memstore.py 1.15.2 causal_v2 behind MEMSTORE_CAUSAL_V2 (default 1 from 2026-10-04, memstore 1.15.4): both-direction links between named things with sign and memory, graph-node-only guarded matching, deterministic endpoint split, non-blocking access write.
Predicted improvement
Every declared lookup contract passes on dev and held-out cohorts; no new false links.
Regressions
None on the controls (unrelated pairs 40/40 no false link).
Lessons learned
Units: % of lookup control cases passed, dev + held out (118/400 -> 400/400; 'does A cause B?' 2/80 -> 80/80). Installed off because the end-to-end test showed no answer gain. 2026-10-04: the operator approved turning it on by itself (memstore 1.15.4 default on). Routing stays off.
2026-10-04 · v3 ✓ kept 50 → 100 (+50) · $0.00

Hypothesis. counterfactual.py 'remove' cut X's outgoing edges and walked from X, so every effect vanished by construction (a tautology).

Research sources
#9 package §6 (counterfactual oracle), §11 P3 (semantics), §15 (unsupported computation).
Implementation / version
memory/counterfactual.py 1.1.0: but-for with redundant causes (an effect is lost only if all its recorded supporters are X or lost; prevents is not support); kept effects name their other supporters; output says qualitative. Hidden from the Day as before.
Predicted improvement
Fixture effects classified correctly, including ones with another supporter.
Regressions
None; still qualitative.
Lessons learned
Units: % of fixture effects classified right (old marks the independently supported ones lost: 2/4 -> 4/4).
2026-10-04 · v3 ↺ reverted 70 → 67.5 (-2.5) ± 0 · $1.30

Hypothesis. She answers causal questions about her own notes badly because the right links never reach her turn: she does not call `causal`, and the auto-injection is case-sensitive and drops the named-pair links.

Research sources
#9 package §9 (routing defect), §11 P6 (conditional routing), PREREG-GH.txt.
Implementation / version
mailtool/trigger_gate.py 1.4.0 behind TRIGGER_CAUSAL_V2 (case-insensitive, named-pair asks, links first) + lookup v2; bench seat 1.18.0.
Predicted improvement
Held-out C9 up with CI lower bound > 0, no more non-answers, cost <= 1.25x.
Regressions
Forward 71 -> 64, no-link 50 -> 42 (noise-sized); inverse 86 -> 93.
Lessons learned
Dev +25 was mostly load: the matched held-out control scored 70, not 55. The links arrived (40/40) and she cited them, then reasoned past them; with 61% of edges loose readings, treating the graph as one witness is defensible. Switches off; live unchanged.