Research / Effects, recovery and replay

P10 · full · draft

Witnessed-Inverse Externs: Transactional Host Mutations with Checked Undo

shipped evidence full Effects, recovery and replay

State of the work. Mechanism and filesystem catalog ship on the Python tier with a dense test base; the TypeScript tier is partial (weaker guard, no directory-fd syscalls); no overhead or corpus-scale evaluation yet.

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

Planned to be combined with P12 (Classify-then-Lower: Mapping Shell Commands and HTTP APIs onto Reversible Effects) into a single paper.

Pitch

An extern that ships with its own checked inverse makes a host mutation transactional: commit keeps it, abort undoes it newest-first, and the line between reversible and irreversible is something the program engineers rather than something the world fixes.

Abstract

Languages that track effects classify host actions as irreversible unless the programmer writes a bracket or a compensation, and an agent that edits files therefore asks for approval on every write. We introduce witnessed-inverse externs. A witnessed[fs] extern returns a witness and declares an inverse; the compiler registers the inverse automatically, only on the success branch, as a third kind of teardown entry beside acquire and compensate. Unlike an acquire, a witnessed inverse does not run on a clean unload: commit discharges it and drops the witness, abort replays it newest-first in one stack with the bracket entries. The inverse must itself be a host-local, non-emitting, non-witnessed call and its witness must be serializable data, so a crashed session can replay it from a write-ahead log. We describe the design, a filesystem catalog (write, rm, move, mkdir) with copy-on-write preimage capture, and the runtime seam on the Python reference tier. We report what building it taught us: the first runtime replayed mid-session inverses in registration order and silently destroyed pre-session data while reporting no residue; a host-bound registration path registered the inverse nowhere; and a restore that installed whatever sidecar it found could be redirected by a same-user writer. Each became a regression test, and the fixes are stated as inverse-identity checks bound to the bytes captured. We state the limits: reversal does not undo observation, the guard is a runtime refusal not containment, and the ts tier cannot express directory-fd walks.

Problem and claim

Effect systems classify host actions as reversible or irreversible by declaration (no agent framework was surveyed). The consequence is that every file write counts as an irreversible crossing and needs a prompt, even though a snapshot and a rename would make it undoable. The design doc states the need directly: "every mutation today is an emission, so every one is irreversible by declaration and prompts for approval" (docs/witnessed-fs.md:3-8).

The claim: a host mutation declared together with a witness type and an inverse can be treated as a transaction entry. Three properties make this precise. (1) It is not an acquire: the inverse does not replay on a clean unload, and commit discharges it (docs/design/243-witnessed-externs.md:9-21). (2) The inverse is checked: it must be classified non-emission and non-witnessed, which excludes infinite regress, and it must be idempotent on replay (rules 3 and 5 of the design doc). (3) The restore installs only the sidecar it captured, which the design checks by (dev, ino) and ctime identity (docs/witnessed-fs.md:433-482); this holds under the stated premise that the host holds exclusive sidecar-directory write ownership (docs/witnessed-fs.md:224-226).

The argument is that this is a language feature with a runtime contract, not a library, because the compiler owns the Ok-conditional registration and the teardown order.

Contributions

  1. A third reversal form with explicit semantics: abort-only replay, commit-time discharge and witness GC, one LIFO stack shared with bracket entries (docs/design/243-witnessed-externs.md:9-21; Python seam at "Slice 2a as implemented", points 1-3; test tests/test_witnessed_abort_lifo.py:252).
  2. Static rules: effect-position only, Ok-conditional registration, inverse classification, serializable witness (design doc, "Correctness rules 243 must enforce").
  3. A filesystem catalog as the first instance, with copy-on-write preimage capture for write and rename-into-garbage for rm (docs/witnessed-fs.md:20-53; stdlib/fs.rvl:349, :401, :429, :448).
  4. Mid-session per-tool-call registration with newest-first drain, and the data-loss defect that motivated it (design doc, point 5; tests/test_witnessed_abort_lifo.py:131-210).
  5. Inverse-identity checks and receipts: restore installs only the sidecar it captured; a write receipt carries previous and new digests bound onto the witness (docs/witnessed-fs.md:433-482, :378-432; tests/test_fs_restore_sidecar_identity_1016.py; tests/test_fs_witnessed_receipt_binding_623.py:108, :130, :198).
  6. A statement of where the guarantee stops, taken from the docs (docs/witnessed-fs.md:604-645).

