findings(E-5,E-8,E-9): module-identity ngspice events + runWaitCompletion admission gate

E-8 (re-implemented for JSPI — the codex gate is entangled with the dropped
execution owner; under JSPI a fresh non-suspending JS→wasm entry while
another activation is suspended is structurally safe on its own stack, so
the admission boundary for worker completions is liveness + trap state, not
execution ownership):
- jspi-scheduler.js grows `terminal` (trapped instance; distinct from `dead`),
  canTouchNative(), _terminalizeNativeTrap() (WebAssembly.RuntimeError +
  cross-realm string classification), and runWaitCompletion(site, token,
  prepare, inertResult): prepare runs immediately and owns ALL native work;
  stale tokens and dead/terminal instances drop loudly without resolving
  (resolving would resume the parked frame inside the damaged module); a
  trap latches terminal; a plain JS bug resolves inertResult so the wait
  fails instead of stranding. beginWait refuses (token 0) when dead/terminal.
- all four delayed completion sites route their native work through the
  gate: 'OCC export completion' (exporter_step_stub), 'OCC model completion'
  (oce_plugin_stub — the MEMFS cache write moves inside the gate too),
  'ngspice request completion' and 'ngspice vector completion'
  (sharedspice_client — every HEAP32/HEAPF64/malloc write inside prepare,
  inertResult 1 = transport error). Every wxWasmBeginWait caller in the
  stubs bails on token <= 0.
- deliberately NOT ported from codex: ownerModule, enqueueNativeCompletion,
  executionBarrier, the byte-credit native-entry FIFO — completions are
  one-shot per wait token and stream volume is bounded at the E-6 transport
  credit window. Cross-refs logged for group M (M-2/M-6/M-8).

E-5 (re-implemented; codex shape kept, owner APIs replaced with the E-8
gate): js_ngspice_install_events binds the handler to the EXACT installing
module (handler.__pcbjamNgspiceOwnerModule stamp; presence is not identity),
re-installation replaces a foreign module's handler, a superseded handler
disarms itself, native entry goes through installingModule._malloc/
._pcbjam_ngspice_event (never lexical Module), each dispatch checks
canTouchNative() (loud drop on a dead/terminal module), and a trap on the
per-line entry latches the terminal gate.

Tests: scheduler-shim.test.ts +7 (gate happy/stale/dead/terminal/cross-realm/
js-bug/beginWait-refusal). e2e specs updated from the codex line: occ-export
decode-fault recovery (real onmessageerror transition via failDecode, J-4),
ngspice-probe direct-service coverage, eeschema-sim rewritten onto the E-7
applied-generation receipt (codex's executionBarrier await replaced with a
pendingWaits('ngspice') drain poll — the JSPI-line equivalent).

Also bumps the kicad submodule to the E-7/E-9 commit (dd5751038f7).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Istvan Matejcsok 2026-08-19 16:03:16 +02:00
commit c14e76651c
9 changed files with 763 additions and 149 deletions

View file

@ -202,6 +202,13 @@
earlyWaitResolves: 0,
beginWait: function (kind) {
if (this.dead || this.terminal) {
// Refuse to mint a wait an unhealthy instance can never satisfy.
// Callers treat token 0 as "not started" (the C++ bridges bail);
// a stray waitPromise(0) settles immediately and warns.
this._note("beginWaitRefused", kind, 0);
return 0;
}
var token = ++this.waitSeq;
var entry = { kind: kind, resolved: false, resolve: null, promise: null };
entry.promise = new Promise(function (resolve) { entry.resolve = resolve; });
@ -274,6 +281,76 @@
},
dead: false,
// --- E-8: admission gate for delayed worker/MEMFS completions -----------
// `terminal` means the wasm instance TRAPPED (WebAssembly.RuntimeError or
// its cross-realm string equivalent): the heap may be mid-mutation, so no
// further native work (malloc / heap stores / FS writes) may run and no
// parked frame may be resumed into it. Distinct from `dead` (orderly
// shutdown). One-way.
terminal: false,
canTouchNative: function () { return !this.dead && !this.terminal; },
_terminalizeNativeTrap: function (site, e) {
var isTrap = (typeof WebAssembly !== "undefined"
&& WebAssembly.RuntimeError
&& e instanceof WebAssembly.RuntimeError)
|| /unreachable|memory access out of bounds|index out of bounds|null function or function signature mismatch|Aborted\(/i
.test(String((e && e.message) || e));
if (!isTrap) return false;
if (!this.terminal) {
this.terminal = true;
this._note("terminal", site, 0);
console.error("[wx-scheduler] native trap in " + site
+ " — instance is terminal; all further native completions are inert: "
+ e);
}
return true;
},
// The one admission boundary for delayed completions that both touch
// native state and wake a parked waiter (the four worker/MEMFS completion
// sites: OCC export, OCC model, ngspice request, ngspice vector).
// `prepare` runs IMMEDIATELY, never queued — it owns the parked waiter's
// output pointers, and queuing it behind anything can deadlock the very
// frame this completion wakes. Disposition (every drop is loud, never
// silent):
// stale/unknown token -> drop + warn (late frame from a retired
// worker generation)
// dead or terminal instance -> drop + warn, DO NOT resolve — resolving
// resumes the suspended frame INSIDE the
// damaged module
// prepare() traps -> latch terminal, DO NOT resolve
// prepare() throws plain JS -> resolve inertResult (fail the wait
// rather than strand its parked frame in
// a healthy instance)
runWaitCompletion: function (site, token, prepare, inertResult) {
var entry = this.waits.get(token);
if (!entry || entry.resolved) {
console.warn("[wx-scheduler] " + site + ": completion for stale wait "
+ token + " dropped");
this._note("staleCompletion", site, token);
return false;
}
if (!this.canTouchNative()) {
console.warn("[wx-scheduler] " + site + ": completion dropped ("
+ (this.terminal ? "terminal" : "dead") + " instance)");
this._note("inertCompletion", site, token);
return false;
}
var result;
try {
result = prepare();
} catch (e) {
if (this._terminalizeNativeTrap(site, e)) {
this._note("completionTrap", site, token);
return false;
}
console.error("[wx-scheduler] " + site + ": completion failed: " + e);
this._note("completionError", site, token);
this.resolveWait(token, inertResult == null ? 0 : inertResult | 0);
return false;
}
this.resolveWait(token, result | 0);
return true;
},
shutdown: function (why) {
this.dead = true;
// S6 teardown contract: queued-but-