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.
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:
Does she understand causation?
Does the graph or tool contribute?
Do her real answers improve?
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:
seeing: P(Y | X);
doing: replace X's rule and keep everything else;
imagining: the counterfactual for one past episode. It first works out what the random parts must have been, given what was observed, then changes X and keeps them.
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:
Both ends named ("does A cause B?").
Inverse wording.
One end only.
A "prevents" edge.
A name variant.
Two unrelated things.
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:
"From what you've got recorded: does X lead to Y, or is it the other way round?"
"What led to Y? Was it X, or is that backwards?"
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:
She treats the graph as one witness, not the answer. She cites the injected line ("no recorded link either way in the causal graph"), then reasons about the mechanism and commits to a direction anyway. Five of the no-link misses are that.
She disputes edges, sometimes rightly. One example: "the graph read narration order as causation… worth knowing it does that". With 61% of edges being loose readings of their source, distrust is a defensible policy.
The questions blur two things. "From what you've got recorded" mixes what is recorded with what is true. A no-link item marks a reasoned direction as wrong even when the reasoning is sound. That is a limit of this track, not only of her.
The fix made her cheaper and quieter. 28% fewer calls, 13% faster, 25% cheaper and fewer non-answers, but no more correct.
Both directions. When a question names two things, the links between them are checked in both directions and reported with their recorded direction, a sign (prevents = −) and the memory they came from. "NO RECORDED LINK" is said explicitly.
Starting points. These come only from graph nodes: exact normalised match, then a guarded substring match, then content-word overlap. Two-letter names no longer flood the starting set; one exact name used to match a median of 14 nodes.
Endpoint split. Endpoints are split from the question deterministically, so the language model's entity split can't break "security flag block" into three generic words.
Never blocks. The access-count write never waits on another writer. That had stalled the walk for 30 s under load.
Both switches are installed but off (MEMSTORE_CAUSAL_V2, TRIGGER_CAUSAL_V2). The live system behaves exactly as before.
Case-insensitive. The automatic injection now matches regardless of case.
Named-pair questions. It also fires on "does X cause / lead to / bring about Y" questions.
Links first. It shows the named-pair links before the nearby chains.
It is a machine trigger, not a prompt rule.
Counterfactual (memory/counterfactual.py 1.1.0, live; it had no switch to begin with):
But-for semantics. "What if X hadn't happened" is now a but-for analysis with redundant causes. An effect is lost only if every recorded supporter is X or another lost effect, and a "prevents" edge is not support.
Honest output. Kept effects name their other supporters. The output says it is qualitative: recorded support only, no probabilities.
It is still hidden from the Day.
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.
Real incident debugging. Workers debug with bash and file reads, and nothing here measures whether that reasoning is causal. The package's incident simulator and repair-regret track are not built.
Whether a cleaner graph would change her trust. 61% of edges are loose readings of their source. The extractor that writes them is unchanged; a typed relation (cause / enables / chronology / dependency) is the package's P5 and not done.
One model, k=1. Every question was asked once, of one model (deepseek-flash).
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.
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.
Date
Suite
Type
Score
±
Live use
Cost
Notes
2026-10-09
v3
sealed
55
1
—
—
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-09
v3
sealed
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-04
v3
anchor
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-04
v3
anchor
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-04
v3
anchor
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-04
v3
anchor
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-04
v3
anchor
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-04
v1
sealed
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✓ kept29.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.
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✓ kept50 → 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).
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).
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.
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.