Research / Effects, recovery and replay

P19 · full · draft

Counterfactual Incident Replay: Asking What a Different Policy or Action Would Have Changed, With Zero Live Effects by Import Shape

partial full Effects, recovery and replay

State of the work. Policy reader over a WAL and action reader over a session log are shipped with tests; no incident corpus, fidelity measurement or causal-cone computation yet.

This is a draft, not peer reviewed. Repository references link to revl at commit 45bec431f.

Planned to be combined with P18 (Time Travel over Typed Effects: Backwards Replay, Forward Replay and Execution Bisect), P20 (Replaying Agent Runs: A Write-Ahead Log That Seals Each Model Decision and States Which Replay Modes the Log Can Support), P21 (Branching Agent Sessions With Rewound Side Effects: Scope-Gated Rewind and a Frozen Parent) into a single paper.

Pitch

Because revl's admission rules and action classifier are pure functions, a logged incident can be re-graded offline under another policy, or with one agent action swapped, and the policy reader imports no runtime tier, so nothing in it can fire an effect.

Abstract

Post-incident questions of the form "would the new policy have stopped this" are usually answered by re-running the incident or by reading a policy simulator that never saw the incident. Both are unsatisfying for agents that act on the world: a re-run repeats the effects, and a simulator answers about a hypothetical rather than about the logged run. We describe two counterfactual readers in revl, a language whose compiler and gate mediate an agent's capability use. The first, revl replay WAL --under POLICY, regrades the boundary crossings logged in a write-ahead log under an alternate policy and a candidate composition, names the earliest crossing the new policy would have refused, and reports whether the candidate is admitted. The second, revl_counterfactual, takes a session's action log, replaces, inserts or drops one action, decides both arms with the gate's own pure classifier and ticket rules, and reports the first divergence and the downstream steps it changes. Neither module imports the runtime tier. For the policy reader, a test runs a full counterfactual in a fresh interpreter and asserts the runtime never enters the module table, and another pins the WAL digest equal before and after. The action reader is pinned only by a test that makes runtime calls raise, and eight of its nine tests are skipped unless the cordis package is installed. We state what the approach cannot say: the WAL holds no admit decision and no host response, so the first reader recomputes reach rather than re-deciding the crossing, and the gap for a free host-extern emission depends on the tier that wrote the log: on the Python tier such an emission is now recorded (backends/python/runtime.py:1388-1414), while the bound the reader prints still says it leaves no record, so for those logs the printed text is stale; other tiers were not checked. The implementation is covered by 14 tests for the policy reader, 9 for the action reader and 26 for the related policy-diff.

Problem and claim

Incident review asks counterfactual questions: would policy B have refused the crossing that hurt us, would this candidate component have been admitted, would the gate have decided differently had the agent done something else at step k. Existing answers either re-execute (unsafe for an agent whose actions are real) or evaluate the policy on synthetic inputs (not about the incident).

The claim is narrow and checkable: when the decision procedure is a pure function of logged data and static compiler output, a counterfactual can be answered with no effect authority, and the absence of authority can be a property of the code (no import path to a runtime) rather than a discipline. The claim is paired with an equally precise statement of where the answer is only a recomputation. The design doc says so itself: the crossing report recomputes the alternate policy over the reach the log names rather than re-deciding the crossing that was logged (docs/design/467-counterfactual-replay.md, section "What this surface cannot answer").

On main, the WAL reader recomputes reach; it does not re-decide the crossing and it does not compute a causal cone. The session-log reader does re-decide, with the gate's own classifier, and reports downstream steps changed by an approval shifting, but over the in-memory session log, not a WAL.

Contributions

  1. A WAL reader that regrades every logged emission crossing, in logged order, as permitted, refused or not-in-candidate under an alternate policy, names the earliest refused crossing, and separately reports the whole-policy admission verdict for a compiled candidate (src/revl/counterfactual.py:375 replay_under; CLI src/revl/cli/parser.py:1951-1965).
  2. A structural zero-effect property for the policy reader: no runtime import, WAL digest equal before and after, and a report field stating liveEffects: 0, each pinned by a test (tests/test_467_counterfactual_replay.py:499).
  3. A catalogue of printed bounds that the answer cannot cross (authority absent from the log, free host-extern emissions on a tier that does not record them, reach-leg only per crossing, torn WAL, realm rules without a candidate), carried on every report rather than in prose.
  4. A single-action substitution reader over a session's action log (replace, insert, drop) that decides both arms with the gate's pure parts, shifts approvals with the surrounding calls, and reports per step whether the original arm reproduces the receipts, so a divergence is not read off an arm that did not reproduce (src/revl/mcp/counterfactual_act.py:52-226, tests/test_counterfactual_act_1752.py).
  5. A policy-diff preview over a WAL with a bounded blast radius and a refusal to report "unchanged" on any leg the WAL cannot decide (src/revl/policy_diff.py, tests/test_468_policy_diff.py).

