Skip to content

Observer gate: compile-time elision of observer bookkeeping (#915) - #1034

Merged
InauguralPhysicist merged 26 commits into
mainfrom
perf/915-observer-elision
Aug 23, 2026
Merged

Observer gate: compile-time elision of observer bookkeeping (#915)#1034
InauguralPhysicist merged 26 commits into
mainfrom
perf/915-observer-elision

Conversation

@InauguralPhysicist

@InauguralPhysicist InauguralPhysicist commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

The #915 observer gate: a compile-time liveness scan (obs_needed per EigsState) that lets a program provably never reading observer state skip entropy bookkeeping entirely, plus the eager literal-load_file compile that makes it fire on real consumers.

Banked: 8.51x on EigenMiniSat 4x4 Tseitin on the final tree (n=5 interleaved, one byte-identical binary, solver counters identical across all 10 runs; series 8.47–8.53 across the loop). Corpus gate coverage 302/441. The ungated observer walk was 88% of that workload's runtime.

Verification — the loop is TERMINATED

Nineteen blind-critic rounds, closing on the stated terminator: two consecutive clean rounds (18: exhaustive shared-field induction; 19: novel-surface sweep with evidence-of-absence reporting) plus a twice-green 18/18 CI matrix including the macOS legs and the TSan lane.

Deliberately conservative

Filed, not hidden

#1031 (double-compile, budget-bounded, population-pinned), #1032 (env-flag convention), #1033 (memo scan), #1027 (descriptor pre-call history), #1035/#1036 (pre-existing races, with repros and lane-witness notes).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt

InauguralPhysicist and others added 22 commits August 21, 2026 10:08
The #915 gate skips observer bookkeeping for programs that can never
interrogate it. The whole risk of that change is SILENT-WRONG: a program that
does reach the observer, misclassified as one that does not, still runs and
still prints — just with a dead observer channel. No crash, no leak, no failing
assert unless a test happens to assert on the affected binding. "The suite
passes" does not test for that, so the bar is every tracked .eigs program
producing byte-identical stdout+stderr+exit code under both builds.

Two properties, both of which a gate harness needs to avoid reporting success
it has not earned:

  - It establishes the deterministic subset by diffing the baseline AGAINST
    ITSELF first. That excludes 23 programs — the examples/invariant_* family
    prints wall-clock timings — which would otherwise read as gate breakage on
    every run forever.
  - It refuses to pass vacuously: a clean diff against a build that gated
    nothing proves nothing, and the harness says so rather than printing PASS.

capture/compare rather than two binary paths, because the runtime resolves its
stdlib relative to its own executable directory: a build copied outside src/
silently loses load_file of lib modules, and the diff would then be measuring a
broken stdlib path instead of the gate. src/eigenscript is a hard link the
Makefile re-points per variant, so only one build is runnable at a time.

Validated with a planted fault before being trusted. Against a deliberately
broken gate (observer disabled outright — #915's own ceiling probe) it reports
FAIL on 27 programs, exactly the observer-facing set: test_convergence_oracle,
test_trajectory, test_entropy_*, test_windowed_converged, test_observer_large,
the observer_corpus programs, examples/observer_predicates,
examples/structural_observer, and test_observer_level_set from #862.

Not wired into run_all_tests.sh: three full-corpus captures cost ~6 min, which
does not belong in the default suite. It is a pre-merge gate for the #915 change.

Refs #915

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#915)

Every assignment computed the entropy of the assigned value, walking the whole
reachable container graph, whether or not anything ever observed the binding. On
EigenMiniSat — which uses zero observer features — that was 88% of wall time.

Measured, Tseitin torus 4x4-odd, n=5 vs n=5, arms interleaved:

    gated  33.84 s (median of 35.02 33.84 34.03 33.78 33.81)
    forced 294.36 s (median of 296.57 293.57 294.36 293.15 295.27)
                                                          8.70x

ONE byte-identical binary serves both arms (EIGS_OBS_FORCE=1 restores pre-gate
behaviour), so no second build can confound the comparison. Final counters are
identical across arms — status=UNSAT conflicts=9986 resolutions=33873
decisions=15275 propagations=44166 restarts=11 — so both did the same search and
the ratio is like-for-like. #915 measured the ceiling at 8.50x with a
semantics-breaking early return; a runtime gate reaching it is expected, since
that probe had the same shape.

That does not work here: load_file is a RUNTIME builtin with no module cache
(builtins_host.c), so the compiler never sees a loaded module's AST — and
EigenMiniSat opens with four load_file calls, so static elision would either be
unsound across load boundaries or never fire for the workload that motivated the
issue. The ceiling was itself measured with a runtime early return, so a runtime
gate captures all of it; elision would add only the 4.21% dispatch slice.

chunk_reads_observer() (chunk.c) scans COMPILED BYTECODE, not the AST. An AST
walker switches over ~30 node kinds and a forgotten kind falls to default —
which for this question means "does not observe", the silent-wrong answer.
Bytecode is what actually executes, so a new AST node compiling to a reader
opcode is caught with no change here. The instruction walk is driven off
op_verify_operands, the same operand table the verifier and disassembler use
(#737 exists because a hand-written second copy drifted on 15 opcodes).

It also scans the constant pool for observer-read builtin NAMES. `local r is
report` emits no reader opcode at all — GET_NAME + CALL — so an opcode-only scan
reports "unobserved" and silently breaks every aliased report.

Hooked at compile_ast, the one choke point every compilation path funnels
through (script, eval, load_file, import, REPL, embed, ext_http handlers), so
there is no caller list to drift. Monotonic: once on, never off.

Force-on where evidence is unavailable rather than gating on its absence: the
REPL and the embed API (line N+1 can interrogate a binding from line N, and no
scan sees a line not yet typed), and assembled chunks from vm_run_bytecode /
sandbox_run, which never pass through the compiler — the same situation #831
documents one line away.

g_trace_obs_hist is READ at the gate rather than mirrored into g_obs_needed at
each of the five sites that arm it. Mirroring would be five hand-maintained
copies of one fact, and a sixth arming site added later would silently record a
tape full of dead observer snapshots.

The drift gate below flagged eigs_observe_safepoint as a real reader: it dumps
observer state on a pending SIGUSR1 and is called from every loop check. Gated,
that dump printed every binding as "equilibrium" with an empty trajectory — which
reads as "everything is settled" when the truth is "nothing was ever measured".

A SIGUSR1 dump exists to inspect an ALREADY-RUNNING process, the one case where
re-running under EIGS_OBS_FORCE=1 is not available, so dropping the capability
would have traded a shipped debugging feature for throughput on exactly the
programs most worth debugging. The first signal now declares the absence of data
and ARMS observation; the second carries real trajectories. Safe to flip there:
the dump runs at a loop safepoint, not in the handler, and the flag is monotonic.
test_sigusr1_dump.sh asserts both phases and keeps every original data assertion.

- Suite 3874/3874, 0 failed, leak tally 0.
- tools/observer_gate_diff.sh: 438-file byte-exact differential, 412/413
  identical. The exception is tests/tsan_seeded_race.eigs, a deliberately-seeded
  race that reproduces baseline 6/6 under the gated build — the harness's 2-run
  determinism filter admitting a rarely-flaky fixture, not a gate regression.
  Harness validated first against a deliberately broken gate (observer disabled
  outright): FAIL on 27 programs, exactly the observer-facing set.
- tools/observer_reader_ops_check.py: drift gate proving every opcode whose VM
  dispatch reaches an observer READ is listed in chunk_reads_observer. An
  unlisted reader means a program gates itself off and then reads slots nobody
  updated — returning "equilibrium" forever with nothing to fail on. Validated by
  planting a missing reader (removed OP_REPORT_NAME → FAIL, restored → PASS).
  Wired into the suite as [99n], which also pins that the gate CLOSES on a
  program with no observer surface (a gate that stopped firing would make every
  perf number here stale) and OPENS on the aliased form.

EIGS_OBS_FORCE=1 restores pre-gate behaviour; EIGS_OBS_GATE_STATS=1 reports the
per-unit decision.

Refs #915

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All three are SILENT: the program runs, prints, exits 0, and every observer
query returns a rest band. None was caught by the suite or by the full-corpus
differential, which is the point — a gate whose failure mode is silence needs an
adversary, not more green checkmarks.

1. WORKER THREADS — the significant one. obs_needed lived on EigsThread, which
   eigs_thread_attach xcalloc's fresh per worker while only the spawning thread
   ever runs compile_ast. So every assignment executed on any spawned worker
   skipped observation, permanently, while the gate's own stats still printed
   "observed" and EIGS_OBS_FORCE=1 could not rescue it. Moved to EigsState,
   beside the observer thresholds that are already per-state for the stated
   reason ("shared across worker threads").

   The corpus differential was VACUOUS here: its only spawn+observer program,
   tests/test_obs_mt_race.eigs, asserts iteration counts and never report
   content, so it printed byte-identical output on both builds.

2. eval — compiles at RUNTIME, after this unit's assignments have already run,
   so its scan cannot arrive in time to have observed them. The source may not
   exist until it is built ("rep" + "ort of a" defeats any name match), so the
   presence of eval at all is the signal. EigenMiniSat uses none.

3. load_file — same shape: parent assigns, then loads a module that
   interrogates. Closed by pre-scanning string-literal load targets, so the
   consumer that motivated this issue (four literal load_file calls) keeps its
   gate instead of being forced on wholesale.

Two things the pre-scan got wrong first, both worth the comments they now carry:
matching str_val flagged observer words inside ordinary message STRINGS in
lib/solver.eigs and lib/bench.eigs, taking EigenMiniSat from 6 gated units to 4;
and opening candidate paths relative to the working directory failed for
lib/int_vector.eigs, which resolves into EigenScript's stdlib rather than the
consumer tree. It now matches IDENT tokens only, matches the temporal
interrogatives by token TYPE, and resolves through resolve_eigenscript_file —
the resolver load_file itself uses.

Suite 3877/3877, 0 failed. Corpus differential 413/413 byte-identical.
[99n] now covers all three classes, asserting the observed VALUE rather than the
gate's stats — a stats-only check passes the worker-thread bug.

Refs #915

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adversarial review broke the pre-scan three times, each in a new place:

  - a computed path (`parts[0] + parts[1] + ...`) is not a string constant, so
    nothing resolved and nothing was scanned;
  - `resolved_any` was an OR across all candidates, so ONE benign literal load
    disarmed the conservative fallback for every other load in the same unit —
    the failure got LESS likely as the program got simpler, which is why no
    corpus differential would ever have found it;
  - the six predicates lex to their own token types (TOK_CONVERGED, ...), so
    matching them as TOK_IDENT was dead code and a module using the documented
    "preferred form" went unseen.

And `import` was never scanned at all: it is an opcode with a bare-name operand,
so nothing about it reaches the constant pool as a string that a name list could
match — while `import` is the idiomatic module form throughout examples/.

The common root is that the pre-scan was trying to prove something about code
that does not exist yet. Each fix leaked somewhere new, which is the signal to
stop patching and change the shape rather than to try a fourth time.

So: presence of a dynamic-code construct — eval, load_file, or OP_IMPORT — now
forces observation on. Sound by construction and needs no path resolution, no
transitive file scanning, no module name lists, and no depth cap. It is a net
deletion.

THE COST IS REAL AND IS NOT HIDDEN: EigenMiniSat opens with four load_file calls,
so it now gates 0 of 6 units and the measured 8.70x does NOT apply to it as
shipped. 118 of the first 190 corpus programs still gate, so the change is not
worthless — but the consumer that motivated the issue no longer benefits, and
recovering that needs the loaded modules compiled EAGERLY at parent compile time
with the real compiler (not token heuristics), which is a larger piece of work.

Refs #915

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The checker existed to prove every observer-reading opcode is listed in
chunk_reads_observer. Adversarial review found it was reporting
OP_LOOP_STALL_CHECK as "listed but not reading" — the exact opcode this file's
docstring cites as the reason the callee closure exists. It detected 9 readers
against 16 listed, so its real sensitivity was roughly half and a genuinely new
reader opcode had a good chance of passing. A gate that cannot see the example in
its own docstring is decoration.

Root cause: the discriminator was "calls a named reader FUNCTION".
obs_stall_trajectory (vm.c:47) reads observer state by direct struct access —
env_obs_slot(...) then s->dH, s->entropy — and calls no named reader at all.

Fix: touching a slot is the signal, and it seeds the closure so a helper reading
by struct access marks every opcode that calls it. env_obs_slot cannot simply be
a reader, because writers call it too and listing a writer would force
observation on at every assignment — deleting the gate. So the four write-only
opcodes are waived BY NAME with their reason, per the visible-exemption rule.
A new opcode touching a slot must now be classified deliberately rather than
defaulting to invisible.

Detection: 9 -> 13 readers.

Validated by planting the fault the OLD checker could not catch — removing
OP_LOOP_STALL_CHECK from chunk_reads_observer's list, which previously left the
checker green. Now exit=1 with the opcode named; restored, exit=0. The earlier
validation (deleting OP_REPORT_NAME) only ever proved it catches opcodes it
already detected, which is why the blind spot survived it.

Refs #915

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…odules gates (#915)

#915's gate was PARKED because the safe version bought the consumer that
motivated it nothing: any `load_file` forced observation on, EigenMiniSat opens
with four of them, and it gated 0 of 6 units.

A load whose target is a STRING LITERAL is now compiled EAGERLY at the parent's
compile time with the real compiler, so the module's own verdict lands before
line 1 of the parent runs and the ordering hazard is gone. Everything else keeps
the old answer: a computed path, an alias, an unresolvable file, a unit that can
chdir, more than 64 loads, or a multithreaded compile. ONE unrecognized use of
the name poisons the whole unit — the fallback is an AND, not the OR that was
this feature's recorded failure #2.

`import` stays conservative and #915's import half stays open: its resolution is
project-first-then-stdlib against a per-module dir, and replicating that here
would be a second resolver free to drift from the first (#737).

MEASURED, EigenMiniSat 4x4 Tseitin, n=5 per arm, interleaved, ONE binary
(baseline arm = EIGS_OBS_FORCE=1):

    base  median 293.00 s   (293.93 292.73 293.14 292.93 293.00)
    gated median  34.29 s   ( 34.42  34.34  34.20  34.26  34.29)
    speedup 8.54x

with the solver's counters identical across arms (conflicts=9986
resolutions=33873 decisions=15275 propagations=44166 restarts=11), i.e. the same
search. Corpus gate coverage 219 -> 302 of 441 programs.

GATES: corpus differential 417 byte-identical (one binary, both arms) and FAILing
on 69/417 against a deliberately broken gate; release suite 4090/4090; ASan
+UBSan detect_leaks=1 4079/4079 with leak tally 0.

THREE DEFECTS THIS FOUND IN ITSELF, all by measurement rather than reading:

1. `#ifdef EIGENSCRIPT_FREESTANDING` where the tree uses `#if`. eigenscript.h
   defaults the macro to 0, so it is ALWAYS defined: the stderr muting compiled
   to an unconditional `return -1`, every literal load took the failure path, and
   the whole feature silently reverted to the old behaviour. It still compiled,
   because nothing in the surviving branch referenced dup() or open(). The corpus
   differential passed byte-identical (conservative IS the baseline) and seven of
   the eight new probes passed vacuously. ONE check caught it: the positive
   control asserting the gate CLOSES.
2. The eager pre-pass DUPLICATED a loaded module's compile diagnostics. The
   differential was blind: no corpus program loads a module that fails to
   compile. Now suppressed at fd 2 (48 raw fprintf(stderr) sites across three
   files with no helper to hook), and asserted as a differential, not a string.
3. The new bytecode walker read operands big-endian; the VM is little-endian.

Suite section [99u] (renamed from [99n], which this branch's rebase collided with
#958's gate — the other five pre-existing collisions are #1025) carries 17 pinned
checks, each verified to fire against a build with its mechanism removed.

tools/obs_reader_sync_check.sh replaces tools/observer_reader_ops_check.py: the
old checker derived the reader-opcode set from the C SOURCE, the open level where
five separate derivations have now produced confident wrong answers. The new one
reads BOTH homes — the /*obs:READS*/ markers (#1024, closed by construction) and
the case arms of chunk_reads_observer() — and asserts the implication between
them modulo two pinned, reasoned exemptions. It found a live divergence on its
first honest run. 5-mutation train, all caught, plus a positive control.

tools/observer_gate_measure.sh banks the measurement recipe. Its own counter-
equality check was vacuous twice over before it worked (a pattern that matched
nothing, and state assigned inside a command-substitution subshell); both causes
and the VOID guard are documented in its header.

Refs #915, #972

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
Three blind critics, three FAIL verdicts, seven real defects. Five were in code
written to FIX or VERIFY something rather than in the feature.

SOUNDNESS — time-of-check / time-of-use (critic 1, two executed repros)

The eager pass reads a literal target when the parent COMPILES; load_file reads
it again when the call RUNS, and the whole program runs in between. Executed:

    x is 1.0 / x is 2.0 / x is 3.0
    write_text of ["plugin.eigs", "print of (report of x)"]
    load_file of "plugin.eigs"

gate closed, EIGS_OBS_FORCE=1 -> `moving`, gated -> `equilibrium`, rc 0 both
sides. Second instance never touches the scanned file: a file created in the cwd
SHADOWS it, because resolve_eigenscript_file tries cwd before the script dir.

The `chdir` opacity check was the wrong population key (mechanical-gates §60) —
a one-element denylist enumerating one cause when write_text, rename, mkdir,
remove_file and any subprocess reach the same state. Deleted. The guard is now
on the OUTCOME, in builtin_load_file: compiling the module flips the observer
bit 0 -> 1 exactly when the gate closed on stale evidence, and that raises,
naming EIGS_OBS_FORCE=1. The bit is monotonic and the missing history cannot be
reconstructed, so late is not recoverable — loud is.

FREESTANDING BUILD BREAK (critic 3) — the same defect class, made twice

read_file_util does not exist under EIGENSCRIPT_FREESTANDING=1 (builtins_host.c
is a whole-TU carve-out). The earlier #ifdef-vs-#if fix repaired the test on the
mute helpers and left the CALLEES outside the guard, so `make freestanding-check`
and tools/embed_stack_soak.sh both failed at the LINK step. Both are CI-only,
which is why release 4090/4090 and ASan 4079/4079 could not see it. EigenOS
consumes this runtime in that profile.

C STACK — `char resolved[8192]` in the loop made the frame 8,864 bytes on EVERY
compile_ast, recursing to depth 8: 70,912 bytes of that buffer alone, more than
the entire 64 KiB budget embed_stack_soak.sh enforces. Moved to the heap;
compile_ast's frame is now 976 bytes.

MODULE-DAG BLOWUP — the pass walked the load graph as a TREE. Measured on a
synthetic 8-file DAG: 131,072 re-compiles of one leaf, 313,122 opens, 145,636
fd-2 mute cycles, 8.5s vs 1.2s — a 7x REGRESSION from a feature whose purpose is
speed. Fixed with a thread-local memo of resolved paths, persistent for the run
(load_file has no module cache by design, #496) and consulted BEFORE the read
(resolve is an access probe; read pulls the file and discards it). Now 1.43s vs
1.31s, 16,385 leaf opens against the program's own 16,384, and 7 mute cycles.
Staleness is not a soundness question here: skipping a re-scan can only make the
pass MISS a module that has since become observing, which is precisely what the
outcome check above raises on.

--lint / LSP no longer reach the filesystem on the gate's behalf
(g_obs_gate_scan_enabled). The LSP recompiles on every didChange, so this had an
editor stat/read/compiling a file's load targets on every keystroke.

APPARATUS (critic 2) — the gates were the weakest part

  * obs_reader_sync_check.sh advertised --selftest and HAD NO SUCH BRANCH; the
    "5-mutation train" behind it was a one-off banked nowhere. All four
    assertion bodies could be gutted while it printed PASS on a tree carrying
    that assertion's fault, because ck() sat outside the loop bodies and the
    count pin was invariant under exactly that mutation (§45). CHECKS now counts
    COMPARISONS (38, not 8), and --selftest is real and wired into the suite: a
    FAULTS train (the gate must go red) and a WITNESS train (gutting an
    assertion must let its fault SURVIVE, proving that assertion and not a
    neighbouring floor does the work). 10/10.
  * observer_gate_diff.sh printed `PASS — 0 programs byte-identical` on empty
    captures. Its only floor was a RATIO, and the denominator shrinks with the
    numerator when a capture is killed partway. Absolute floor added.
  * observer_gate_measure.sh accepted a bare `DONE` with no counters (`-z` is an
    emptiness test, not a floor), and printed "identical across all N runs"
    ABOVE a VOID verdict. Both fixed.
  * suite check 17 was a bare cmp; both arms degrading to the same error passed.

Two self-inflicted regressions caught by the re-run: check 13 cd'd away from the
RELATIVE $EIGS_BIN so it executed nothing and reported 0 (a probe that cannot
run is not a probe), and check 16 pinned a gate STATE the memo legitimately
moved — the mutual-load cycle now terminates on the memo rather than by
exhausting the depth bound, and with both modules observer-free, closing is
correct. It now asserts termination plus arm equality.

GATES: release 4094/4094; ASan+UBSan detect_leaks=1 4083/4083, leak tally 0;
corpus differential 417 byte-identical; freestanding-check both stages; embed
stack soak 64 KiB; sync-gate selftest 10/10; EigenMiniSat still 6/6.

Refs #915, #972

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
Two more blind critics, two more FAIL verdicts. Both broke the ROUND-1 FIXES
rather than the original feature, which is now the established base rate here.

SECOND SOUNDNESS BREAK — vm_run_bytecode (critic A)

`builtin_vm_run_bytecode` and `builtin_sandbox_run` opened with a LATE
`g_obs_needed = 1`. That cannot work: the bit is monotonic, so flipping it when
the descriptor runs does nothing for host assignments that already executed
unrecorded. Executed:

    x is 1.0 ; loop 40x: x is x * 2.0
    print of (vm_run_bytecode of [1, [83, 0, 0, 40], ["x"]])

answered `equilibrium` under the gate and `diverging` without it — a geometric
runaway reporting settled, the inversion #861's own comment says must never
happen, at rc 0 with nothing to fail on. Descriptors resolve the HOST's bindings
(g_builtin_call_env), so nothing stands between them and module-scope slots.
Both sites now carry the same outcome guard load_file uses. sandbox_run was
unexploitable only because its env is a sealed root — the sandbox's defence, not
the gate's.

THE ROUND-1 TOCTOU GUARD WAS ONE-SHOT AND OVER-BROAD (critic B)

It compared the monotonic bit before/after the module compile. Both halves broke:

  * ONE-SHOT — the first detection flips the bit, so every later load saw
    "already open" and skipped. The error is catchable, so ONE `try:` around the
    first load disarmed the guard for the rest of the run and restored the exact
    silent-wrong answer it was written to stop.
  * OVER-BROAD — the eager pass bails conservatively for six reasons, only one
    of which is staleness. `L.count > 0 && g_vm_multithreaded` is reachable at
    run time but not at the parent's compile time, so `spawn` + a module that
    itself loads a module became a hard error with every clause of the message
    false. That shape is SHIPPED: lib/io.eigs loads lib/string.eigs.

The predicate is now the module's OWN verdict (chunk_reads_observer on the
compiled module) with a sticky `obs_history_gap` precondition, rather than a bit
that the detection itself changes.

AND THE SAME OVER-BROAD MISTAKE, MADE AGAIN AT THE DESCRIPTOR SITE

The first descriptor guard asked "does this chunk read observer state at all",
which broke 57 assertions in tests/test_vm_run_bytecode.eigs — the self-hosting
bridge legitimately assembles chunks with readers over their OWN frame slots.
Narrowed to chunk_reads_named_binding(): only a NAME-operand read that resolves
in the host env can reach a binding whose history is missing. Residual stated in
the code: the bare OP_PREDICATE form has no operand to inspect and is not
covered.

THE SELFTEST WAS PASSING FOR THE WRONG REASON (§21)

Fault E planted by DELETING a `case` line, which also tripped SWITCH_FLOOR — so
the FAULTS row passed via a floor and the WITNESS claim held only with floors
zeroed, i.e. not in the production configuration. Replanted by ADDING a marker
(§41: replace, don't delete). That change immediately caught a real break: the
vm_run_bytecode refactor had moved the `case` arms into chunk_has_reader_opcode,
so the gate's extraction anchor found ZERO opcodes and SWITCH_FLOOR went red —
which is why that floor is absolute and not a ratio. Suite check 21 now floors
`SELFTEST: N passed` too; rc 0 alone is satisfied by a one-row selftest.

THE DIFFERENTIAL VIOLATED §10 — THE RULE ITS OWN ISSUE BOUGHT

A round-1 critic reported it; I deferred it as pre-existing. It then produced the
predicted false alarm: tests/tsan_seeded_race.eigs, a DELIBERATELY seeded race,
agreed with itself across both baseline samples, was admitted to the statistical
filter, diverged in the third capture, and reported as `the gate changed
observable behaviour on 1 program`. Measured afterwards on ONE fixed build: 8
runs gated gave stderr of 43/268/43/43/43/43/43/43 bytes and 8 baseline gave
246/43/43/43/43/43/43/166 — nondeterministic in BOTH arms, and it SIGSEGVs under
the capture ulimit either way. Now denied BY NAME with that measurement, per §10.

Also corrected: three comments still claiming the deleted `chdir` opacity check
(§6/§61 — a claim in a comment is load-bearing), and the lint/LSP opt-out's
stated reason, which asserted lint "touches nothing but the file in front of it"
— false before this change, since E003 already realpath-resolves and opens
literal load_file targets. The opt-out is right; the justification was not.

Two stale probes fixed: checks 13/18 grepped an error message the rewrite had
reworded, so they reported 0 = "the guard did not fire" when it fired correctly.

GATES: release 4096/4096; ASan+UBSan detect_leaks=1 4085/4085, leak tally 0;
corpus differential 416 byte-identical; freestanding-check both stages; embed
stack soak 64 KiB; sync gate 38 comparisons + selftest 10/10; EigenMiniSat 6/6.

Refs #915, #972

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
Two critics, two FAIL verdicts, eight defects. The two worst were in the ROUND-2
FIXES — including a regression this commit's parent introduced.

SOUNDNESS: A SLOT-OPERAND DESCRIPTOR READ REACHES HOST BINDINGS

Round 2's descriptor guard inspected only NAME operands, on the written
reasoning that "a SLOT-operand reader addresses the descriptor's own frame and
cannot reach a host binding". That comment was false and load-bearing:
vm_execute hands the descriptor the HOST env and callframe_init makes it the
frame's fn_env, so at top level the descriptor's frame slots ARE the host's.
A critic took round 2's own repro and swapped the operand:

    x is 1.0 ; loop 40x: x is x * 2.0
    print of (vm_run_bytecode of [1, [81, 252, 0, 40], []])     # OP_REPORT_SLOT

-> `equilibrium` under the gate, `diverging` without, gate closed, rc 0.
chunk_reads_host_observer() now treats top-level slot readers and the bare
OP_PREDICATE as host reads, bounded by the host's binding count so the bridge's
out-of-range boundary fixtures stay exempt. Nested function chunks get a fresh
call env and keep the exemption.

REGRESSION, BISECTED TO 6e9e0e1: THE DESCRIPTOR STOPPED ARMING THE OBSERVER

Round 2 deleted `g_obs_needed = 1` from both descriptor sites, reasoning that a
late flip cannot help. That is right for host bindings assigned BEFORE the call
and wrong for everything the descriptor does AFTER it — so a descriptor that
wrote a geometric series into its own frame slot and read it back answered
`equilibrium`. The tell sat two lines below the whole time: chunk_arm_temporal()
is #831's "a descriptor must turn recording ON itself" for the temporal channel,
and its observer twin had been removed while the sibling was left in place. Both
are needed; the guard covers before, the arming covers after.

Why no gate saw it: tests/test_vm_run_bytecode.eigs is byte-identical under
EIGS_OBS_FORCE=1. Every assertion lands on a rest value, so it has ZERO
sensitivity to the observer being dead and the corpus differential is
structurally blind to the class. Check 24 is therefore written to assert on
observed CONTENT.

APPARATUS: THE FIX FOR FAULT E WAS NEVER CARRIED ACROSS TO FAULT D

Round 2 replanted fault E by ADDING a marker, because planting by DELETING a
`case` also trips SWITCH_FLOOR — so the FAULTS row passes for the wrong reason
(§21) and the WITNESS row cannot tell "assertion present and sole" from
"assertion already gone". Fault D still planted by deletion, and a critic
deleted the §3 waiver-liveness assertion with the gate, its own --selftest AND
the suite all green. D now plants in the gate's EXEMPT list, where the switch
count does not move.

BOTH NON-VACUITY FLOORS ARE REDUNDANT — MEASURED, NOT ASSUMED (§42)

A critic zeroed each with everything green and could not tell redundant from
decorative. Two runs settle it: with the derivation broken and floors zeroed the
HARD direction still fires; with the marker tool emitting nothing the SOFT
direction still fires. Recorded in the header with both commands. They stay
because they NAME the failure precisely — that is how the extraction anchor
breaking (when chunk_has_reader_opcode was split out) got diagnosed in one read
— and they deliberately carry no self-test row, because a row that cannot fail
for its own reason is decoration.

Also fixed, each executed:
  * checks 9 and 16 passed against a do-nothing binary. Check 9 is a NEGATIVE
    assertion satisfied by silence, and its own comment claims it is the one
    check that catches the eager compile going dead. Both now require a
    substantive reference first — the guard check 17 already had one screen below.
  * the --selftest resolved a DIFFERENT artifact than the one running: the
    fallback silently redirected the trains onto the tracked file, so a copied
    mutant could never self-test itself (§32, resolve once).
  * `speedup:` printed ABOVE a VOID verdict — the number that gets pasted into a
    PR, one line above its own invalidation.
  * the differential now reports `informative` vs `silent`: 68 of 416 programs
    emit nothing, so 16% of that headline was "" == "".
  * a waiver pointed at suite section [99n] (the #958 operand-width gate) when
    it meant [99u]. A pointer in a waiver is load-bearing (§6).

Recorded, not acted on: six of the eight OBS_BUILTINS names have no witness, but
a 481-name behavioural sweep found ZERO undeclared observer readers, so the list
is conservative padding rather than a hole.

MEASURED on this tree, EigenMiniSat 4x4 Tseitin, n=5 interleaved, one binary:

    base  median 292.80 s   gated median 34.39 s   ->  8.51x

counters identical across all 10 runs. Three independent measurements this
session: 8.54x / 8.55x / 8.51x.

GATES: release 4098/4098 (25 pinned checks in [99u]); ASan+UBSan detect_leaks=1
4087/4087, leak tally 0; corpus differential 416 byte-identical (348 informative,
68 silent); freestanding-check both stages; embed stack soak 64 KiB; sync gate 38
comparisons + selftest 10/10; EigenMiniSat 6/6. Separately, an 87-program
regression sweep against the PRE-FEATURE binary (27 consumer tests + 60 examples)
found no behavioural change.

Refs #915, #972

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
Three critics, three FAIL verdicts. Two converged independently on the same root
cause, which is the first time this loop has found a MODEL error rather than
another wrong guard.

THE ROOT CAUSE: ONE BIT ANSWERING TWO QUESTIONS

`g_obs_needed` means "is observer recording on?". Both soundness guards used it
as their precondition for "is the recorded history COMPLETE?" — a different
question. Several sites arm recording at RUN time, and every one of those flips
told both guards "the gate is open, nothing at risk", permanently. Executed:

    x is 1.0 ; loop 40x: x is x * 2.0
    warm is vm_run_bytecode of [1, [0,0,0,40], [7]]   # reads NOTHING
    print of (vm_run_bytecode of [1, [83,0,0,40], ["x"]])

One line that touches nothing turned a loud raise into `equilibrium`, rc 0. The
same warm-up disarmed the load_file guard through the TOCTOU route suite check
18 asserts on, and `spawn` + the SHIPPED lib/io.eigs reached it with no
descriptor at all (the multithreaded bail in the eager pass is the same flip).

Fixed with eigs_obs_enable() — the only sanctioned way to arm recording. It sets
the sticky history-gap flag when it fires after execution has begun, so the two
questions stay separate. All sixteen flip sites route through it; obs_exec_started
marks the boundary in main.c. Fixing the model made three separate symptoms
disappear at once, which is what distinguishes a root cause from an instance.

FOUR WALKERS, ONE OF THEM UNGATED AND ALREADY DIVERGENT

chunk_reads_observer / chunk_has_reader_opcode / chunk_reads_host_observer /
chunk_scan_static_loads all walked the same bytecode, and the third kept its OWN
hand-written reader lists that tools/obs_reader_sync_check.sh never read — a
fourth home for a rule that already had three, in the same change that quotes
mechanical-gates §26 at length. It had already diverged: OP_LOOP_STALL_CHECK is
obs:READS and listed in the gated home, absent from every branch of the fourth.
Collapsed to one set (opcode_is_observer_reader), which is what the gate now
extracts.

THE NINTH OBS_BUILTINS NAME

`record_history` sets g_trace_obs_hist — half of what opens the observer channel
— at RUNTIME, and has no opcode form, so its name in the constant pool is its
only fingerprint. Executed: adding `record_history of 1` changed a gated
program's answer while the compile verdict still read `obs-gate: unobserved`.
`record_history of 0` then closes the channel again, so it can flicker. Added,
and two false claims corrected with it: the comment asserting the gate "cannot
flicker mid-loop" (true of obs_needed, false of the pair), and the one asserting
OBS_BUILTINS is "anchored to the sandbox allowlist" (that group is five names,
this is nine, and report_value/trajectory are not builtins at all).

THE DESCRIPTOR READ SEAM IS FILED, NOT HALF-GUARDED (#1027)

A descriptor reading observer state about a host binding assigned before the
call still gets a rest value. Three static guards were tried and each traded one
wrong answer for another: "reads at all" broke 57 bridge assertions; "reads a
NAME operand" missed the slot form; "NAME, or an in-range assigned slot, or the
thread alias" fires on the bridge's own #737 fixture, whose operand bytes are
load-bearing (1,1 == two OP_NULLs, so a drifted operand walk stays synced) and
whose reader is JUMPED OVER. Telling that apart from a real host read needs
reachability analysis over caller-supplied bytecode. That is build-loop's
oscillation signal, so it is filed with three repros and two candidate fixes
rather than tuned into a fourth variant. VR_RAW is also not "slot" — the role
enum spells it "count / kind / line / runtime-guarded slot" — so no generic
operand rule can separate OP_PREDICATE's kind from OP_TRAJECTORY_SLOT's slot.

Removing that raise also restored sandbox_run's documented contract: it is
specified not to throw, and the round-3 guard made it throw.

ALSO FILED: #1026, a PRE-EXISTING sandbox escape. A sandboxed descriptor using
bare OP_PREDICATE reads host observer state, bypassing the sealed-root env
because that opcode consults no env at all. Verified to reproduce identically on
3210ee9~1, so it is not caused by this branch and is not conflated with it.

DOCUMENTATION — the branch was shipping three error messages telling users to
"Re-run with EIGS_OBS_FORCE=1", an env var documented NOWHERE. docs/OBSERVER.md
also still said this optimisation "is not shipped" and called the scan "future".
Corrected, with a "Using the gate" section covering both env vars, what each
raise means and why refusing beats answering, and the #1027 residual.

Plus: the composed-seam check whose absence let the disarm survive three rounds
(every other check exercises ONE guard in isolation); a duplicate `# 21.` in
[99u]; dead code at the sandbox site; and fault B's mutation re-anchored after
the reader-set extraction changed the indentation it matched on — it reported
MISS rather than a false pass, which is the behaviour a mutation harness must have.

MEASURED, EigenMiniSat 4x4 Tseitin, n=5 interleaved, one binary:

    base median 293.80 s   gated median 34.45 s   ->  8.53x

counters identical across all 10 runs. Four measurements this session:
8.54 / 8.55 / 8.51 / 8.53.

GATES: release 4098/4098; ASan+UBSan detect_leaks=1 4087/4087, leak tally 0;
corpus differential 416 byte-identical (348 informative, 68 silent);
freestanding-check both stages; embed stack soak 64 KiB; sync gate 38
comparisons + selftest 10/10; doc examples 84 byte-for-byte; EigenMiniSat 6/6.

Refs #915, #972, #1026, #1027

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
…that could not see it (#915)

Round 5. Two critics, both FAIL, converging independently on the same defect —
and the sharper half of the finding is about the instrument, not the artifact.

THE EAGER PASS WAS WRITING TO THE PROGRAM'S WORLD

obs_gate_resolve_static_loads runs the REAL compiler over a module's source to
decide whether that module reads observer state. compile_node ARMS the trace
history channel as a side effect — trace_arm_history_name/_all,
trace_arm_occurrences_name, g_trace_hist, g_trace_obs_hist — and none of it was
saved or restored. So merely SCANNING a module switched per-assignment history
recording on in the PARENT.

    x is 1.0 / x is 2.0 / x is 3.0
    load_file of "mod.eigs"        -> prev of x == 2
    load_file of ("mod"+".eigs")   -> prev of x == null     (and correct)

The SPELLING of a load path had become semantically load-bearing. Worse, it was
non-monotone in observation: adding an unrelated `report of z` opened the gate,
which skipped the eager pass, which un-armed the name — a program that asked the
observer MORE got LESS history. A `state_at` module armed the WILDCARD, so the
parent's hot loop paid per-assignment recording for a module it had not loaded
(0.36s vs 0.26s; now 0.11 vs 0.12).

The pass already sealed its OTHER output channel — it mutes fd 2, on the stated
principle that "a pre-pass that speaks is a pre-pass that changes the program's
output". It had two channels and only one was sealed. trace_arm_snapshot /
trace_arm_restore now bracket each eager compile, and the header's claim that
"the only thing kept is the side effect on g_obs_needed" is true again. The same
missing-restore applied to first_error and is fixed with it.

EIGS_OBS_FORCE WAS NEVER A FULL BASELINE — AND THAT IS WHY NOTHING CAUGHT IT

The force flag is checked in compile_ast ONE LINE BEFORE the eager pass, and
arming makes that pass not run. So the reference arm of tools/observer_gate_diff.sh
executes a DIFFERENT CODE PATH from the arm under test: it never exercises the
eager pass at all. Four rounds of "416 programs byte-identical" meant the two
arms AGREE, not that the pass is inert — and could not have meant the latter.
The same applies to the three shipped error messages telling users to re-run
with EIGS_OBS_FORCE=1: that reproduces the pre-eager-pass run, not the gated one.

The tool now takes EIGS_GATE_DIFF_BIN so it can be pointed at a real pre-feature
build, and its header records why the force flag alone is insufficient. Run
against a fresh build of 3210ee9~1: 415 programs BYTE-IDENTICAL. That is the
comparison a user actually cares about, and it was structurally unavailable
until now.

MEASURED, NOT JUDGED: the #1027 residual bites nobody

Rather than escalating the descriptor-read residual as a judgement call, it was
measured. Every file in the ecosystem that uses vm_run_bytecode or sandbox_run:

    ouroboros/test/bootstrap.eigs      gate opens (455 units)
    ouroboros/src/codegen.eigs         gate opens (1)
    iLambdaAi/tests/test_validate.eigs gate opens (4)
    iLambdaAi/scripts/eval_graded_v2   gate opens (9)
    iLambdaAi/lib/ouro_codegen.eigs    gate opens (1)
    iLambdaAi/lib/validate.eigs        gate opens (3)

0 of 6 close the gate, so none can meet the precondition — and structurally so:
ouroboros's gate cannot close because `eval of name` in cg_is_builtin and
`record_history of 1` in ouro_run are intrinsic to how it detects builtins and
supports prev/at. A blind critic then confirmed it behaviourally across
23 workloads in 13 repos, 11 of them against a freshly built pre-feature binary.
The severity table is on the issue so the trade is evidenced, not asserted.

FILED, NOT FIXED
  #1026  a sandboxed descriptor reads host observer state through the bare
         OP_PREDICATE, bypassing the sealed-root env. PRE-EXISTING — verified
         to reproduce identically on 3210ee9~1.
  #1027  the descriptor-read residual, with its measured severity.
  #1028  the embed seam force-arms, so EigenOS — whose entire userland routes
         through eigs_eval_string — gets 0% of the 8.5x. The force-arm is
         correct (successive evals accumulate against one global env), so this
         is an unrealized opportunity, not a defect. Also records the ~2x
         compile-startup cost that lands on programs whose gate ultimately opens.

MEASURED, EigenMiniSat 4x4 Tseitin, n=5 interleaved, one binary:

    base median 293.02 s   gated median 34.61 s   ->  8.47x

counters identical across all 10 runs. Five measurements this session:
8.54 / 8.55 / 8.51 / 8.53 / 8.47.

GATES: release 4100/4100 (27 pinned checks); ASan+UBSan detect_leaks=1
4089/4089, leak tally 0; corpus differential 416 byte-identical same-binary AND
415 byte-identical against a true pre-feature build; freestanding-check both
stages; embed stack soak 64 KiB; sync gate 38 comparisons + selftest 10/10;
ecosystem 23 workloads across 13 repos unchanged.

Refs #915, #972, #1026, #1027, #1028

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
…gences (#915)

Round 6. Three critics. The SOUNDNESS axis came back dry for the first time —
three oracles, ~800 targeted runs, a 252-program syntactic sweep, a 417-program
true-baseline corpus, and a reader set independently re-derived from vm.c's case
bodies rather than read off the existing list. It found nothing in the gate.
The other two critics found four defects, two of them in ROUND 5's fixes.

A LITERAL load_file IN DEAD CODE MADE THE COMPILER READ THE FILE — AND HANG

chunk_scan_static_loads recurses into chunk->functions, so a load inside an
UNCALLED function is still scanned. try_resolve_path admits anything access(F_OK)
accepts, including a FIFO, and read_file_util's S_ISREG rejection happens AFTER
fopen. Executed:

    define maybe() as:
        return load_file of "fifo.eigs"     # maybe() is never called
    print of "reached"

hung indefinitely (rc 124 under timeout, nothing printed, dead before
vm_execute); the same program with a computed path ran fine. A 120 MB target
referenced only from an uncalled function took peak RSS from 2.8 MB to 248 MB.
Now stat-then-reject before fopen, with an 8 MiB ceiling: this pass reads on
behalf of code that may never run, so an unbounded speculative read is unbounded
cost for no benefit. A load_file that actually executes is unaffected.

THE ROUND-5 first_error FIX DID NOT WORK

The snapshot was taken AFTER tokenize, which unconditionally zeroes all five
fields at depth 0 — so the "restore" wrote back the zeroes and wiped the
parent's recorded error, and the parse-error path exited without restoring at
all, leaving the MODULE's error installed in the parent. Moved before tokenize,
restored on both exits. Found by ENUMERATING the compile path, not by a test:
the only readers (lint, LSP) disable this pass, so nothing could have caught it.

THE ROUND-5 ORACLE FIX CREATED A LAUNDERING CHANNEL

`capture` recorded nothing about which binary produced it, and `compare` assumed
— never checked — that <base> and <base>2 came from the same build. They decide
which programs are "nondeterministic" and get EXCLUDED. Executed: with the
round-5 trace-arming fix reverted, lib/test_runner.eigs genuinely diverges (667
lines vs 129, byte-identical across 5 runs of each build). Comparing with a
mismatched reference moved it from MISMATCH to `nondet:`, took `compared` from
416 to 415 — far above the 380 floor, so nothing fired — and turned
`RESULT: FAIL on 1 program(s)` into `RESULT: PASS`, exit 0. §45: the bypass is a
SUPERSET of the population, so the count does not move while what it counts is
destroyed.

Worse, the EIGS_GATE_DIFF_BIN recipe added in round 5 OMITTED the `capture
truebase2` step, forcing the operator to improvise the one step whose env var
decides whether divergences are measured at all.

Every capture now writes a .MANIFEST (binary realpath, sha256, git rev), and
`compare` hard-fails unless base and its determinism reference share a hash,
printing all three. Verified: rc=2 on a mismatched reference AND on a missing
manifest. The recipe is corrected.

AND THE DIFFERENTIAL PASSED AGAINST A DO-NOTHING BINARY

`compared: 440 (informative: 0, silent: 440) ... RESULT: PASS`. The
informative/silent split was added in round 4 and only PRINTED. Printing is not
gating (§37). Floored at 330.

Suite checks 26-27 were bare equalities, so a do-nothing binary scored "agree"
on both — the same trap checks 9/16/17/22 in the same section already carry
guards against. Both arms must now carry the pre-#915 answer (`null`).

FILED, NOT FIXED: #1029 — `state_at` returns keys in hash-bucket order and the
bucket derives from the interned name POINTER, so the order changes with ASLR
(7 distinct orders in 8 runs; `setarch -R` pins it to one). That breaks the
EIGS_REPLAY contract: record and replay of the same program diverge 3/3. It is
PRE-EXISTING — the pre-feature binary does it too — and it is a weak heap-layout
disclosure, the class closed in #1007. Denied by name in observer_gate_diff.sh
IN ADVANCE, because the first corpus program to print one would otherwise be
silently reclassified as nondeterministic and dropped from this gate's only
full-corpus comparison (§10).

THE DURABLE ARTIFACT is the enumeration: 22 rows covering every piece of global
or per-state data the compile path can mutate, each marked restored / provably
unobservable / leaks. Two latent entries carried forward rather than fixed and
recorded here so they are not rediscovered: trace_arm_restore shrink-and-frees a
PROCESS-GLOBAL set under a PER-STATE multithreaded guard (unreachable today only
because eigs_eval_string arms before compiling — correct by another file's
accident), and an abort() inside the muted window would die with stderr at
/dev/null, rendering a fatal message as silence.

MEASURED, EigenMiniSat 4x4 Tseitin, n=5 interleaved, one binary:

    base median 293.88 s   gated median 34.65 s   ->  8.48x

counters identical across all 10 runs. Six measurements this session:
8.54 / 8.55 / 8.51 / 8.53 / 8.47 / 8.48.

GATES: release 4100/4100; ASan+UBSan detect_leaks=1 4089/4089, leak tally 0;
corpus differential 416 byte-identical against a TRUE pre-feature build with
provenance verified (base and ref hashes equal, gated distinct); freestanding
both stages; embed stack soak 64 KiB; sync gate 38 comparisons + selftest 10/10.

Refs #915, #972, #1029

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
…rong (#915)

Round 7. Two critics, four defects in the branch and two PRE-EXISTING bugs
surfaced. Every guard that failed this round failed the same way: it was
locally sensible and wrong about what it RANGED OVER.

HEAP-USE-AFTER-FREE VIA ext_http — the "unreachable" entry, reached

Round 6 recorded that trace_arm_restore shrink-and-frees a PROCESS-GLOBAL name
set under `g_vm_multithreaded`, a PER-STATE flag, and judged it unreachable
because embed_concurrent.c arms the gate first. It did not check ext_http.c,
which is a second multi-state configuration: a fresh EigsState per connection on
its own OS thread, `multithreaded` 0 in every one, route payloads compiled
through compile_ast directly. Captured under `make asan-http`, two concurrent
`code` routes with literal loads:

    heap-use-after-free READ in arm_set_has <- trace_arm_history_name
    <- compile_ast <- builtin_load_file <- handle_request <- http_conn_thread
    freed by another connection thread in trace_arm_restore

The fd-2 mute is process-wide for the same reason: ten /ping requests during one
long eager compile produced ZERO stderr lines, and two staggered compiles left
the server's real stderr replaced by /dev/null permanently — every later error,
OOM and sanitizer report discarded.

A per-state flag can never see a sibling state, so the precondition was never
"this state is single-threaded", it is "this PROCESS has one thread". Now guarded
on a mutex-protected process-global attached-thread count.

A FATAL OOM WAS SILENT DEPENDING ON THE SPELLING OF A LOAD PATH

x_oom aborts from inside the muted window. Executed under `ulimit -v`: a 7 MB
module reported "out of memory" when loaded by a COMPUTED path and printed
NOTHING via a literal one, same rc 134. Fatal paths now restore real stderr
first. Nested muting needed a second pass — recording every level meant the
inner unmute cleared the flag and the OUTER unmute returned WITHOUT restoring;
mutual loads recurse, so the suite caught that within one run.

THE informative FLOOR WAS BLIND IN ITS OWN MOTIVATING FAILURE MODE

Round 6 added it after a do-nothing binary scored a full green. It thresholded
BYTES, and every capture ends with an appended `rc=N` line: `rc=0` is 5 bytes
(silent) but `rc=124` is 7 (informative). A binary that only `exit 124` scored
440/440 informative and PASSED — and 124/134/139 are exactly the timeout, OOM
and crash codes the floor's own comment names as its hazard. Now it strips the
rc line and asks whether anything else is there.

And half the round-6 provenance claim was never implemented: `sha_b` was
assigned and echoed, never compared. The arms must now differ in binary or in
gate setting, or the run refuses.

CHECK 24 WAS DECORATION, AND FIXING IT FOUND A PRE-EXISTING HEAP BUG

Its fixture ended with a string-literal load of an observing module, which the
eager pass resolves at the PARENT's compile time — 2 units already `observed`
before line 1, so the descriptor's arming was never load-bearing and both
mechanisms it named could be deleted with the section green. A discriminating
fixture needs a descriptor whose OWN bytecode carries the reader.

Writing that fixture surfaced #1030: a descriptor that writes a local slot
corrupts the HOST env. vm_execute hands a top-level descriptor the host env and
callframe_init makes it the frame's fn_env, so `SET_LOCAL 0` decrefs a live host
binding and the host then reads freed memory. Pre-existing (identical on
3210ee9~1); the release build does not crash, it prints a plausible answer. I
first filed it with the WRONG diagnosis (an undeclared local slot) and corrected
it after bisecting: declaring the slot changes nothing, a read-only descriptor
is clean, the trigger is the write. The corrected fixture puts the reader in a
NESTED function chunk, which gets a fresh call env — ASan-clean, and verified
discriminating (clean `diverging`, mutant `equilibrium`).

Checks 28-29 added for two mechanisms that had NO check at all: the speculative
read's FIFO/size ceiling, and the first_error restore. The third — the memo that
fixed a measured 7x DAG regression — is recorded as covered by
tools/observer_gate_measure.sh rather than faked with a timing assertion that
would be flaky on a loaded 2-core box.

ALSO FILED: #1029, `state_at` returns keys in hash-bucket order derived from the
interned name POINTER, so the order changes with ASLR (7 orders in 8 runs;
`setarch -R` pins it) and record/replay diverge 3/3. Pre-existing. Denied by
name in observer_gate_diff.sh in advance, so the first corpus program to print
one cannot be silently laundered into the nondeterminism exclusion list.

MEASURED, EigenMiniSat 4x4 Tseitin, n=5 interleaved, one binary:

    base median 293.21 s   gated median 34.47 s   ->  8.51x

counters identical across all 10 runs. Seven measurements this session:
8.54 / 8.55 / 8.51 / 8.53 / 8.47 / 8.48 / 8.51.

GATES: release 4102/4102 (29 pinned checks); ASan+UBSan detect_leaks=1
4091/4091, leak tally 0; corpus differential 416 byte-identical against a TRUE
pre-feature build with provenance verified; freestanding both stages; embed
stack soak 64 KiB; sync gate + selftest 10/10.

Refs #915, #972, #1029, #1030

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
…the oracle's force axis (#915)

Round 8 of the build loop. The gate itself was clean; every defect below is in
the machinery built to bound it or to certify it.

RUNTIME

- EIGS_OBS_FORCE was read with a bare getenv(), so `EIGS_OBS_FORCE=0` and
  `EIGS_OBS_FORCE=` forced the gate OPEN — a documented control doing exactly
  what it says not to for anyone who spells "off" the obvious way, while
  EIGS_STRICT and EIGS_VERIFY_SELF both use `s[0] && s[0] != '0'`. Now matches.

- The speculative budget was charged BEFORE the memo check, billing a shared
  module once per reference while reading it once, so a DAG exhausted the
  budget on bytes it never opened — the defect the memo exists to prevent,
  re-committed in the accounting instead of the I/O. Executed: 24 thin parents
  sharing ONE 59 KiB leaf (59 KiB unique) opened the gate; that leaf loaded
  once closed it. The budget now counts bytes READ.

- The budget value sat inside the population it was meant to sit above. All 86
  multi-unit trees across the 13 ecosystem repos: lib/ui 287 KiB, studio 252,
  tidepool 201, minisat 179, nothing over 1 MiB. 256 KiB cut lib/ui by 12% (so
  the largest tree paid a quarter-megabyte of speculative compiling AND lost
  the gate) and cleared studio by under 2%. Raised to 1 MiB — clears the whole
  measured population 3.5x, still stops the pathological case at ~7% of its
  work (60 modules / 14 MB behind an UNCALLED function: 14.2s/61MB unbounded,
  0.51s/38MB now).

ORACLE

- An arm's identity is (sha, force); the provenance guard ranged over sha
  alone. Capturing the determinism reference with the force flag off made two
  arms of the SAME binary disagree on three programs, which the filter then
  excluded as nondeterminism: `compared: 413` (floor 380, nothing fired),
  RESULT: PASS, rc=0 on a build with case OP_REPORT_NAME: deleted. Guard now
  covers both axes for the reference too. §61: repaired on the narrow axis
  before, left open on the other.

- capture_manifest recorded force=${EIGS_OBS_FORCE:-0}, collapsing "unset" and
  "=0" — which behaved OPPOSITELY. A "gated" arm captured with
  EIGS_OBS_FORCE=0 ran the BASELINE and printed a provenance line
  byte-identical to an honest run: `415 programs byte-identical`, rc=0.
  Normalised with the runtime's own rule, which also folds the =2-vs-=1
  residual. Both bypasses verified closed end-to-end against the real fault:
  honest rc=1 / A rc=2 / B rc=1.

GUARDS — [99u] 30 -> 36 checks, each plant-fault verified to fire, and the
three budget checks verified to fire INDEPENDENTLY (each fault flips exactly
its own check):

- 30 budget is live; 31 + control shared modules charged once; 32 the largest
  real module tree still gates closed — this pins the constant to the
  POPULATION rather than to the number, so a stdlib tree growing past it fails
  a check instead of silently costing the win.
- 34 all four EIGS_OBS_FORCE spellings in one composite verdict.
- 35 + control: a WITNESS for the multithreaded precondition. Round 7's whole
  UAF fix could be deleted with the section green (a blind critic scored 30/30
  on the mutant) — a claim, not a guard. Its decision turns out to be
  observable without a sanitizer: a module loaded after spawn compiles while
  multithreaded, so the bail leaves it `observed`. Clean 2, mutant 0, no-spawn
  control 0 in both. Residual stated in-tree: this pins that the bail FIRES,
  not that the race it prevents is absent.
- 29 upgraded from `grep -c fe_mod = 0` to a four-way verdict — an absence
  assertion satisfied by a --lint that printed nothing at all.

Gates: release 4109/4109 (36/36 pinned); ASan detect_leaks=1 4098/4098, tally
0; 416 programs byte-identical vs a rebuilt true pre-feature binary
(3210ee9~1) AND vs the forced arm of this one; sync gate 38 assertions +
selftest; freestanding both stages + mini-libc differential; 64 KiB stack
soak. EigenMiniSat 8.48x (n=5 interleaved, base 292.72s / gated 34.52s,
solver counters identical across all 10 runs).

Double compile filed as #1031 with the population table and cost curve
(~0.6s per MiB speculatively read); the budget bounds it meanwhile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
, #1032)

Found by sweeping every getenv() site in src/ after fixing EIGS_OBS_FORCE,
rather than assuming that defect was isolated. It was not: the very next row
of the same docs/OBSERVER.md table had it — EIGS_OBS_GATE_STATS=0 printed its
stats. Reading the fix would not have found this; enumerating the sites did
(mechanical-gates §15 corollary).

The sweep also produced the counter-example that stops this becoming a blanket
change, and that is worth as much as the fix: EIGS_TRACE=0 is NOT "tracing
off" — it writes the tape to a file literally named `0`. Presence-only is
CORRECT for value-carrying variables; it is wrong only for booleans. My
heuristic classifier over the remaining sites also called
EIGS_JIT_DUMP_PREFIX a boolean, which the name alone refutes. So the tree-wide
split is filed as #1032 with the candidates marked explicitly as a worklist to
READ, not a defect list.

- check 36 pins all four spellings of the stats flag; [99u] 36 -> 37.
- docs/OBSERVER.md states the convention once, instead of leaving it implicit
  in two adjacent rows that disagreed.

Gates on the final tree: release 4110/4110 (37/37 pinned); ASan
detect_leaks=1 4099/4099, tally 0; 416 byte-identical vs a rebuilt true
pre-feature binary AND vs this one's forced arm; sync gate 38 assertions +
selftest; freestanding both stages + mini-libc differential; 64 KiB stack
soak. EigenMiniSat 8.50x (base 293.68s / gated 34.55s, n=5 interleaved).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
Round 9 of the build loop. A blind critic ran the round-8 guards through a
mutation train and they all held — six checks, six mutants, exactly one red
check each, no entanglement — then built an oracle the bar did not contain and
found this.

THE DEFECT

obs_memo_seen() keyed on the resolved path STRING, and resolve_eigenscript_file
never canonicalizes (try_resolve_path is access(2) plus a copy). So one file
written N ways got N memo entries: read, compiled, and CHARGED to the
speculative budget N times. Executed on one 55 KiB module written four NATURAL
ways — relative, absolute, through a symlink, ./-prefixed: 4 speculative opens
where the oracle is 1. With enough distinct spellings the budget is spent on
referenced rather than unique bytes and the gate flips open: the critic
measured the flip at N=18 spellings of a 59,380-byte leaf, my reproduction at
N=24 of a 55,180-byte one — the difference is exactly the leaf size against the
same 1 MiB line, so the two independent reproductions confirm the mechanism.

This is round 8's double-charge defect re-entering through the KEY instead of
the ORDERING, one commit later — and check 31 could not see it, because its
diamond fixture writes the identical literal in every parent.

THE FIX

Key on (st_dev, st_ino). It beats realpath(): it is the true identity, it also
folds hard links, it needs no PATH_MAX buffer, and it costs no extra syscall —
the budget guard already stats the file. The stat now runs BEFORE the memo is
consulted (cheap, charges nothing, yields the key) and the charge AFTER it.
Verified: 1 speculative open and a closed gate on all three arms (4 natural
spellings, 24 identical, 24 distinct), matching the oracle exactly.

My own reproduction produced two bugs before it produced a verdict — a
`grep -q observed` that also matches `unobserved` (the negative verdict
contains the positive as a substring) briefly made the CONTROL look broken, and
a fixture that cycled 4 spellings through a modulo while claiming 24. Either
alone would have justified "critic was wrong". Both are now recorded in
mechanical-gates (§76, §77) and build-loop.

THE GUARD

Check 37, plant-fault verified (reverting the key to a spelling hash flips ONLY
check 37; 30/31/32 untouched): 24 spellings of ONE file must gate closed. It is
the exact COMPLEMENT of check 30 — same statement count, same referenced bytes,
opposite verdict; 30 is N distinct FILES (must open), 37 is N spellings of ONE
file (must close). The pair separates identity from arithmetic; either alone
tests neither. [99u] 37 -> 38 pinned checks.

Also corrects the docs/OBSERVER.md sentence the critic falsified ("a module the
pass has already read is free to reference again" — it was once per spelling).

Gates: release 4111/4111 (38/38 pinned); ASan detect_leaks=1 4100/4100, tally
0; 416 byte-identical vs the rebuilt true pre-feature binary AND vs this
binary's forced arm; sync gate 38 assertions + selftest; freestanding both
stages + mini-libc differential; 64 KiB stack soak. EigenMiniSat 8.47x (base
293.54s / gated 34.66s, n=5 interleaved, counters identical).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
…silence (#915, #1033)

Round 10 of the build loop. The critic's verdict on the FEATURE was PASS — the
first round where the artifact held everything executed against it: inode reuse
(executed, reuse hit on try 1, behaviorally neutral), every staleness/deletion
divergence on a loud conservative path, the round-9 plant-fault claims
independently reproduced, hard-link folding and 1-speculative-open verified by
strace. The finding was in the GUARDS.

THE HOLE (mechanical-gates SS18: a crash rendered as silence)

The four closed-expectation checks — 31, 31C, 32, and round 9's new 37 — were
written as `grep -q observed || echo closed`, stdout discarded, rc never read.
So any silent death scored "closed". Executed: a planted abort() at 20 memo
entries SIGABRTs (rc=134, core dumped, nothing printed) on check 31's own
fixture, and the section ran 39/39 GREEN. The blind spot spanned BOTH lanes:
an ASan report contains no "observed" line either, so the sanitizer lane
scored it "closed" too. And memo populations >=17 entries exist ONLY in these
fixtures — a crash-at-scale bug in the memo cluster was invisible to the
entire bar.

THE FIX

obs_gate_closed_verdict(): "closed" now requires rc=0 AND the program's own
stdout marker AND a positive `obs-gate: unobserved` line; an observed line is
"open"; everything else is its own loud verdict (died-rcN / no-output /
no-evidence) that fails the comparison with the reason in the string — the
positive-evidence pattern checks 2 and 29 already used. Plant-fault verified
both directions: clean -> closed, mutant M8 -> died-rc134.

ALSO, from the same review:

- The "costs no extra syscall" claim in the round-9 comment was FALSE on the
  memo-hit path (measured: 48 newfstatat vs 1 on the 24-reference diamond —
  the string key stat'd only on misses). Corrected to the true statement: one
  stat per reference is the deliberate price of identity keying; the stat
  yields the key, costs microseconds against a read+compile, and a stat
  failure on a later reference now takes the conservative path, which is the
  direction we want.
- The st_dev half of the memo key is untested on a single-filesystem box
  (dropping the dev comparison survives the section). Failure direction is
  conservative-only, so noted as a residual in check 37's comment rather than
  fixtured with a bind mount.
- The memo's linear scan is superlinear on many-tiny-module trees (36k
  modules: 3.6s stall, budget-bounded, once per thread; no real tree within
  three orders of magnitude) — filed as #1033 with the numbers.

Runtime code untouched this round (comments only; the binary's behaviour is
identical). Gates: release suite 4111/4111, 38/38 pinned checks green under
the new verdict helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
obs_gate_closed_verdict became the sole judge for four checks last commit;
gutted to `echo closed` it would pass all four, and nothing above catches a
gutted helper — the count pin sees deleted checks, not blind ones (§64: a
checker never shown to fail is decoration). Two planted inputs it must refuse
to call closed, now in-suite: a program that dies (raises, rc nonzero) reads
died-*, and an exit-0 program that never prints its marker reads no-output —
the compiler finishing is not the program running. [99u] 38 -> 40 pinned.

Suite: 4113/4113, both controls green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
…s labels to returns (#915)

Round 11 of the build loop — a full-branch fresh-eyes critic. THE finding:
OP_IMPORT's reader-set membership had NO witness anywhere in the bar. A
one-line DEMOTION — `case OP_IMPORT:` moved from the return-1 group to the
return-0 group — was silent-wrong on a five-line program (forced arm
`diverging`, mutant `equilibrium`, rc=0: the exact #861 inversion the gate's
header forbids) and passed EVERY oracle:

- [99u]: no check exercised import gating.
- The 416-program differential: no corpus program interrogates a binding
  assigned before its first import — the only shape that diverges.
- The sync gate: its walker COLLECTED `case OP_*:` labels without binding
  them to their return group, so a deletion moved the count 17->16 and
  failed while a demotion kept 17 and passed BYTE-IDENTICALLY to a healthy
  run. Reproduced here before fixing.

And the seam is exactly where the code says the next change lands ("import
stays conservative; #915's import half is still open") — nothing held that
line.

FIXES, each verified by execution:

- The walker now BINDS labels to their return group: labels accumulate as
  pending and only a `return 1` in the same fall-through run emits them;
  `return 0`/`default:` drops them. Demotion now fails rc=1 with two loud
  FAILs; healthy unchanged at 17.
- Selftest fault G — the round-11 fault verbatim (a reader demoted to the
  return-0 group), planted so it MISSes loudly if the switch's tail shape
  moves. 11/11 rows; the suite's selftest floor raised 10 -> 11 so deleting
  the row regresses.
- [99u] check 40, the behavioral witness: a host assigns, then imports a
  module that `report`s the pre-import binding; the answer must be
  `diverging`. Whoever narrows import's rule must now arrive with machinery
  that keeps that answer right. (Its first suite run failed on its OWN
  harness — a relative $EIGS_BIN under cd, a recorded trap that bit again —
  which doubles as proof the check can go red. Absolutized.)
- No CASE(IMPORT) runtime backstop, deliberately: with OP_IMPORT in the
  reader set the 0->1 flip is unreachable today, and an unwitnessable guard
  is decoration by this repo's own rules. The check and the sync gate hold
  the line instead.

THE THREE SIBLINGS (same review, all executed):

- Check 41: check 30's fixture population gets its own closed-proof control —
  the gate opened on a PARSE ERROR in a rotted fixture exactly as on genuine
  budget exhaustion, so garbage awk modules kept check 30 green while testing
  nothing. One module from the same population alone must PROVE closed.
- observer_gate_measure.sh reads both arms' exit codes and requires exactly
  one DONE line: a stub whose arm printed DONE then SIGSEGV'd measured
  "RESULT: valid" (the tool never read rc, and head -1 took the first DONE of
  several). Now: crash -> VOID rc=139, two DONEs -> VOID, healthy -> valid.
- observer_gate_diff.sh: missing-in-ONE-arm was `continue`d identically to
  missing-everywhere, so 30 deleted gated captures still printed
  "compared: 410 ... PASS" above the 380 floor. Now a hard rc=2 FAIL naming
  the one-sided programs; absent-from-all-arms stays a skip.

Two falsified comments corrected: vm.h named a chunk_reads_host_observer
function that exists nowhere (now describes what the descriptor sites do, and
points at #1027); chunk.c claimed a dict-key mention of load_file makes the
unit opaque (executed: it does not — VR_NAME role is the population key, and
the comment now says so with the counter-example).

[99u] 40 -> 42 pinned checks. Suite: 4115/4115.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
…e-site meta-gate (#915)

Round 12 of the build loop. The critic's runtime surface came back fully
clean — OBS_BUILTINS audited against all 252 builtins plus extensions (no
unlisted observer reader; the only indirection routes are eval/listed,
literal load_file/eager-compiled, computed load_file/opaque, import/reader
opcode), load_file shadowing conservative in all three shapes, temporal
parity identical across four arms, fixture generators verified against their
comments, docs re-falsified and clean. The finding was in round 11's own
commit: check 40 was rc-blind — `head -1` verdict, rc never read — so a
runtime that prints `diverging` and then crashes scored PASS. Executed with a
stub (printed the answer, kill -SEGV, actual rc=139: check PASSed).

That is the THIRD consecutive round for this class (round 10:
closed-verdicts; round 11: measure.sh DONE-then-SIGSEGV; round 12: check 40,
written in the same commit as the round-11 fix). Prose — including the
standing rc_ok rule in .claude/rules/test-suite.md — demonstrably did not
stop it. So this commit does two things:

SWEEP THE CLASS, NOT THE INSTANCE. All 13 success-answer captures in [99u]
now go through rc discipline: 11 via the new obs_gate_answer helper (rc != 0
returns died-rcN, which fails any expected-answer comparison loudly), 2
inline where env/cd is needed (checks 18, 40). Plant-fault verified:
crash-after-answer stub reads died-rc139; healthy answers unchanged.
Raise-EXPECTING checks stay on their own capture — a raise exits nonzero by
design, and a crash there already produces different text.

WRITE-SITE META-GATE (hooks-beat-advice: thrice-bitten means mechanise).
Check 42m greps THIS FILE's [99u] region for bare `$EIGS_BIN ... | head/tail
-1)` captures; the four surviving ones are waived BY COUNT with their
reasons (1x G19 raise-expecting; 3x check-34 stderr-stat captures that fail
closed on absence), so a new unrouted capture — or a silently vanished
waived one — both go red. Plant-fault verified: one planted bare capture
moves the count to 5. Writing this gate itself caught two of my own bugs
before they shipped: both of my first range terminators ([99v],
OBS_GATE_TOTAL_AFTER) were PHANTOMS — sed with a missing end pattern runs to
EOF and the count was right by luck. The range now ends at ^OBS_GATE_RAN=,
which exists.

Also from the same review: the dead `index($0, "^}")` line removed from the
sync walker (awk index is literal; the /^}/ rule below it is the real
terminator), selftest still 11/11.

[99u] 42 -> 43 pinned checks. Suite: 4116/4116.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
…915)

Round 13 of the build loop. Two plant-verified findings, both apparatus; the
runtime surface held everything, including the surfaces never before probed
(`unobserved:` composition — 8 divergence probes, all identical; multi-state
memo/budget contamination — unreachable by construction, both embed evals
force the gate; SIGINT-mid-mute — no handler exists, the process dies).

1. THE META-GATE COULD NOT CATCH THE BUG THAT MOTIVATED IT. Check 42m's
   regex anchored on `$EIGS_BIN` — and round 12's actual defect was spelled
   `"$OBS_EIGS_ABS"`. Planted verbatim: count stayed 4, gate green. Not an
   active-dodger evasion — the natural next accidental spelling (copying
   check 40's cd scaffolding, a shellcheck-quoted "$EIGS_BIN", `head -n1`)
   evaded identically. Re-anchored on the CAPTURE SHAPE — merged-stderr
   output piped straight into a first/last-line pick — which is what makes a
   capture rc-blind regardless of what the binary variable is called. Same
   4 waivers at HEAD; all three respellings now go red (plant-verified).

2. eigs_obs_unmute_for_fatal HAD ZERO WITNESSES. With the unmute deleted
   from x_oom, an OOM inside the muted window died rc=134 with nothing on
   stderr but timeout(1)'s own core-dump line, and no check went red — a
   crash rendered as silence, in the exact mechanism built to prevent one.
   Check 43: ~518 KB fixture (under the 1 MiB budget, so the eager pass DOES
   read it; the first draft overshot by 9 KB and would have tested the
   unmuted real-load window instead) behind a literal load under
   ulimit -v 60000. Asserts the `out of memory` MESSAGE, not stderr
   non-emptiness — the mutant's stderr is not empty, it is just all
   timeout(1). SKIPs on sanitizer builds with the reason in the check:
   ASan's allocator abort lands inside the window on BOTH arms.

3. measure.sh: counters_are_substantive now requires a DIGIT after each
   field — presence alone scored `conflicts= propagations=` (empty values)
   as substantive, and two arms both emitting empty fields compared
   "identical". Stub-verified: empty values -> VOID, healthy -> valid.

[99u] 43 -> 44 pinned checks. Suite: 4117/4117.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
…s a platform probe (#915)

Round 14 of the build loop. The critic hunted the iLambdaAi lesson — a
converged loop's blind spot is the entry-point class nobody ran — and found
it in the PLATFORM dimension: thirteen all-Linux rounds, and [99u] could not
run on the four macOS CI legs, two of which are the release workflow.
Executed simulation (timeout shimmed to rc=127 over the extracted section):
TOTAL=45 PASS=23 FAIL=22. Loud, not silent — round 12's rc-discipline turned
every death into died-rc127 — but release-blocking, and invisible locally
because the branch has never been pushed: no CI has ever seen this section
(§46: the failure population lives on the machines you did not run).

TWO DEFECTS, TWO FIXES:

- Eight bare `timeout N` sites (plus the two helpers' `timeout "${3:-60}"`)
  bypassed the $EIGS_TMO convention the suite header defines PRECISELY
  because macOS lacks timeout(1). All ten now route through obs_tmo, a
  section-local runner resolved once from the same detection (§32):
  timeout, else gtimeout, else run unbounded — verified under a stripped
  PATH containing neither. My first span-rewrite caught only 7 of 10: the
  span terminator matched the meta-check's own sed PATTERN, truncating
  early — the phantom-terminator trap a third time, caught this time by
  grepping the remainder instead of trusting the count.

- Check 43's ulimit -v (RLIMIT_AS) is not enforced on macOS, so the OOM
  witness ran clean there and scored a permanent red — which trains people
  to ignore the section (§13). A 128 MB string-doubling probe under the
  rlimit now decides per-platform: dies (Linux, rc=134) -> the real arm
  runs, and ran-clean stays a LOUD fail because the rlimit provably bites;
  runs clean (macOS) -> a visible SKIP naming the reason, mirroring the
  sanitizer arm. Probe verified both directions on this box (rc=134 under
  the limit, rc=0 without).

Also from the same review: the meta-gate's stderr alternation widened to
(2>&1|2>/dev/null) — `2>/dev/null | head -1` is equally rc-blind, MORE
natural, and evaded the old regex — with the §45 residual stated exactly
(sed -n 1p / awk NR==1 / |& respellings remain out of scope; the gate
targets the accidental spellings that have occurred, review owns dodgers).
Same 4 waivers at HEAD.

Runtime surface: fully clean this round too — REPL/stdin parity, --fmt,
--api, --bundle roundtrip, trace record and replay, error-path parity, and
the asan-build [99u] extraction all held; NOTHING in the branch diff is
unnamed across 14 rounds.

Suite: 4117/4117 (44 pinned). NOT counted as a clean round — the finding was
new and actionable. The exit criterion now explicitly includes the real CI
matrix: this branch goes to CI before the loop can declare itself dry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
Comment thread src/compiler.c Dismissed
Comment thread src/state.c Fixed
InauguralPhysicist and others added 4 commits August 23, 2026 09:11
…e CI matrix answers (#915)

Round 15 of the build loop, plus the first run of the REAL exit gate — the CI
matrix on PR #1034, which no local round could stand in for. Five failures,
four distinct causes, each verified fixed by its own oracle.

ROUND 15's FINDING (blind critic, BSD-sed simulation): the sync tool's
selftest had 12 GNU-only suffix-less `sed -i` sites — round 14's class one
layer down. BSD sed parses `-i 'script' file` as -i <ext> <script=path>,
errors, leaves the file UNTOUCHED: faults never plant, the selftest exits 1
(6 passed, 5 failed), [99u] check 25 reads `0 rows, floor 11`, and all four
macOS CI legs fail. The critic also caught the deeper defect: an INERT edit
made the WITNESS rows pass vacuously (unplanted fault + ungutted gate both
exit 0 — the expected value). One helper fixes both: sedi() writes-to-temp
+ mv (portable both ways; `sed -i ''` is GNU-broken in the other direction)
and CMP-VERIFIES the edit changed the file, so an inert mutation surfaces as
a loud MISS on its own row on every platform. The B row moved to python —
its replacement embeds \n, which BSD sed rejects even with -i fixed.
Verified: BSD-shim selftest 11/11 + gate PASS; a deliberately-inert plant
now draws 4 loud MISSes.

THE MATRIX'S OTHER THREE (none reachable by any local lane):

- TSAN: g_obs_exec_started was stored UNCONDITIONALLY at the top of
  vm_execute_common (this branch's own round-2 line) — a write-write race
  the moment workers exist, flagged in six programs at vm.c:6921, while the
  local release suite was 4117/4117 green. Fixed with the #297 write-once
  pattern: workers are created BY an executing VM, so the creating thread's
  store happens-before every worker (pthread_create); guarded, workers read
  1 and never store, removing ALL concurrent writes. Discriminated locally
  on a TSan build: unguarded 1 warning (matching CI), guarded 0, across the
  five failing programs.

- EXTENSIONS: the #885 planted-fault CONTROL ("a shared global DOES
  cross-talk") failed while all four isolation rows passed. The control is
  byte-identical on main — the flake is its own: no start barrier, so
  thread A can run ALL rounds before B exists; zero overlap reads as "the
  harness does not race" and fails a healthy harness. pthread_barrier
  before the first round. Verified: cross-talk A=175 B=175 / 200 rounds,
  30x soak 0 failures.

- CODEQL: 1 high + 1 note, both in this PR's changed code, exactly per the
  recorded "~4s CodeQL fail = real new alert" rule. The high
  (cpp/path-injection on the eager pass's read_file_util) is the repo's
  long-standing FP class verbatim — #196/#175: load_file resolving a
  script-supplied path IS the builtin's contract, and the eager pass opens
  the same population through the same resolver with tighter bounds —
  dismissed per-alert with precedent-matching reasoning. The note
  (function-in-block, state.c) fixed properly: the block-scoped extern
  moved to file scope.

Suite: 4117/4117 (44 pinned) on the fixed tree.

The TSan finding is the exit-criterion widening earning its keep: a real
race in this branch's own runtime change, invisible to fifteen rounds of
local critique because that lane exists only in CI — the converged verdict
is bounded by the lanes the critics can run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
…nstance (#915, #1035)

Round 16 of the build loop. The critic's question was whether the previous
commit's write-once guard closed the TSan class. Demonstrated answer: NO —
the same plain read-then-write survived ONE FIELD OVER, in eigs_obs_enable()
on obs_needed/obs_history_gap, reachable from pure EigenScript on a worker
thread via sandbox_run (deliberately NOT in OBS_BUILTINS, so the gate stays
closed until the worker's own call performs the 0->1 store). Reproduced 3/3
on a TSan build: T1 write in eigs_obs_enable vs T2 read in
eigs_obs_gate_open, same heap block, allocated in eigs_state_new.

Unlike obs_exec_started, NO happens-before argument exists here: two workers
calling sandbox_run concurrently both legitimately read 0 with no create
edge between them. The #297 write-once pattern cannot apply.

FIX: the three flags (obs_needed, obs_history_gap, obs_exec_started) are
accessed through relaxed __atomic_load_n/__atomic_store_n — the tree's own
atomics idiom. The g_obs_* macros are now LOADS, not lvalues, so any future
assignment through them FAILS TO COMPILE and must use obs_flag_store: the
write sites stay enumerable by construction rather than by grep. Relaxed
suffices — no data is published through these flags; each consumer's
correctness rests on its own thread's sequenced reads plus the sticky
obs_history_gap semantics, and a bounded stale-0 window is the documented
conservative-late behaviour. A relaxed load is a plain MOV on x86; measured:
8.53x on the full n=5 interleaved EigenMiniSat run (base 292.81s / gated
34.33s, counters identical across all 10 runs) — the best number of the
8.47-8.53 series, i.e. zero cost.

The OBS_BUILTINS alternative was REJECTED deliberately: adding
sandbox_run/vm_run_bytecode to the name list would cost every program
naming them its gate (a semantic + perf change), would break check 24's
discriminating premise, and still would not cover a C embedder running an
assembled chunk with no name in any pool. Atomics close every path at zero
semantic change.

Verified: the critic's repro 12+ consecutive clean runs (was 3/3 flagged);
the five CI spawn programs clean; the one intermittent residual identified
by stack as the PRE-EXISTING sandbox_run lock-free env walk (unchanged on
main) — filed as #1035 with the fix direction and a lane-witness note, not
folded in here.

ALSO: ci.yml sanitizers timeout 30 -> 45. The 59e294c matrix run — 17/18
with BOTH macOS legs green on their first-ever [99u] execution — lost its
asan job to the 30-minute ceiling at 30:02 (previous green: 19m57; this
branch's section launches the sanitized binary ~60 times and startup
dominates). A timeout rendered as a failure is a recorded gotcha, not a
verdict; the same tree's local ASan suite is 4117/4117.

Suite: 4117/4117 (44 pinned). Round 16 is NOT clean (real runtime race
found and fixed); dry counter stays 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
…ng pair (#915, #1035)

Round 17 of the build loop. The critic asked the round-16 question of the
round-16 fix — "does the same shape survive one field over?" — and it did,
for the THIRD time: g_trace_hist / g_trace_obs_hist are plain process
globals stored from a worker (sandbox_run -> chunk_arm_temporal) while every
other thread reads them at per-assignment safepoints, and g_trace_obs_hist
is the SECOND OPERAND of eigs_obs_gate_open — the very expression whose
first operand the previous commit made atomic. TSan: 2 warnings, 3/3, on an
18-line repro. The critic also ran the repro on MAIN and got the same 2
warnings — the instance is pre-existing; what is the branch's is (a) making
the flag verdict-carrying and (b) a comment claiming "the race class, not
the instance" was closed. Falsified within one round of being written; the
comment now says so, verbatim, as a warning to the next author.

FIXES:

- Both trace flags use the same idiom as the obs flags: storage renamed,
  the old names are relaxed-atomic LOAD macros, writes fail to compile
  unless routed through trace_flag_store (14 sites converted). The compiler
  then found what grep had not: the JIT bakes the FLAG'S ADDRESS into
  emitted code (emit_movabs_rax(&g_trace_hist)) — not an lvalue once the
  name is a load macro. That site now takes &g_trace_hist_storage
  explicitly, with the note that an emitted plain load is ISA-identical to
  a relaxed load on x86 and sanitizer-blind either way (the recorded JIT
  rule). Verified: round-17 repro 3/3 -> 0/3; round-16 repro clean of flag
  races (its residual intermittent is #1035's documented env-walk pair,
  identified by stack).

- The relaxed-order hole (round 17's second finding): enable stores
  gap-then-needed, the load_file guard reads needed-then-gap; both relaxed,
  ARM may show needed==1/gap==0 — a silence-that-should-RAISE,
  conservative-EARLY, contradicting the documented contract. Window is one
  module compile wide (practically unobservable); fixed anyway because the
  sound version is free: obs_flag_store is now RELEASE (cold stores; plain
  MOV on x86, stlr on ARM), the guard's paired read is ACQUIRE, hot
  safepoint loads stay relaxed with the pairing argument written down.

- #1035 widened: the arm NAME SETS (g_arm_*, g_occ_*) are the same worker-
  reachable surface and are NOT fixed by flag atomics; lane-witness note
  added there.

Suite: 4117/4117 (44 pinned). Freestanding both stages OK (relaxed int
__atomic builtins emit no libcalls). Round 17 is NOT clean — dry counter
stays 0; the terminator has not started.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
@InauguralPhysicist
InauguralPhysicist merged commit b3bd698 into main Aug 23, 2026
18 checks passed
@InauguralPhysicist
InauguralPhysicist deleted the perf/915-observer-elision branch August 23, 2026 19:02
InauguralPhysicist added a commit that referenced this pull request Aug 23, 2026
Bought across #1034 rounds 16-18: three adjacent field groups, three rounds,
one race shape — closed by one induction pass. New safepoint-read flags use
the load-macro idiom from the start.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants