Commit graph

679 commits

Author SHA1 Message Date
Gergő Törcsvári
c0ed5d59a7
chore: drop orphaned binaryen gitlink
The jspi migration removed binaryen from .gitmodules (no asyncify
post-link pass) but left the gitlink behind, so every submodule
command errors with 'no submodule mapping found for binaryen'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmR6goSk7JC17h7fkgGvHG
2026-08-14 17:57:36 +02:00
Gergő Törcsvári
d10b196e47
chore: bump pcbjam-shared to 218fddb — source sequencing + PROJECT_FILE_REVISION_HEADER
The private repo's content-addressed files route imports
PROJECT_FILE_REVISION_HEADER from @pcbjam/contract, which re-exports
@pcbjam/shared; the previous pin (22217c9) predates that export by one
commit and breaks the closed server's watch build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmR6goSk7JC17h7fkgGvHG
2026-08-14 17:57:09 +02:00
Viktor Vaczi
e652810de7 bench: jetson FPS battery — both backends saturate at 80.9 MB scale
Parameterizes the throttle-sweep FPS battery (fpsBattery helper) and adds a
jetson-agx-thor run: asyncify raf 2.1-5.3 / distinct 0.2-0.8 vs JSPI raf
1.9-5.0 / distinct 0.1-0.7 — indistinguishable. Suspension overhead is a
per-event-loop-turn cost; at hundreds of ms of GAL work per frame it stops
discriminating. vme-wren (~27 MB) remains the discriminating size class.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 12:07:18 +02:00
Viktor Vaczi
da299ed6f9 bench: JSPI vs asyncify A/B — harness + results
Adds pcbnew-large-perf.spec.ts (PERF_LARGE-gated: repeated cold loads,
vme-wren/jetson opens, rAF + distinct-glcanvas-frame FPS under throttle,
wasm/JS heap checkpoints), fetchIntoMemfs + openAndWait/sampleMemory/
measureFpsDetailed perf-utils, dual 9.99+10.0 config seeding in pcbnew.html
so foreign-branch builds boot wizard-free, and the full benchmark report +
raw data under docs/features/async/migration-evidence/.

Headlines: wasm 94 vs 113 MB raw (18.6 vs 36.7 MB gzip), post-link tail
1.6 s/49 MB vs 63 s/6.1 GB per build, cold load −40 %, 27.7 MB board open
−45 %, real redraws +68 % at 4× throttle, boot heap −31 %.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 11:53:06 +02:00
Viktor Vaczi
9c475a804e jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.

Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
  could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
  (WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
  WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.

Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
  workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
  of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
  scaffolding (existed to parallelize the deleted wasm-opt phase; the
  postprocess is a seconds-long node script and now runs inline),
  build-monitor's dead asyncify rows, sched-context orphan build
  output, dead .gitignore entries, the .jspi-assets spike dir (the two
  wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
  called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
  main_stack_runner.h + 5 includes, the always-null context-sleep weak
  hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
  noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
  WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
  (gerber-demo keeps it: it loads the deployed CDN release, which
  predates emscripten 6 — noted inline).

Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).

Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).

Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).

Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).

Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
Viktor Vaczi
3ee174e9b4 tests: un-skip sweep — 26 tests revived on the JSPI build, failures re-gated with fresh evidence
Empirical pass over every skip/fixme whose premise the JSPI migration
could have changed. Revived (verified green):

- Firefox wasm-budget guards RETIRED (drift-trio, drift-trio-fuzz,
  drift-trio-scenarios, ysync-two-tab, ysync-libsymbols): the JSPI
  build (~half the asyncify size) fits three editor tabs inside
  Firefox 153's per-process budget — +20 firefox collab tests.
- roundtrip 'pcbnew preserves items through a yjs round trip': the
  asyncify-fragile envelope parse it waited on is gone — both engines.
- drift-trio-scenarios S4/S4b: converge now (was KNOWN ~5-8%).
- pcbnew-collab + eeschema-collab 'a local move propagates A→B'.
- web eeschema-fp-selector, read-only-editor's fixme'd writer-stream
  test, footprint-browse-remote read path (chromium; firefox gated:
  FootprintEnumerate rows never appear in 60s — slow wasm tier
  suspected).

Still broken, re-gated with re-verified reasons:

- 3d-viewer raytracer engine toggle: still inert, both engines.
- maximize display geometry: wxDisplay reports 0x0 in the harness.
- web editor WRITE bridge (symbol/footprint × remote/spike): wedges at
  the New Symbol/Footprint dialog step on both engines — the
  web-e2e-rot 01 gap stands for writes.

Verify runs: kicad+wx touched files 93 passed / 0 failed / 7 skipped
(intended gates); web touched files 7 passed / 0 failed / 9 skipped.
lint:determinism + lint:ci-coverage green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-13 18:43:36 +02:00
Viktor Vaczi
db819850ee jspi: fix the dead-tools ownership bug, emscripten-6 fallout, and green the full suite on Playwright 1.62
Live-app fix (Place Footprints / routing dead in Chrome): submodule
bumps carry the coroutine ownership fix (kicad 012d95ecb4) and the
handler-exception survival fix (wxwidgets 1b5f0e31f4).

Emscripten-6 fallout:
- occ/ngspice worker wrappers: mainScriptUrlOrBlob was removed
  upstream; pthread children re-run the wrapper blob, so an em-pthread
  realm now importScripts the glue and gets out of the way (before:
  recursive service boots, pool never fills, silent 180s boot hangs —
  every occ spec and ngspice bg_run).
- Makefile.wasm: -sASYNCIFY frankenlinks on the no-wx coroutine repro
  targets ported to -sJSPI (the JSPI-only libcontext crashed at first
  yield under them); mainloop/gl repro pages drive their tick through a
  promising export (emscripten_set_main_loop callbacks cannot suspend);
  retired inject-dyncall-shims lines removed (targets were unbuildable
  since Phase 8); $stringToNewUTF8 force-included (the EM_ASM value
  bridge aborted the runtime on the first decoded exception).
- fiber-park levers: neither embind shape can drive suspending levers
  (plain throws on strict-JSPI Firefox; emscripten::async() re-executes
  its invoker on settle) — kept sync for manual Chromium probing, spec
  coverage moved to the jspi-coroutine harness (18 cases).