Evidence on main

Tree: revl main at commit 45bec431f.

Claimpath:line or testStatusNote
revl replay WAL --under POLICY [--candidate FILE...] existssrc/revl/cli/parser.py:1951-1965; src/revl/counterfactual.py:375shippedFirst slice
First refused crossing is the earliest in logged order; admission verdict is separatetests/test_467_counterfactual_replay.py:165, :210shipped
Without a candidate the question is "not answerable", exit 1tests/test_467_counterfactual_replay.py:302shippedtoken is not on the WAL
Refusal with no logged crossing behind it is reported as suchtests/test_467_counterfactual_replay.py:260shipped
Torn WAL: intact prefix read, tornWal bound addedtests/test_467_counterfactual_replay.py:376shipped
Zero live effects: no runtime tier in sys.modules, WAL unchangedtests/test_467_counterfactual_replay.py:499shippedOnly checks the Python reader's own interpreter
The report carries the boundstests/test_467_counterfactual_replay.py:392shipped
Free host-extern emission recorded in the WAL (issue #841)docs/design/467-counterfactual-replay.md section "What this surface cannot answer"; backends/python/runtime.py:1388-1414shipped (py, cordis-gated)The Python tier records a direct host-extern emission through extern_emit before the host body runs (backends/python/runtime.py:1388-1414), and tests/test_crash_recovery.py:766 asserts exactly one emission step for it (the test is skipped when cordis is not installed). The freeHostExterns bound text in src/revl/counterfactual.py:75-90 still says no record is written, so it is stale for logs written by that tier; other tiers were not checked. The design doc's pointer to backends/python/replay.py lines 1328-1339 is stale: that range is unrelated code and holds no #841 comment
Gate re-decision of the original admitsame doc, section on authority missing from the lognot built, not intendedThe WAL holds no admit decision
revl_counterfactual replace / insert / drop over a session logsrc/revl/mcp/counterfactual_act.py:52; docs/mcp-reference.md:671shippedSecond slice, issue #1752
Original-arm reproduction check; runs nothingtests/test_counterfactual_act_1752.py:92, :173shipped (cordis-gated)
Downstream step changes when an approval movestests/test_counterfactual_act_1752.py:129, :146shipped
Verb advertised read-onlytests/test_counterfactual_act_1752.py:215shipped
Policy-diff over a WAL, newly allowed / denied, bounded blast radiussrc/revl/policy_diff.py:1-80; tests/test_468_policy_diff.py:127, :179shipped, with a major caveatSee next row
Policy-diff over a WAL written by today's writerssrc/revl/policy_diff.py module docstringpartialThe docstring states every WAL written today is unscoped and its action set is empty; such entries are withheld, not reported clean (tests/test_468_policy_diff.py:272). The diff is only useful on a WAL with annotated scope
Replay readiness analysissrc/revl/replay_modes.py:86; tests/test_250_replay_modes.pyshipped (readiness only)A different mechanism from this paper
Causal cone of downstream actions from a WAL crossingnone foundmissingThe WAL reader does not compute it. The action reader reports only approval-driven step changes
Re-execution of either arm against a forked workspacedocs/design/467-counterfactual-replay.md "What was left out"designed, deferredA live executor

Numbers that exist

None as measurements. Test counts only, from counting test functions in the files (not a run): 14 in tests/test_467_counterfactual_replay.py, 9 in tests/test_counterfactual_act_1752.py, 26 in tests/test_468_policy_diff.py. No latency, accuracy or case-study data exists.

Paper outline

  1. Introduction: the incident question and why re-running is not acceptable for acting agents.
  2. Background: gate, WAL, capability tokens, boundary policy (docs/boundary-policy.md).
  3. Policy counterfactual over a WAL: crossings, candidate, verdicts, ordering.
  4. Action counterfactual over a session log: classifier, tickets, approval shifting, reproduction check.
  5. Zero effects by shape: import discipline, digest pinning, report field, the test with teeth.
  6. What the log cannot say: the printed bounds, and why not logging the decision is deliberate.
  7. Evaluation: incident corpus, fidelity of recompute, comparison with a generic policy engine.
  8. Related work and limits.

Threats to validity and limits

Fact-check

Claims in this draft were checked against revl main at commit 45bec431f: 60 checked, 6 corrected.

Cite this draft

@misc{revlp19,
  author       = {Thomas Moussajee},
  title        = {Counterfactual Incident Replay: Asking What a Different Policy or Action Would Have Changed, With Zero Live Effects by Import Shape},
  year         = {2026},
  howpublished = {\url{https://inso1337.github.io/revl/research/P19.html}},
  note         = {Research draft, not peer reviewed. Written against revl commit 45bec431f. Licensed CC BY 4.0.}
}

Text licensed CC BY 4.0, author Thomas Moussajee.

← All research drafts