Observer gate: compile-time elision of observer bookkeeping (#915) - #1034
Merged
Conversation
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
…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
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
This was referenced Aug 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The #915 observer gate: a compile-time liveness scan (
obs_neededperEigsState) that lets a program provably never reading observer state skip entropy bookkeeping entirely, plus the eager literal-load_filecompile 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.
[99u]: 44 pinned checks, each plant-fault verified; rc-discipline enforced by a write-site meta-gate.(sha, force)both axes.detect_leaks=1clean, tally 0; freestanding both stages; 64 KiB stack soak; tape/replay byte-identity; JIT differential.Deliberately conservative
importstays a reader (the Observer entropy is computed on EVERY assignment regardless of use: 88% of runtime / 8.50x ceiling on a consumer that uses no observer features #915 import half stays open) — check 40 + sync-gate fault G hold that line.EIGS_OBS_FORCE=1restores pre-gate behaviour exactly;EIGS_OBS_GATE_STATS=1shows per-unit verdicts.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