Evidence on main

Claimpath:line or testStatusNote
Witnessed is a fourth extern class; contextual keyword; capability-scopeddocs/design/243-witnessed-externs.md:24-45, :115-123; src/revl/parser.py:109-119shipped
Imported witnessed externs parsesrc/revl/parser.py:7005-7022shippedcontradicts docs/witnessed-fs.md:646-668, which still calls it deferred; the gate is deferred to lowering
Entry kind: abort replays, commit discharges and GCsdesign doc "Slice 2a as implemented" points 1-3; runtime Frame.transactional at backends/python/runtime.py:3868; tests/test_witnessed_wal_recover.py:91, :110shipped (py)
Single LIFO stack with bracket and transactional entriestests/test_witnessed_abort_lifo.py:252 asserts order w4 a3 w2 a1shipped (py)
Mid-session inverses parked and drained newest-firstbackends/python/runtime.py:3930 (transactional_method), :3372; tests/test_witnessed_abort_lifo.py:131, :153, :174, :192shipped (py)design doc says rust, java and wasm are LIFO "by construction" and that ts and go drain their parked list newest-first (docs/design/243-witnessed-externs.md:176-210); not verified on those tiers
Catalog: write, rm, move, mkdir with Result witnesses and Ok-conditional registrationstdlib/fs.rvl:349-448; tests/test_fs_stdlib.py:248, :261, :272, :282, :298shipped (py)
Commit persists, abort reverts residue-free, out-of-root write refusedtests/test_fs_stdlib.py:310, :335, :357shipped (py)
Residue lists enumerate committed and aborted crossingstests/test_fs_stdlib.py:389, :421shipped (py)
Inverse identity: restore installs only the captured sidecardocs/witnessed-fs.md:433-482; tests/test_fs_restore_sidecar_identity_1016.pyshipped (py); ts test file exists but was not runts test named in the doc: backends/typescript/tests/fs_restore_sidecar_identity.test.ts
Receipt binding with prev and new digests, expected-before guarddocs/witnessed-fs.md:378-432; tests/test_fs_witnessed_receipt_binding_623.py:108-262shipped (py), opt-in surface revl.fs
Host-bound registration path fixeddocs/witnessed-fs.md:566-603; tests/test_fs_witnessed_ambient_abort_1071.pyshippedtest file name taken from the doc; not opened
A local mutating inverse is verified, not claimeddocs/crash-recovery.md:375-399designedthe doc proposes a combined register and inverse-roundtrip evidence floor and states local mutating inverses are "permanently declared" under the strong floor; no test for the combined rule was found
Six-tier teardown of witnessed inversesCHANGELOG.md:166-172claimedthe changelog says six tiers; the fs docs describe the confinement pass as shipped on py and ts (docs/witnessed-fs.md:604-645). The six-tier claim is unverified.

Numbers that exist

Paper outline

  1. Introduction: reversibility as an engineered property; why a write needs a prompt today.
  2. The three reversal forms: acquire, compensate, witnessed. Table of replay on unload, abort, commit.
  3. Language surface and static rules: classification of inverse, Ok-conditional registration, witness as data.
  4. Runtime contract: commit versus abort discriminator, one LIFO stack, parked mid-session entries, WAL descriptors.
  5. The filesystem catalog: preimage capture, garbage directory, receipts.
  6. What broke: FIFO drain data loss, host-bound registration, sidecar identity. Each as a failing test, then the invariant.
  7. Evaluation: overhead, abort correctness corpus, recovery sweep (to be done).
  8. Limits and related work.

Threats to validity and limits

From docs/witnessed-fs.md:604-645 (paraphrased where short):

Further, from the same documents:

Fact-check

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

Cite this draft

@misc{revlp10,
  author       = {Thomas Moussajee},
  title        = {Witnessed-Inverse Externs: Transactional Host Mutations with Checked Undo},
  year         = {2026},
  howpublished = {\url{https://inso1337.github.io/revl/research/P10.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