Suite work:
- Playwright 1.61.1 -> 1.62.1 (Firefox 153: JSPI on by default).
- fiber-resume-park.spec retired -> coroutine-lifecycle.spec: census
  gate over boot / board load / chooser open / cancel (deterministically
  red on the pre-fix build).
- Blind asyncify-era pins re-keyed: quasimodal-strand + wait-beacons
  beacon regexes, footprint-chooser-close liveness -> wx parking-timer
  heartbeat (scheduler counters idle flat on Firefox).
- occ/ngspice test providers: 60s boot timeout + worker error
  surfacing (a worker death used to be a silent 180s timeout).
- Harness pages: stale 9.99 config dir -> 10.0 (library_manager wxCHECK
  noise, chooser had no libraries).
- gal-webgl harness: missing artifacts rebuilt (boost/glm extracted to
  the host sysroot), PgmOrNull stub added for the rebased GAL.
- jspi-scheduler: clean-shutdown console line restored (app-quit
  contract), quarantine never yanks SP from a live window.

Gates: test:e2e 699 passed / 0 failed (wx-chromium, kicad-firefox,
kicad-chromium, jspi-firefox, coroutine-firefox); web ff/cr/mobile 71
passed; lint:ci-coverage 166, lint:determinism 163, screenshots
manifest 492 current, corpus 7/7, tools:contract green. Offline
screenshot baselines show expected mass drift from the engine bump —
re-baseline (screenshots:noise -> promote) is a follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-13 17:41:28 +02:00
Viktor Vaczi
94cac5e4d4 port staging 29c61b8's zero-duration nanosleep guard (allocator must not suspend)
mimalloc's mi_atomic_yield() is sleep(0) on wasm, reached from malloc's slow
path under cross-thread delayed-free contention. Under JSPI a yield there
suspends the activation INSIDE the allocator — any other activation that runs
next can re-enter mimalloc mid-operation. ms==0 now busy-returns like stock
emscripten. (The removelist half of the staging fix is obsolete here — no
asyncify, no removelist. The mimalloc-storm harness + spec arrive with the
staging merge.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDeBaKKhQztd8KiVtHuyXr
2026-08-13 09:04:43 +02:00
Viktor Vaczi
e14faeca8b jspi: retire the asyncify pipeline — knob, post-link tail, binaryen hooks
Phase 8 in the parent repo. Deleted: asyncify-scheduler.js, apply-asyncify.sh,
apply-finalize.sh, inject-dyncall-shims.sh, asyncify-imports/removelist.txt,
the wasm-opt/finalize stub pair, scripts/binaryen-hoist-pass/ (the fork stays
a dormant submodule; removal is a follow-up), bench/wasm-opt-bench.sh (README
marked historical), wasm/shims/context_sleep.cpp, and the sched-context
harness app + Makefile targets.

PCBJAM_ASYNC_BACKEND is gone: build-wx-wasm.sh hardcodes the jspi stamp
(still force-cleans pre-migration trees), build-kicad-target.sh gives editors
the JSPI link surface and the CLIs nothing (they pin ASYNCIFY=0), the stub
dance is replaced by an unconditional .real-restore, build-wasm-test.sh lost
its whole post-link loop, docker/build.sh's postprocess is the ENV shim only,
and Makefile.wasm links every app JSPI with the scheduler shim as a tracked
prerequisite. pcbjam_async_policy.h keys on __EMSCRIPTEN__.

jspi-scheduler.js: wxWasmMainLoopPump dropped from the wrap census (the
export died with the D5 detach); inert [TRACE] instrumentation removed.

CI: wasm-build.yml rewritten for the single-cache pipeline (one output cache
keyed on compile inputs; post-processed bytes cached after the shim);
opt_level input removed from both callers. wasm-cache-hash.mjs inputs now
cover patch-env-shim.mjs + jspi-scheduler.js + jspi-exports.txt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDeBaKKhQztd8KiVtHuyXr
2026-08-13 08:39:12 +02:00
Viktor Vaczi
3f09a46ff5 jspi: migration phases 0-7 — build knob, scheduler shim, test successor suite
Toolchain: emsdk 6.0.6 (versions.sh; cache-hash keys on it). Build knob
PCBJAM_ASYNC_BACKEND=jspi|asyncify: build-kicad-target.sh links editors with
-sJSPI + -sJSPI_EXPORTS=@scripts/common/jspi-exports.txt + --pre-js
jspi-scheduler.js (no DYNCALLS, no post-link asyncify pipeline); wx build
stamps the backend and forces clean on flip or unknown provenance;
docker/build.sh passes the knob, seeds the emscripten ports cache from the
volume every launch, jspi postprocess = patch-env-shim only.

scripts/common/shims/jspi-scheduler.js: the JSPI successor scheduler —
token-wait registry, resume turnstile (one armed resume between engine
re-entries, SP swaps only at microtask boundaries), green-region spill
stacks (16-aligned tops), S1 embind mutator FIFO lane + parker wraps, S6
shutdown, libctx integration hooks (suspend/end/quarantine + g_current
arm/clear), SuspendError attributor, lost-wake + stuck-window watchdogs,
__wxWaitDump observability.

Embind: PARKER registrations get emscripten::async() under PCBJAM_JSPI
(wasm/bindings/pcbjam_async_policy.h). nanosleep yields route via the shim.

Tests: tests/asyncify -> tests/jspi successor suite (jspi-stack red/green
shadow-stack battery, jspi-coroutine MiniCoro harness, suspend-races
semantic scenarios + __wxWaitDump books coherence); projects jspi-firefox/
jspi-chrome (asyncify-webkit retired — no JSPI in WebKit); unconditional
Firefox JSPI pref; guard-beacons -> wait-beacons (+wxScheduler/libctxJspi
families); Makefile.wasm links test apps against JSPI with the shim as a
tracked link prerequisite.

Web: WasmTool setRo await + __wxWaitDump forensics, open-flow contained
promise, scheduler-shim.test.ts retargeted (8 green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDeBaKKhQztd8KiVtHuyXr
2026-08-13 07:06:24 +02:00
Viktor Vaczi
33e23e0a65 tooling(sourcetrail): code-graph setup + asyncify allocator-suspend plan
sourcetrail/: Sourcetrail 2021.4.19 indexing pipeline for the wasm port —
compile-db transform (rsp expansion, PCH strip, libc++-11 pinning, path
rewrites), removelist-candidate analysis over the indexed call graph, README
with regen steps and the hard-won tricks (relative --project-file hang,
SDK-header poisoning). Heavy artifacts (.srctrldb, compile dbs, libcxx
headers) stay untracked via the folder's .gitignore.

docs/asyncify-allocator-suspend/plan.md: verified plan for the nanosleep-shim
zero-duration guard (mimalloc mi_atomic_yield=sleep(0) can suspend malloc on
the main thread), the red/green contention test, and the adversarially
verified removelist additions with measured payoff (80.9k -> 60.0k
instrumented functions). Planned, not yet executed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2NHhbzuEHqP2JmcrSD96D
2026-08-11 13:53:01 +02:00
Gergő Törcsvári
80199f42f4
Phase F: restore the doc-19 bounce (footprint chooser dead-app) + regression spec
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Td4ujboGuAw26jvbDQzehj
2026-08-10 10:14:21 +02:00
Gergő Törcsvári
22fcb766e7
Phase F: registry deferred-wake fixes the footprint-chooser dead-app (gap 1)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Td4ujboGuAw26jvbDQzehj
2026-08-10 10:14:21 +02:00
Gergő Törcsvári
1d8237bc08
Phase F fix: open-lane token was lost across the dispatch-context swap — mint it in JS; repro spec added
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Td4ujboGuAw26jvbDQzehj
2026-08-10 10:14:21 +02:00
Gergő Törcsvári
747bf5ecf4
Phase F: kicadOpenFile runs on a dispatch context — the awaited-ccall class retired for the open (fcsTotal 1800→100/load)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Td4ujboGuAw26jvbDQzehj
2026-08-10 10:14:21 +02:00
Gergő Törcsvári
d185f5c1d2
Phase F lifetime: eager reclaim + the ring's population named — owning handles remain the only real fix
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Td4ujboGuAw26jvbDQzehj
2026-08-10 10:14:21 +02:00
Gergő Törcsvári
7122cdc461
Phase F F2/F3: quarantine NOT deletable — the lever proved the drop is load-bearing; registry keeps the fact
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Td4ujboGuAw26jvbDQzehj
2026-08-10 10:14:20 +02:00
Gergő Törcsvári
94e5c68909
Phase F F4: bounce machinery deleted — strand pin holds through the removal
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Td4ujboGuAw26jvbDQzehj
2026-08-10 10:14:20 +02:00
Gergő Törcsvári
594e6f9a27
doc 22: Phase F opens — tripwire audit, W4/W5 hole closed, deletion list scoped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Td4ujboGuAw26jvbDQzehj
2026-08-10 10:14:20 +02:00
Gergő Törcsvári
8ed39a72a0
THE FLIP: levers re-pinned to the post-migration invariant; all gates green incl. blue-screen repro 5/5
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Td4ujboGuAw26jvbDQzehj
2026-08-10 10:14:20 +02:00
Gergő Törcsvári
b9986d7909
doc 22: D-on probe clean after the bridge set; Phase E telemetry + prod-provider smoke recorded
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2SU74acXyviwSxBhMunFe
2026-08-10 10:14:19 +02:00
Gergő Törcsvári
d6c4efcf1f
Phase E complete: K4-K6 become token waits — the bridge set is converted, suite at baseline
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2SU74acXyviwSxBhMunFe
2026-08-10 10:14:19 +02:00
Gergő Törcsvári
75209dc07e
Phase E retry: K1 lands — early-resolve retention, not the lossy guard; pthread-ondemand link fixed
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2SU74acXyviwSxBhMunFe
2026-08-10 10:14:19 +02:00
Gergő Törcsvári
54e6560037
docs 22: Phase E first attempt (K1 as a context wait) - REVERTED, with the post-mortem
Converting the symbol-library bridge from one EM_ASYNC_JS into
beginWait -> start -> wxWasmYieldUntil -> take_result made the app hang after
every library request: the KiCad suite went from 7 minutes to 1.2 HOURS, 111
passed with the rest timing out, and the log goes silent right after a lib
request - a parked context nobody resumes. Reverted; the tree keeps the
working EM_ASYNC_JS and no code from the attempt.

Recorded for the next attempt:

- The PRECONDITION it found (four lines, quoted in the doc, deliberately NOT
  landed): wxWasmYieldUntil must not park a context whose wait is already
  resolved, because resolveWait deletes the entry. The in-place form had no
  such window; every bridge in this pattern re-opens it. It was not the hang,
  but it has only ever existed in the broken build, so it lands WITH the
  conversion it protects, not before.
- Three hypotheses in evidence order, cheapest first: the resolve never
  reaching the registry (this is the first KiCad-side caller of the wait
  registry - check Module["_wxWasmSchedResolveContextWait"] exists in the
  KiCad link); the lib request running inside the chooser's modal where the
  dispatch context is ALREADY parked on a nested wait (a context holds one
  park); or the Asyncify instrumentation closure shifting when the
  EM_ASYNC_JS left that translation unit.
- Process: gate a bridge conversion on a SINGLE spec, never the full suite.
  This cost a 1.2-hour run to learn one bit.

Tree is unchanged from the last verified landing state (STAR_DISPATCH=0,
kicad 139 passed / 1 pre-existing occ-probe) - docs only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBjomQfKyRa3jBdeAKpmTw
2026-08-10 10:14:19 +02:00
Gergő Törcsvári
ab6e44e57f
docs 22 + wx bump: DOM entries on the dispatch context; canvas tools green at D-on
The increment doc 22 ordered last round. Every entry that can reach a tool
coroutine - the four DOM callbacks and the mailbox tick - now goes through the
scheduler, so a coroutine is never entered by a star transfer from one path and
a direct symmetric swap from another. At D-on all four canvas-tool specs are
GREEN and the KiCad suite is 136/3 (was 135/5 before the sleep work, with the
tools red throughout).

context_sleep's wake learned the mirror lesson: now that the mailbox runs ON a
context, it must NOT call drain_all from there (drain refuses re-entry, and
should) - it marks ready and lets the outer drain_all perform the entry, with
an armed pump as a backstop.

Recorded honestly, not papered over: two of the three remaining D-on failures
are the timer-park and quasimodal-strand levers, each failing ONE assertion -
"scheduler shim observed the concurrent-park window", expected >0, got 0 -
while fired/done/parked/errors all pass. That counter needs TWO concurrent
in-place Asyncify parks, and the lever stages "timer park x MAIN-LOOP YIELD
PARK"; D5 removed the main loop's Asyncify park, so the overlap cannot occur.
Re-pinning those levers to the post-migration invariant is a Phase F decision
alongside fiber-resume-park's red->green flip, NOT an assert to relax now.

Landing state: STAR_DISPATCH=0, kicad 139 passed / 1 (pre-existing occ-probe).
Next: Phase E - the K1-K7 bridges are the only in-place parks left under a
context, and are exactly what the current()!=0 fallback still tolerates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBjomQfKyRa3jBdeAKpmTw
2026-08-10 10:14:19 +02:00
Gergő Törcsvári
80468e7c5b
Phase B: main-thread sleep parks its context; the real D blocker is DOM entries
wasm/shims/context_sleep.cpp: a main-thread nanosleep whose frame stands on a
scheduler context that OWNS the stack arms a mailbox wake and yield_parks that
context instead of suspending the stack in place. It lives in the sleep
primitive rather than in tool_manager.cpp on purpose - KiCad and the wx core
stay untouched (CLAUDE.md's fork rule) and the whole K7 class moves at once,
not just TOOL_MANAGER::RunSynchronousAction's spin loop.

MEASURED AT D-ON, and it is NOT what unblocks Phase D. The four canvas-tool
specs still fail, but the trace now names a different cause: the fatal swap is
old=<libcontext ROOT> new=<tool coroutine> with mouseEventHandlerFunc above it
- a DOM mouse handler entering wasm DIRECTLY on the main stack, bypassing the
tick. So one coroutine is entered two ways: by the tick through the dispatch
context as a STAR TRANSFER, and by DOM handlers as a DIRECT SYMMETRIC SWAP. A
capture written by one path cannot be rewound by the other -> index out of
bounds in doRewind. That is section 7 rule 5 (partial migration is worse than
none) in its purest measured form, and it is why the harness stays green: its
coroutines are only ever entered from one place.

So the next increment is the DOM event entries (mouse/key/wheel/resize must
hand their events to the dispatch context as the tick does), not another park
site. It subsumes the one-root work too: with no dispatch on the main stack,
resolve_root_identity() always answers "the running context".

Landing state: STAR_DISPATCH=0, kicad 139 passed / 1 (pre-existing occ-probe)
= baseline, with the sleep shim in and inert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBjomQfKyRa3jBdeAKpmTw
2026-08-10 10:14:19 +02:00
Gergő Törcsvári
f6171a7d34
docs 21+22: name the tool-body park site that blocks Phase D
Traced, not inferred: TOOL_MANAGER::RunSynchronousAction spins
`while(synchronousControl == STS_RUNNING) { wxYield(); wxMilliSleep(1); }`
(tool_manager.cpp:370-371), and on wasm wxMilliSleep -> nanosleep ->
__wasm_main_thread_yield_ms is an ASYNCIFY PARK OF THE STACK IT STANDS ON —
in a loop, inside a tool body, with a nested wxYield() dispatch running on
top of the parked stack.

The case closes on its callers: they are exactly the tools whose specs died
at D-on — edit_tool_move_fct (move-with-m, presence-locks move),
sch_drawing_tools (draw wires), the drawing/edit tools behind draw-lines.
The pcbnew spec comments already described the symptom from outside ("the
asyncified pointer-move handler") without naming the park; this is it.

So doc 21's K7 "anywhere" class has ONE caller that blocks Phase D, and it
moves first: the wait must yield the owning context instead of sleeping in
place, with the atomic's transition marking it ready. Recorded with the fix
shape, the upstream-divergence question it raises, and the gate that matters
(the KiCad suite's four canvas-tool specs — the harness has no
RunSynchronousAction and stays green either way).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBjomQfKyRa3jBdeAKpmTw
2026-08-10 10:14:18 +02:00
Gergő Törcsvári
78ec52ea14
docs 22 Phase B: gaps 1+2 closed, and the D-on boundary measured on KiCad
Phase B increment recorded in §10. The wx battery is GREEN at D-on (395/1,
the 1 pre-existing) with dispatch contexts, context waits and star transfers
all live - the first clean battery of the migration. Gaps 1 and 2 from the
D-on probe are closed (terminal coroutine finish; wake/refusal semantics),
and a third containment was found and added: an exception escaping a handler
propagates out through drain()'s fiber swap and would otherwise leave the
registry mid-transition, dead-pumping every later wait. Shim carries the new
abandon call; .ci-cache-epoch -> 12.

THE BOUNDARY: on the full KiCad suite D-on loses four canvas-tool specs
(draw-wires, draw-lines, move-with-m, presence-locks move) to `index out of
bounds` in doRewind - the blue screen itself. Real tool coroutines park IN
PLACE inside their bodies, and a star transfer over an already-parked stack
rewinds state the fiber layer cannot see; the harness's coroutines yield
cleanly, so it goes green while KiCad does not (the doc-19 lesson again:
the harness models the shape, not the parks). So D cannot carry KiCad until
the tool-body park sites are contexts too - C+E completion, which §5 already
ordered before the flip. This measurement makes that ordering non-negotiable.

Landing state verified: STAR_DISPATCH=0, kicad 139 passed / 1 (pre-existing
occ-probe glb) = the Phase A baseline exactly, wx battery 395/1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBjomQfKyRa3jBdeAKpmTw
2026-08-10 10:14:18 +02:00
Gergő Törcsvári
7dfd4f1834
docs 22 D-on probe + app-quit teardown spec; bump wx (alignment) + kicad (root identity)
The D-on probe (wxWASM_STAR_DISPATCH=1 on top of D5) is measured and
recorded in doc 22 §10: the week's 'environmental cliff' was adopted-fiber-
stack alignment (trap 1, fixed in wx - retires the D5 entry's findings 2-3
with a mechanism); with it fixed D5+D reached 388 passed; the remaining
reds decompose into three named Phase B gaps (wake ordering on a running
context, finished-coroutine transfer livelock, invocation-aware root
routing), each with a deterministic repro suite. D parks again behind the
switch until Phase B owns coroutine lifetimes.

app-quit.spec.ts + a wx_test_quit hook in minimal_test drive a real
File->Quit-shaped exit through D5's detached teardown (loop exits on its
context, S6 latch 'clean', OnExit + wxUninitialize there) - green in the
D-on probe, closing the D5 teardown-gate open item.

Landing state (D off): coroutine + coroutine-pthread + app-quit green;
nested red at case 3 in the pre-existing doc-19 wake-window family
(cases 1-2, yesterday's blockers, now pass); races wakeup_during_transition
stays layout-sensitive. Both are the class the C+B+E flip removes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBjomQfKyRa3jBdeAKpmTw
2026-08-10 10:14:18 +02:00
Gergő Törcsvári
801aff6649
docs 22 + wx bump: D5 done, delta-zero; three non-D5 findings from the bisection
D5 (main loop on a context, DoRun returns) is implemented and measured:
387 passed with D5 on, identical failure set with D5 off. The §10 D5 entry
records the working-tree bisection of the remaining reds, none of them D5:

1. the WIP jump_fcontext->fiber_transfer libcontext change is not
   behavior-preserving even dormant (kills nested baseline_fiber_alone;
   passes with the file reverted to Phase A) - fix before the flip, and it
   corrects this doc's earlier attribution of the WIP-tip failure to D;
2. the Phase A scoreboard is stale for coroutine-nested TODAY: at the
   exact Phase A state it dies at fiber_yield_across_modal_close -
   environment-sensitive, re-baseline before attributing;
3. races wakeup_during_transition flips with wx binary layout alone.

All three are the hot-main-swap-out wake-window class that C+B+E remove
structurally - expect those harnesses green AT the flip, not before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBjomQfKyRa3jBdeAKpmTw
2026-08-10 10:14:18 +02:00
Gergő Törcsvári
550edbb665
docs 22: trace where the D5 change must go (wx core, not the wasm layer)
main -> wxEntry -> OnRun -> MainLoop -> DoRun, and wx/app.h:102 says plainly
that when OnRun returns the program starts shutting down. So DoRun returning
propagates into wx teardown - which collides with the standing rule to keep
fixes inside the wasm port. Records the three options in evaluation order
(wasm-port OnRun override first, __WXWASM__-gated core change second, parking
DoRun rejected with its reason) and the teardown surface the gate must cover.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EarUW9DS1c1sSW4ZNrkGQS
2026-08-10 10:14:18 +02:00
Gergő Törcsvári
ef58c6ebce
docs 22: what D5 actually costs (a lifetime change, not a mechanism change)
The per-frame park is how a synchronous C++ frame waits; a frame that must not
park can only RETURN. So D5 = loop body on a context + DoRun returns + main()
returns without tearing wx down (EXIT_RUNTIME=0). Step 3 is the real cost and
needs its own gate - teardown, ~wxTopLevelWindowWasm, the S6 shutdown latch
and the v0.1.28 quit-notify fix all assume DoRun returning means the app ends.

Checked rather than assumed that no smaller fix exists: the scheduler stack IS
the main stack, so while it is parked in the rAF yield any pump entry re-enters
a logically suspended stack, and the loop parks every frame.

Recommends doing D5 first and alone (wx-only, cheap to iterate) before
re-enabling the already-written D/C/B/E on top.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EarUW9DS1c1sSW4ZNrkGQS
2026-08-10 10:14:18 +02:00
Gergő Törcsvári
41d49bdacc
WIP star-flip: shim routes context waits; docs 22 records that D forces D5
NOT GREEN. resolveWait marks a context-parked waiter ready and arms a pump
instead of resolving a promise nobody awaits.

The doc records the measured correction: DoRun parks the MAIN stack every
frame in wxWasmYieldToBrowser, which doc 21 called safe-by-construction only
because dispatch also ran there. With the scheduler swapping contexts from the
tick, those interleave over one currData - overlapped-wake, the exact class
this work exists to remove. So the main loop must become a context (D5) and
the bridges (E) join the same flip: D5+D+C+B+E land together.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EarUW9DS1c1sSW4ZNrkGQS
2026-08-10 10:14:18 +02:00
Gergő Törcsvári
ff4b2bdb3d
docs 22: the one-root constraint that orders the B+C+D wiring
The scheduler fiber and libcontext's root would both adopt the main stack -
two emscripten_fiber_t describing one stack, mutual corruption on first entry.
Harmless in Phase A (KiCad never called drain), fatal at the flip. So dispatch
must move onto a context FIRST, the libcontext root then adopts the running
context rather than the main stack, and only then can jump_fcontext become a
star transfer.

Also records that the synchronous-Call question is answered and pinned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EarUW9DS1c1sSW4ZNrkGQS
2026-08-10 10:14:17 +02:00
Gergő Törcsvári
258cb37487
star-flip: harness scenarios for star-mode transfers (doc 22 Phase B)
star_transfer_call_is_synchronous stages exactly the TOOL_MANAGER shape -
Call() then read Running() on the next line - and pins that a parked caller
resumed by the scheduler is indistinguishable from a synchronous return.
star_transfer_chain pins a three-deep nested call (root -> A -> B and back) in
strict order, with a parked star context proved undisturbed throughout.

A rule learned the hard way here: a fiber entry must NEVER return (emscripten
ends the program if it does), so these bodies park forever once their script
is done, like libcontext's trampoline loops. Getting it wrong wedges the
battery with no output at all.

The fiberRunning assertion flips to the stronger Phase B invariant: once the
pump is quiescent NO fiber is on the CPU, the scheduler is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EarUW9DS1c1sSW4ZNrkGQS
2026-08-10 10:14:17 +02:00
Gergő Törcsvári
837bce9c0a
docs 22: Phase B scoping from the seam read (the sync/star crux)
The synchronous TOOL_MANAGER contract vs the star: Call() must run to first
yield before returning, drain() refuses re-entry, and dispatch is not yet a
context - the same knot from the other side, and why B+C+D are one commit.
CALL_CONTEXT::Continue is a hand-rolled scheduler that should disappear rather
than be ported. Phase A's grace-ring-over-capacity: 33 says B must fix
coroutine LIFETIME, not just topology. Records the upstream-divergence
decision B has to take first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EarUW9DS1c1sSW4ZNrkGQS
2026-08-10 10:14:17 +02:00
Gergő Törcsvári
586fbadfe9
design-b Phase A: absorb libcontext's wasm backend (doc 22)
Bumps wxwidgets (fiber lane in sched_context.h) and kicad (libcontext's wasm
backend becomes an adapter over it), so there is now ONE party performing
every emscripten_fiber_swap and recording who is on the CPU. Behaviour is
preserved throughout - libcontext still decides, the registry observes and
beacons any disagreement - which is the de-risking step doc 20's D2 never had.

Harness + spec gain three fiber-lane scenarios: a fresh fiber enters at its
entry and a swap suspends the swapper (fiber_roundtrip), releasing a suspended
fiber is legal while a stale id refuses instead of use-after-free
(fiber_release_suspended), and symmetric swaps leave a parked star context
undisturbed (fiber_and_star_coexist). The spec asserts the lane's counters,
including the tripwire that must stay zero: fiberNonEnterableSwaps.

Doc 22 gains the Phase A work log: the gate (kicad suite 139/1, the 1 being the
pre-existing occ-probe glb matrix; wx 346/1/3; batteries 48/48; zero tripwires
anywhere), the four bugs the tripwires caught and the single rule behind three
of them, the grace ring and the 33-coroutine measurement Phase B needs from it,
and the process traps this run paid for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EarUW9DS1c1sSW4ZNrkGQS
2026-08-10 10:14:17 +02:00
Gergő Törcsvári
23c636dced
docs: doc 22 (absorb libcontext) + honest scoreboard on doc 20
Doc 20 gets a status table instead of a narrative: D-1/D0/D1 done, D2
reverted, D3's goal met but its MEANS skipped (waits still park in
place — only the stack they park on changed), D4/D5/D6 not started. Plus
an explicit "what was skipped and is still owed" list, so nothing quietly
reads as delivered: dispatch contexts, waits-as-yields, bridges,
real-park-site buffer sizing, the both-EH CI matrix.

Doc 22 is the handoff plan a fresh session can implement from:
- the bug it targets is the BLUE SCREEN (a context recovered twice or by
  the wrong fiber), not doc 19's hang, which is fixed and was only one
  ingredient;
- the diagnosis that matters: bookkeeping is already centralised
  (currData is single-writer, the shim sees every swap) but the DECISION
  is not — libcontext decides swaps and never tells anyone, so three
  layers each invented heuristics. Centralising means moving the
  decision;
- why D2/D3 knotted, with the measurements, so nobody retries that order;
- phases A-F with estimates (~4-6 wk): A absorbs libcontext behaviour-
  preservingly and is landable ALONE (the de-risking step D2 never had),
  B+C+D must land as one commit, E bridges, F deletes the guards only
  once they are provably silent;
- gates including the crash's own repro, since the batteries never
  reproduced it and therefore cannot certify it;
- the traps this run paid for: 16-byte fiber stack alignment, the
  per-fiber stack-limits gotcha, host-side asyncify buffers, the
  stack-ownership rule, partial migration, and the gitignored-artifact
  triage trick;
- measured build/test cycle costs, so the estimates are grounded.

README: index rows for 21 and 22, doc 19 marked FIXED, and the stale
2026-06 "single decisive next step" folded away behind the current one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:17 +02:00
Gergő Törcsvári
cc7c1d1f16
design-b D3: docs — doc 19 FIXED, and the plan re-scoped around that
Doc 19: status DIAGNOSED+PINNED RED -> FIXED, with the mechanism and the
explicit note that waits still park in place; the cure was to stop
parking on a COROUTINE stack, not to stop parking.

Doc 20: D3 work log (layering table, gate numbers) and a status that no
longer claims a phase queue. The bug that motivated the whole core
rewrite is closed WITHOUT contexts, so D2/D4/D5/D6 have lost their
forcing function and must be re-justified rather than continued by
default — including the honest option of deleting D1's unused context
layer.

Trap recorded: detection must capture the main stack's bounds at
top-level DoRun, not query them live — finishContextSwitch resets the
limits to the incoming fiber's, so a live query reports "main stack"
from everywhere and detects nothing.

Gate: kicad 139 passed / 1 failed (pre-existing occ-probe glb); wx +
asyncify + coroutine 394 passed / 1 failed (pre-existing
environment-sensitive modal:125). coroutine-nested — the battery D2
regressed — is green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:17 +02:00
Gergő Törcsvári
48da418f34
design-b D3: fix the doc-19 hang — quasi-modals off the coroutine stack
THE BUG IS FIXED. tests/kicad/quasimodal-strand.spec.ts flips from a
test.fail() pin to a plain green regression test: 3/3 runs
closed=true dialogs=0 refused-resumes=0 (was closed=false dialogs=1
refused-resumes=1 on every run).

Mechanism: a quasi-modal's nested event loop parked on the TOOL
COROUTINE's stack, which suspends the fiber's body where the fiber layer
cannot see it — so the stale-fiber guard quarantined the fiber and then
refused its own resume, the dispatch guard was never released, and every
click after that was deferred forever. Bouncing the nested loop onto the
main stack leaves the coroutine suspended the legitimate way (a recorded
fiber swap), so nothing is quarantined and nothing is refused.

Layering, so this is not a pile of WASM ifdefs in KiCad:
- wx (3d37db3bf1) owns the POLICY and the hook; it must not know what a
  coroutine is.
- wasm/bindings/main_stack_runner.h is the only place that may know both
  sides: it finds the frame's TOOL_MANAGER and bounces via RunMainStack.
  Header-only and self-installing, so no build-script change; included by
  every editor's binding TU.
- KiCad gets ONE ifdef-free method (2c777efede), needed only because
  TOOL_STATE is opaque outside TOOL_MANAGER. libcontext and dialog_shim
  are untouched — an earlier draft edited both and was reverted.

This also reframes the remaining plan: the doc-19 class is closed WITHOUT
migrating tool coroutines onto scheduler contexts. Note it does not make
the wait a context yield — waits still park in place, just never on a
coroutine stack.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:17 +02:00
Gergő Törcsvári
5d8d53b678
design-b D3: record the stack-ownership rule and the corrected scope
Bumps wxwidgets fc762bed (yield_park refuses a yield from a foreign
stack).

D3's phase text assumed the wait's caller runs on the context it yields.
The doc-19 path does not: the dialog is opened from a KiCad tool
coroutine — a libcontext fiber above whatever dispatched it — so a yield
would save the tool fiber's stack into the host context's fiber struct.
Guarded now, and the rule is explicit: a wait yields the context that
OWNS its stack.

That makes D3 three things, not one: tool coroutines become scheduler
contexts, waits yield the owning context, and dispatch moves onto a
context (the old D2). They are atomic — D2 alone regressed the nested
battery, and D3 alone has no context to yield. §5's architecture already
listed "tool contexts" among the three kinds the scheduler drains; only
the phase text was narrower.

Also pinned green: fiber-on-context nesting works. An earlier buggy
scenario trapped and briefly suggested otherwise, which would have sent
the plan somewhere wrong.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:16 +02:00
Gergő Törcsvári
6e80827cec
design-b D3 prep: pin fiber-on-context nesting and the foreign-stack refusal
Two scenarios, deliberately separate because they answer different
questions that D3 depends on:

- fiber_nests_in_context: can a libcontext-style fiber run nested inside
  a scheduler context at all? YES. Worth pinning explicitly — an earlier
  version of this scenario trapped `unreachable` and briefly looked like
  proof that nesting was impossible, which would have been a serious
  wrong turn for the plan. The real cause was the test's own bug: the
  host-side emscripten_fiber_t was zero-initialised, so the swap unwound
  into a null asyncify buffer. Nesting works; the scenario now proves it.
- foreign_stack_refused: a yield from that nested fiber is REFUSED, and
  the host context and scheduler both survive it.

The spec asserts foreignStackRefusals == 1, so the refusal path cannot
silently stop being exercised.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:16 +02:00
Gergő Törcsvári
5a3dd44b23
design-b D2: attempted, reverted, resequenced behind D3
Bumps wxwidgets d220aae5 (D2a: sched_context.h moved into wx's port,
header-only so evtloop.cpp can see it) and 5ee60a81 (D1 fix: 16-align
context stacks — EM_ASM's arg buffer lives on the running stack and the
glue asserts buf % 16 == 0, so misaligned contexts trapped in
readEmAsmArgs; std::vector<char> only gives malloc's 8-byte alignment).

The dispatch switch itself is NOT landed. Running the tick's
ProcessEvents on a context took the battery from 363 green to 388/7, six
of them the coroutine-nested harness wedging at
fiber_create_run_destroy_inside_modal via aliased-wake-live ->
fiber-resume-refused — doc 19's mechanism. A quasi-modal opened from a
tick handler suspends the dispatch context INSIDE the still-in-place
wait, putting one more Asyncify layer under every libcontext fiber.
Pooling contexts (8 burned in 30 ms) and falling back to entry-stack
dispatch both failed to avoid it, because the layer exists as soon as
the context is suspended.

That is doc 20's own risk 2 arriving on schedule, so the plan is
corrected rather than the symptom patched: D3 (waits become context
yields) must come first, after which the dispatch context is released at
its tick boundary instead of suspended and the failure class is
structurally absent. Verified the revert: the nested battery is green
again at this baseline (5 passed / 1 failed, the 1 being the
environment-sensitive modal:125 that also fails without any of this).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:16 +02:00
Gergő Törcsvári
41f3f35919
design-b D1: work log + audit note
Doc 20: D1 entry (star topology as the load-bearing decision, the
9-scenario gate, measured memory) and the §3 asset table flipped —
registry/park/resume/drain are no longer "stubs that throw".
Doc 21: what D1 built, plus the sizing instruction for D4 — take each
bridge's deep-park high-water from its own beacon; do NOT carry the
harness's ~34 B/frame figure (synthetic frames, three locals each = a
floor) and do not inherit libcontext's 512 K by default either.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:16 +02:00
Gergő Törcsvári
7dbc9fb36e
design-b D1b/c: context harness + the bounded-and-measured memory gate
Doc 20 §6 D1, second slice: the dedicated test app D1 calls for, plus
the doc 20 §7 risk-1 gate.

tests/apps/standalone/sched-context/ drives the primitives with no wx
linked (a failure can only be the contexts layer) and assertions ON —
unlike the races harness, an emscripten "cannot start an async
operation" state here IS the bug, not a tolerated state. Nine
scenarios, each pinning one invariant; the load-bearing ones:
- parked_does_not_block: a parked context does not stop others running.
  This is doc 19's freeze made unrepresentable — there a parked activity
  held a global interlock and the UI died; here "parked" simply means
  "not runnable", and three workers run past it before it resumes.
- one_transition_in_flight: a context calling drain() gets a no-op, with
  a second ready context queued so a buggy nested drain would actually
  run something and be caught.
- async_wake: a real macrotask hop (setTimeout → mark_ready → drain),
  the shape every production bridge has.
- deep_park_sizing: parks 64 live frames deep to measure what a park
  actually costs.

The memory gate asserts the ceiling (peak live contexts, peak bytes),
that nothing leaked (live=0, bytes=0, created==finished), that nothing
was left mid-transition, and that refusals occurred (zero would mean the
illegal-operation scenarios stopped provoking).

Measured here: ~34 B/frame, 2200 B for a 64-frame park, 1 MB peak for 4
concurrent contexts. Recorded with the caveat that the harness's frames
carry three locals each, so this is a FLOOR — real bridges save far more
per frame (libcontext runs 512 K after a 64 K buffer silently
overflowed). The apparatus and its units are validated; the sizing
DECISION needs deep-park numbers from real bridges at D3/D4.

playwright.config: asyncify-firefox now matches every spec in ./asyncify
instead of only asyncify-races* — a new harness there is covered by
construction rather than by remembering to widen the pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:16 +02:00
Gergő Törcsvári
98731cd30e
design-b D1a: scheduler context primitives on emscripten fibers
Doc 20 §6 D1, first slice: wasm/sched/context.{h,cpp} implements
create / yield_park / mark_ready / drain with the registry as truth.

The shape that matters — a STAR, not libcontext's symmetric swap:
contexts only ever swap OUT to the scheduler, and only the scheduler
swaps IN. So "is this target safe to enter?" stops being a guess
(libcontext's swap_suspended / parked / hot-main refusals) and becomes a
lookup: the registry says Parked/Ready and holds the buffer. Doc 19's
refused-resume is unrepresentable here because resume is not a decision
made at the swap site.

Enforced, not hoped for:
- at most one transition in flight (drain() refuses re-entry, so a
  context calling drain() cannot turn the star into a cycle);
- mark_ready() never resumes inline — it queues, and drain() resumes
  from a clean stack (doc 13 §1.4's deferred-wake law, per context);
- yield_park() off a context is REFUSED, which is the "nothing parks in
  place" rule made mechanical;
- destroy() on a non-Finished context is refused (freeing a parked
  stack strands whatever is on it);
- FIFO ready queue (no starvation);
- main-thread-only, per doc 21 §2's pthread finding.

Memory is accounted from the start (doc 20 risk 1): live/peak contexts,
bytes/peak bytes, and per-context asyncify high-water measured from
asyncify_data.stack_ptr — the same quantity the shim reports as `rem=`,
from the other end. Sizes deliberately start at 128 KB C stack + 128 KB
asyncify buffer rather than inheriting libcontext's 512 K, so the number
gets derived from evidence; a >75% buffer use beacons BUFFER-PRESSURE
because that overflow is silent corruption, not a crash.

No production path runs on this yet — dispatch moves at D2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:15 +02:00
Gergő Törcsvári
832cbcdf91
design-b D0: record the audit + red pin in docs 19/20/21
Doc 19: status DIAGNOSED -> DIAGNOSED + PINNED RED; the repro section
leads with the automated deterministic spec (the manual Leonardo flow
stays as the field repro).
Doc 20: D0 work-log entry; status D-1 + D0 done; next = D1.
Doc 21: red-spec section updated to "landed", with the staging test's
anti-vacuity role.

Carried finding: the strand reproduces on a 2-OBJECT fixture schematic,
so warm-load byte volume (the 68/1 dice-loader) is not an ingredient —
two concurrent parks suffice. That is why D3 closes this class and D5 is
not required for it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:15 +02:00
Gergő Törcsvári
73a239e0be
design-b D0: land the doc-19 strand as a deterministic red spec
Doc 20 D0 second deliverable (doc 21 §4). tests/kicad/quasimodal-strand.spec.ts
reproduces the Symbol Properties hang on demand, in two parts:

- "staging" (GREEN): double-click the fixture symbol → Symbol Properties
  opens → arm the parking timer (wasm/bindings/timer_park.h) WHILE the
  dialog is up. Because the opener's fiber park is open-ended, a timer
  firing now necessarily parks on top of it — the overlap is structural,
  not a won race. Asserts the dialog opened, the timer fired, its OK
  button is hittable, and the shim beaconed concurrent contexts. Keeps
  the red pin from rotting into vacuity, and fails loudly on its own.
- "doc-19 red" (test.fail()): clicks OK and asserts the desired end state
  — dialog closes, zero fiber-resume-refused beacons, wait books balanced
  (no unresolved nested/modal wait). Goes green at D3, when Playwright
  will report "expected to fail but passed" and the marker comes off.

Verified 6/6 consecutive full-file runs, identical outcome each time:
closed=false dialogs=1 refused-resumes=1 — the doc-19 mechanism exactly
(quarantined fiber's legitimate resume refused, dialog never closes).

Deliberately NOT asserted in staging: the timer park COMPLETING (whether
a park survives the aliasing is the disease under test) and sawParked (a
100ms sampler can miss a short park) — both are reported, not gated.

Retires tests/kicad/dialog-deadlock-probe.spec.ts: the 8/4 throwaway probe
that established the mechanism. This spec supersedes it and, unlike it, is
deterministic (the probe's 3 blind waitForTimeouts were the only
determinism-lint violations in the tree; the guard is now clean).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:15 +02:00
Gergő Törcsvári
704668ef7a
design-b D0: park-site audit (doc 21)
Doc 20 D0 first deliverable: every Asyncify park site post-D-1,
classified by whose stack it suspends (tool fiber / entry stack / main
loop) with a routing decision per site — 14 production sites (8 wx, 9
KiCad/bridge counting quartets/pairs) + 3 deliberate test levers.
W1 (wxWasmYieldUntilJs) and W3 (popup) route to D3 context yields;
the clipboard/font/lib/3D/occ/ngspice/nanosleep bridges route to D4;
W2 (per-frame yield) stays safe-by-construction unless D5 is taken.
Also settles doc 20 risk 4 (pthreads): all parks are main-thread only;
the lib bridge's pthread path is blocking-proxy, not Asyncify, and is
out of migration scope. Defines what D4's "no handleSleep on a fiber
stack" assertion must cover.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:15 +02:00
Gergő Törcsvári
d519ac89c0
design-b D-1 gate: battery green scheduler-only; startModal import scrub + work log
Gate results (single battery, scheduler-only): wx app battery +
asyncify + coroutine 363 passed / 3 skipped / 0 failed; full kicad
suite 138 passed / 30 skipped / 1 failed — the one failure is the
pre-existing local occ-probe glb case (predates D-1, unrelated).

Post-gate scrubs: vestigial 'startModal' removed from ASYNCIFY_IMPORTS
(tests/apps/Makefile.wasm) and env.startModal from
scripts/common/asyncify-imports.txt (the import no longer exists in any
wasm; both lists are boundary supersets so behavior is identical).
Stale comment pointers to the deleted legacy modal machinery updated.
Doc 20: D-1 work log added, status flipped to IN PROGRESS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:15 +02:00