A review of the group-E fixes found 13 further defects; ten were introduced by
those fixes, two pre-existed and were merely relocated, one is deferred.
Services / transport
E-10 retireWorker synthesized no bg/exit frame, so sharedspice's s_bgRunning
mirror stayed latched true after a mid-run worker death: Run stayed
disabled and the promised fresh-worker restart was unreachable for the
whole session. Retirement now dispatches a synthetic controlled-exit
straight to the installed handler (never through dispatchEvt — a
fabricated frame must not touch the credit ledger). Driving the repro
exposed two further defects, both fixed here: a replacement worker
trapped on pre-init engine reads, and the rerun's cm_input_path/circ hit
that uninitialized engine before KiCad's validate() re-init (the native
flow assumes a crashed engine survives in-process — true for the dll,
false for a dead worker). Reads now answer their empty shapes pre-init,
writes lazy-init, and init is idempotent per worker engine.
E-19 dispatchEvt acked only AFTER handler(evt) returned, and the sharedspice
client deliberately rethrows non-trap errors — so each throw leaked one
unit of the 64-frame credit window until the stream died with a
misattributed "transport exceeded". The ack moves to a finally in both
service copies; the throw still propagates (the trap machinery needs it).
E-20 the oversize-line path promises to transfer the accepted prefix, but
with the window full that flush only DEFERS, and stopEventStream wiped
the deferred queue — losing the diagnostics that explain the failure.
The terminal notice now carries them as pendingEvents; both hosts
deliver them in order, unacked (the fatal frame is outside the credit
protocol).
E-21 the 30s prefetch deadline discarded every model already collected and
reported nothing. A caller-owned progress sink ships the partials and
the omission reaches the export report. (Awaiting the aborted collection
was rejected: an in-flight source fetch is not abortable — E-4's
original disease.) Plus a serving-candidate memo, so a .wrl ref served
by its .step fallback stops re-probing the miss on every export.
Scheduler
E-14 _terminalizeNativeTrap classified by message substring, so any plain JS
error QUOTING 'Aborted(' or 'out of bounds' permanently bricked a
healthy instance. Now structural only: instanceof RuntimeError plus a
duck-typed name check (verified in this build's glue that abort() throws
a genuine RuntimeError both pre- and post-runtime-init). Module.onAbort
now latches the gate — the authoritative notification, previously
ignored.
E-15 the shim half: _pumpResume gates on terminal (catching wakes already
queued at latch time) and resolveWait refuses on terminal WITHOUT
consuming the entry, so a frame stays visibly parked rather than
resuming inside a trapped module.
E-16 the E-5 handler read the realm-global scheduler at dispatch instead of
its installing module's; also frees the per-line buffer on the non-trap
rethrow path.
E-11 get_vec trusted the worker's res.length over the transferred arrays.
Observed death shape: a 4 GiB std::vector threw an unhandled
std::length_error that exited the editor's main loop. Now clamped, with
the buffers freed on every failure path.
Guardrails (replacing two deferred refactors: e2e→production-code injection and
collapsing the four copies of the worker-lifecycle machinery)
E-18 the source contract asserted comment-string counts — rewording failed
CI while moving a guard outside its #ifdef passed. It now parses the
#ifdef regions and asserts on code.
service-stub-parity.ts pins what the four lifecycle copies must share:
credit-window equality parsed from source, the finally-ack, boot
deadlines, terminal-notice consumption. The transport numbers are now
single-sourced from the worker.
CI actually runs the gates: the web/standalone vitest suites (which had
NEVER run in CI), the reducer, the source contract and the parity tool —
with a NON_PLAYWRIGHT_GATES check so deleting a step re-fails the lint.
E-22 the e2e occ stub's 60s boot watchdog, deleted in a66e109, is restored in
the ngspice-stub shape with a wedgeNextBoot() repro hook.
Every behavioral fix has red-then-green evidence (the reds were captured first).
E-17 (a stale RUNNING cross-stamping the next run's generation under E-6's
transport deferral) is DEFERRED with its analysis recorded — a real fix needs
run identity on the bg frames.
Test hygiene: the dwell lint now requires the mandated ": <why>" and all 47 bare
markers carry their reason; three export-report dwells became modal-lease polls;
exact-ledger assertions became relative deltas; the dead data-wx-dom-id branch,
an unused fault hook and unused receipt plumbing are gone; abort scans, wx
dialog drivers, the sim harness and the vitest FakeWorker are each one copy now.
Bumps kicad and wxwidgets to their findings-group-e tips.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
E-5: the module-identity bridge called installingModule._malloc, but this
build exposes _malloc only as a bare glue-closure export (Module._malloc is
absent) — every char/stat event entry threw TypeError, which also starved
the E-6 credit window (thrown dispatches never acked) and wedged the queued
bg-finished frame behind them. The bridge now uses the bare closure exports
(identity is still exact: the EM_JS body IS the installing module's closure;
the __ngspiceOnEvent self-disarm covers supersession).
E-6 (codex reference design corrected — its validation matrix never ran):
a FULL credit window was terminal (stopEventStream at 64 in-flight frames).
Under live e2e that killed a real simulation: bg-thread emissions proxy one
per task, so each line ships as its own frame and a normal transient outruns
a busy main thread. A full window now DEFERS into a bounded FIFO (512 events
/ 4 MiB) drained in order as acks free credit; only true overload or an
invalid ack is terminal. Retention stays bounded (8 MiB in flight + 4 MiB
deferred + 1 MiB open batch). And the service/harness mirror queue now acks
at ENQUEUE — placing a frame in the bounded pre-handler queue is taking
ownership; without that, a stream starting before the C++ handler installs
(the ngspice-probe page) starves the worker window forever.
Test updates: worker-batch reducer — new "a full credit window defers and
drains in order, never terminal" case pinning the regression; the storm case
now proves the deferred caps are the terminal edge. board-ready.ts gains the
owner-free openBoardProgrammatically (codex helper the ported occ-export
spec needs; the barrier-based waitForUiBoardReady was NOT taken).
occ-export.spec: domId is optional on this line's registry (coordinate
fallback is the supported path).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Field bug (2026-08-31): copy-pasting a symbol whose pins kept their source
uuids corrupted the sheet's ydoc on delete (pcbjam-shared: cross-parent
re-key fix, bumped here); the pcbnew tab's sibling mirror then silently
froze on the dangling refs, so "Update PCB from Schematic" with delete
enabled never removed the deleted symbols' footprints — until any later
edit resynced the sheet.
- standalone sibling-restage: render tolerantly past dangling item refs
(docToFile onMissingItem) and console.warn on drops/failures instead of
only the gated debug log — a frozen mirror is no longer silent.
- pcbnew_embind flushDiff: dirty roots that are already STRUCT_DELETED emit
forced removals on both wires instead of being P-5-skipped (observed: 8
footprints deleted on the board stayed in the board room forever); they
are erased from the next baseline so a redo re-adds them.
- eeschema_embind blobFor: lift an unlifted child (field/pin/sheet-pin) to
its screen root before serializing — the selection writer emits nothing
for such a child standalone, so the entry used to reach JS as an empty
envelope and be skipped, silently dropping the edit; residual empty blobs
now warn instead of shipping hollow envelopes. SCH_MARKER_T (ERC
artifacts, never file content) stays out of the snapshot and seed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MLPKYptXFrxHj5Gu7rhToz
Canvas-only eeschema sessions (read-only viewers, hide-UI editors) could not
reach the wx hierarchy pane, so a viewer of a hierarchical design was stuck
on the entry sheet. Adds the React stand-in (SheetPanel) + the bridge:
- eeschema_embind: kicadSheetsGetTree() (SCHEMATIC::Hierarchy(), page-number
ordered: instance KIID path / parent / Sheetname / file / page / depth) and
kicadSheetsEnter(path) (validated synchronously, applied on the coroutine
via SCH_ACTIONS::changeSheet — same route as the wx pane, history intact).
OnSchSheetChanged pushes window.kicadCollab.onSheetsState after EVERY
navigation, wx-driven included, so the panel is event-driven.
- kicad → 9ab93b838e: navigate actions allowlisted for read-only viewers.
- SheetPanel.tsx: LayerPanel shell conventions; rendered + menu row only when
the hierarchy has >1 sheet instance; viewers boot it collapsed.
- tests: fixtures/demo/hier (root → Power, IO → Sub) + web/sheet-panel.spec
(list/order/depth, navigate + collab rebind, menu toggle/close, flat
schematic has no panel, bridge-driven navigation updates the panel);
SheetPanel.test.ts for the wire parser.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtN6ASBvMGNbjPqY5boycg
- P-1 (findings-p.spec P-1c red on CI + locally): a JS exception thrown from a
window.kicadCollab wire callback does NOT reach the C++ catch(...) — under
JSPI it REJECTS the promising coroutine entry, slot.done never flips and the
apply slot is busy for the page's life. Three layers: libcontext flags the
rejected record dead (kicad → d0afbc4bd3),
drainApplies() reaps a !done && !CanResume() body, and every EM_ASM
window.kicadCollab.onX(...) call is try/catch-guarded (collab_common.h,
kicad_editor/eeschema/pl_editor embind).
- popup.spec 'dismisses on outside click': wx P-4 SetFocus delegation excluded
for wxPopupWindow (wxwidgets → a7436d1052).
- ysync-two-tab concurrent seed: repairLayoutY + seedNonce retraction deleted
both headers (pcbjam-shared → 8dabf2e).
- dark-mode.spec: stale light-mode reference (pre wx fa46f1822c DOM
re-projection) replaced with the CI chromium render.
- items-wire-batch-loss.spec: precondition obsolete after P-5 — poisoned entry
is now synthetic, plus a P-5 contract assertion (field blob == '').
- presence-ghost-peer.spec: skip early when no /parties/ socket (CI web e2e
runs the BroadcastChannel provider); findings-p1.spec: polled click-select
precondition.
Local: kicad-chromium findings/presence/ysync/collab 59/59, popup 7/7,
dark-mode 1/1, pcbjam-shared vitest 249/249.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvvARNMxQrq17vogn3REMU
- wasm core: CORE::onDocChanged() from both collab listeners (local commit AND
remote apply) repaints peers' shapes from the live document + re-checks the
local selection post-settle; PresenceStart registers the bridge listener
- wasm core: cursors on their own overlay trio; shapes repaint only when the
non-cursor snapshot changes; new kicadCollabSetRemoteCursors (cursor-only
update, PEER.id) in both TUs + merged editor + JSPI mutator allowlist
- presence.ts: size-aware trailing throttle for cursor/viewport publishes
(128 KB/s budget) + parsed-peers memo; presence-kicad.ts: cursor-only push
when the shape signature is unchanged
- gateway.ts: honor the `gone` control (removeAwarenessStates)
- specs: kicad stale-after-delete gate, web ghost-peer timing (+diag), unit
select-all budget (pcbjam-shared → 4573a7c)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AScTR39aqyrY5i3ZFHmnMn
- P-5: blobForItem returns "" for PCB_FIELD_T (the file writer cannot format a
field standalone → hollow (kicad_pcb …) envelope); liftBlob skips empty blobs
and STRUCT_DELETED roots with a console breadcrumb; the scalar added path and
the seed never attach an empty blob.
- P-1 (not reproduced, structural): the runOnCoroutine apply body runs in
try/catch(...) so an unwinding body still marks the slot done instead of
wedging fit/pan/flushDiff/remote applies forever; new probe
kicadCollabTestApplyQueueState() → {busy, queued}.
- P-4: wxwidgets 9666a743bc (TLW SetFocus delegates to a child,
menubar never takes focus); probe kicadTestFocusWindow() (wx FindFocus).
- Specs: tests/kicad/findings-p.spec.ts (P-1 ×3, P-2 probes as fixme, P-3 ×2,
P-4, P-5), tests/web/findings-p1.spec.ts (P-1 on the real standalone).
P-2 verdict: stock KiCad candidate priority, text scales normally; P-3 not
reproducible — see docs/features/findings/groups/P-editor-tools-embind-interaction.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmnMPkjugyGqe4LvUsBB14
- kicadShow3DViewer embind (pcbnew-only name, registered in the unguarded
section so the merged kicad_editor image carries it): runOnCoroutine →
ACTIONS::show3DViewer.
- Session menu: "3D viewer" row (pcbnew) — the only 3D entry once the wx
chrome is hidden. Runs the deferred model prescan first.
- Read-only sessions: skip the boot-time enableRealtime scope-room socket
(would 401 for non-members) and defer the board's 3D-model prescan until
the viewer is opened (deferBoardModelPrescan / runDeferredModelPrescan).
- kicad submodule → read-only allowlist for the 3D viewer actions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXCntRWNn89M72EkrqMkvc
- kicadLibsInvalidate: a peer's edit only drops the lib's plugin entry + pcbnew's
PreloadedFootprints (the cache the old reload never cleared — tree/preview/
LoadFootprint/update-from-library kept serving the old body); the fat re-load
now runs lazily or from Update-from-library (kicadLibsReload, which also
clears the preloaded cache)
- usage bridges count the Footprint/Symbol Editor's open copy; update re-opens
an unmodified copy, reports a modified one
- embind TU gets eeschema/symbol_editor on its include path; smoke probes
- kicad → 27051b46e2
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Wd1r3ewftpV1DBSEArpRa
Switching OpenGL → raytracing → OpenGL could leave the viewer showing only the
background gradient, with mid-session "[gl1] WebGL context changed" thrash and
INVALID_OPERATION storms on BOTH WebGL contexts. Traced mechanism: the shim's
FFP draw routing keyed on one process-global client-array flag; an interrupted
fixed-function window (MODEL_3D::BeginDrawMulti loops, or the switch-back
reload re-recording display lists with the GL context lock released across
JSPI suspensions) left it set, after which the raytracer blit's and the 2D
GAL's glDrawArrays were routed through the FFP pipeline — and one misrouted
draw permanently repointed the VICTIM's own VAO attributes at shim buffers
(the blit's attribute 0 collides with ATTR_POSITION), so both stayed
broken/blank even after the flag cleared.
Shim fixes (wasm/gl1):
- Owner-context routing gate: __wrap_glDrawArrays/Elements route into the FFP
pipeline only under the shim's owner context (adopted at the first FFP
client-state mutation or programSync in a context); foreign-context draws
always pass through — the 2D GAL can never be misrouted and the context
guard can never thrash.
- VAO isolation (ScopedDefaultVAO): draw executors do their attribute setup on
VAO 0 and restore the caller's binding — a misrouted draw can no longer
corrupt the caller.
- contextSync() resets the whole client-array mirror on a context change
(enables/pointers/VBO names all described the dead context).
kicad pointer bump (d6e3dc1a87a): blit preamble disables the four client
arrays (same-context firewall) + DoRePaint hidden-parent early return now
clears m_is_currently_painting like its six siblings (a standalone
sufficient cause of a permanently blank viewer).
TDD (each observed red before its fix, green after; harness = authoritative):
- T1 VAO corruption, T2 foreign-context routing + guard thrash, T3 stale
client-state surviving context recreation — tests/e2e/3d-webgl.spec.ts over
new harness choreography (appQuad/ffpMakeStale/createSecondContext/
quadDrawFresh). Parity stays 47/47 with zero drift.
- tests/kicad/3d-viewer-engine-toggle.spec.ts (new, CI-skipped like the
deadlock spec): real round-trip happy-path gate — board re-renders, zero
[gl1] lines, zero INVALID_OPERATION. (The raytraced image itself never
displays on the wasm build — the pre-existing inert-toggle KNOWN ISSUE in
3d-viewer-deadlock.spec.ts, out of scope here; the engine switch and the
poisoning reload path run regardless.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closing the viewer destroys its wxGLCanvas's WebGL context; reopening mints a
new one. The gl1 shim cached GL names (FFP program, stream/scratch VBOs) in
never-reset statics behind `if (!handle)` guards — in the new context every
draw died with INVALID_OPERATION and the viewer showed only the clear color
("Reload time 0.031 s" is benign: warm model caches make the rebuild fast).
contextSync() (gl1_state.cpp) now detects the context change in programSync()
— the one choke point every shim draw crosses, and a path the 2D GAL never
reaches (a first attempt checking in the glBindTexture wrap saw the GAL's
context and thrash-rebuilt the program 23x per run) — and drops the cached
names for lazy rebuild in the new context. Context identity is a monotonic id
stamped on Emscripten's per-context record: the numeric
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE is recycled, so a destroy-then-create can
return the same number and a handle comparison detects nothing.
The lost-position half is a wxwidgets wasm fix (pointer bump: GetFromWindow
reports display 0; saved geometry used to carry display=(unsigned)-1, which
LoadWindowState treats as "display not found" and re-centres the frame).
TDD (red observed before each fix, green after):
- tests/kicad/3d-viewer-reopen.spec.ts (new, own worker like the deadlock
spec): load board, open viewer, render-gate, drag by the titlebar, close
via the x, reopen; asserts the board re-renders (was: 1 distinct colour for
90 s) and the window position is restored (was: re-centred to 0,0 after
closing at 40,90). Green run logs exactly one [gl1] context-change line.
- 3d-regression harness: recreateContext() destroys the context AND swaps in
a fresh canvas element (a browser canvas keeps its context for life, so
same-element recreation hands back the live old context and hides the bug);
the new 3d-webgl spec test renders redraw-mini-board-navigator before and
after recreation and requires pixel-identical output. Parity: 47/47, zero
drift.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kicad_tools --resave-batch <outdir> <file>...: N resaves in one process
(one WASM init — marginal resave is ~50ms vs ~0.3s/process). Outputs in
<outdir>/<index>/, per-file "RESAVE-BATCH <index> <exit-code>" stderr
verdicts with the single-file 0/4/5 contract; exit 0 = loop completed, so
an invalid file mid-batch can't mask its neighbors.
loadSchematicHeadless now binds each schematic to ITS project via
SETTINGS_MANAGER::GetProject, never Prj(): with aSetActive=false projects
accumulate across batch entries and Prj() keeps returning the first one —
the writer then stamped a wrong/empty project name into saved
symbol-instance data. (UnloadProject is no fix: unloading the active
project immediately reloads a null "" project.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AH2iPekUGsEAYnMUD5BmAi
Canvas-only sessions (read-only viewers, hide-UI editors) get two
floating, draggable, closable panels standing in for the chrome-hidden
wx panes, opened from the overlay menu's View section:
- wasm/bindings: layer bridge — kicadLayersGetState/SetVisible/SetActive
(bodies mirror the compiled-in-but-unreachable IPC handlers, applies
on the coroutine lane, fresh state pushed to
window.kicadCollab.onLayersState); setters join the jspi-scheduler
mutator lane.
- standalone: LayerPanel + SelectionInspector on the comments-panel
shell conventions (useDraggablePanel, collapse, persisted state);
local-selection store fed from presence's onSelection in edit
sessions and bindLocalSelectionFeed for read-only viewers (with a
bounded post-gesture pull burst — clarify-menu selections produce no
canvas event); pure item-summary extraction + unit tests.
- kicad submodule: read-only selection unlock (selection live for
inspection; point editors + RMB context menus stay locked).
- tests/web: read-only spec updated — viewer click selects (or pops the
clarify list), RMB context menu suppressed with writer positive
control, Delete still swallowed; new viewer-panels test (eye toggle
round-trip, active layer, panel drag, inspector rows from a real
canvas click).
Record: docs/features/read-only-viewer/0002-viewer-panels.md (root).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011gJ3M1RpeZLeNUUj8jKC4h
- itemToJson: pass PADSTACK::ALL_LAYERS for vias — the layerless virtual
PCB_VIA::GetWidth() is an assert trap since the padstack refactor, and the
collab baseline/snapshot serializer hit it once per via per snapshot
(big-board load = assert storm). Values were already correct; wire format
unchanged (applyChanged's layerless SetWidth writes the same slot).
- console: closed state is a content-width bottom-left tab again (version
badge + app bottom edge visible); opened footer panel unchanged.
- console: partial-selection copy works — wx's window-level keydown handler
preventDefaults Ctrl/Cmd+C, so a capture-phase guard stops propagation to wx
when the selection lives in the console; canvas mousedown collapses stale
log selections so they can't steal the editor's own Ctrl+C.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HPtPBVLKQzaXTkirYgcVay
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
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
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
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
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
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
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
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
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
kicadTestFiberParkStartSecond/PokeSecond: a second coroutine started while
the first body is asyncify-parked reproduces the misattributed jump that
launders the parked fiber past the C++ guard (the v0.1.21 prod bypass).
Spec scenario 2 stages it and asserts the JS stale-rewind guard quarantines
the laundered resume (exactly one fiber-resume-refused beacon), the parked
body completes undisturbed, and both coroutines finish cleanly.
Doc: async/16 rounds 2 + WSOD section.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
Companion to kicad f0ce20ef64 (libcontext swap_suspended guard), which this
pins. The v0.1.20 diagnostics decoded the crash that survived v0.1.13–19:
TOOL_MANAGER Resume()s a coroutine whose body is asyncify-parked inside
handleSleep, the swap rewinds the stale fiber suspension, and the runtime is
poisoned. Full chain of evidence in docs/features/async/16-fiber-resume-guard.md
(+ round-3 addendum in 15-timer-park-repro.md).
- wasm/bindings/fiber_park.h + kicadTestFiberPark{Start,Prime,Poke,State}
exports (pcbnew + merged kicad_editor): stages Call→yield→legitimate
resume→sleep park→mid-park Resume, the exact prod state machine. The
first yield matters: it primes a real (then stale) suspension, matching
long-lived tool loops rather than a first-slice park.
- tests/kicad/fiber-resume-park.spec.ts: asserts the healthy contract on
polled state only (embind returns across fiber swaps are unwind
placeholders). RED on the unguarded build — fiber/sleep buffer
cross-restores, a jump-ghost beacon, the parked body zombified. GREEN with
the guard: mid-park poke refused ([collab-fcontext] jump-refused beacon),
park completes, post-yield resume works, no trap signatures.
- Regression sweep green: timer-park-repro, collab-load-fuzz, load-pcb,
pcbnew-collab, collab-undo, eeschema-collab (19 passed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
kicadTestArmTimerPark(delayMs, parkMs): a one-shot wxTimer whose Notify()
emscripten_sleep()s, entering through the exact GAL-refresh-timer path
(emscripten_async_call → TimerCallbackFunc::Run → dispatch guard → Notify) —
the fresh-entry-that-parks the prod board-load trap family needs. Pollable
kicadTestTimerParkState(); inert unless armed. Registered beside
kicadTestSetOpenPark in pcbnew + the merged kicad_editor image.
tests/kicad/timer-park-repro.spec.ts drives four escalating cycles (park
only, 2× + fiber hammering, + 256MB heap growth mid-park) and asserts the
runtime survives every rewind AND that the [wx-asyncify] diagnostics observed
the window — engagement is asserted, so a run where the lever never created
the overlap cannot pass vacuously.
Result so far (docs/features/async/15-timer-park-repro.md): GREEN through
both rounds — genuine double-parks, live currData cross-restores, fiber
swaps, and mid-park heap growth are all handled by the shim + runtime. The
prod trap needs an ingredient this window still lacks (ranked in the doc);
the spec stays as the regression gate for whatever the eventual fix is.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
- kicadOpenFile now holds wxWasmDispatchGuard (open_gate.h). It enters through
embind, so the interlock read "nothing parked" for the whole load and wx timers
dispatched into the half-built board — the residual prod "index out of bounds"
that survived the settle gate.
- new wasm/bindings/gerbview_embind.cpp (the bundle had no embind surface at all):
kicadOpenFile / kicadOpenFiles / kicadOpenFileBusy. Clicking one gerber opens the
whole fabrication set in its folder, since a lone layer is not a useful view.
- cross-app presence rejoins in the boot fan-out (network-only; the wasm-bound half
still waits for the open to settle) — it had been pushed behind the board load.
- tests: gerber-set selection units + a gerbview multi-file open e2e.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0137pGo8W7asomGUTRMB7RzM
Opening a large board could kill the editor runtime outright: "index out of
bounds" / "unreachable executed" / "indirect call signature mismatch", after
which every later focus/key event trapped. Reported in prod against a big
uploaded board, and correlated by the reporter with the moment the presence
WebSocket connects.
That correlation is the tell. Presence/collab work enters the wasm through
runOnFiber -> COROUTINE::Call, i.e. emscripten_fiber_swap, whose stop_unwind
corrupts Asyncify's single currData slot when ANOTHER context is already
parked there. docs/features/async/13 pins the invariant: exactly one
unwind/rewind transition in flight, and prescribes "a single shared is-a-
transition-in-flight guard the pumps consult before re-driving".
Normally the slot is free when fibers drain: the main loop's per-frame
wxWasmYieldToBrowser completes every frame, so drainFibers runs between
yields. It is NOT free when a nested/modal pump tick drives ProcessEvents
while the chain that opened the modal is parked deeper down — precisely a big
board open (progress dialog over a parked load). The wx dispatch interlock
does not cover this: the modal parks deliberately zero its count so their own
pump may dispatch.
So gate the swap itself: drainFibers defers (re-CallAfter) while
asyncifyInFlight(). Bodies are viewport/overlay/apply work, so waiting out the
park costs latency, never correctness.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8jo7zz1ZwzYpjJ64UZKN4
Since comments-ux 0002, WasmTool re-sends the shell theme through
kicadSetColorTheme on every boot; setColorTheme ran CommonSettingsChanged
unconditionally, which recreates the menubar/toolbars — and they come back
SHOWN, undoing the kicadSetChrome(false) the read-only viewer and mobile
canvas-only mode applied moments earlier (CI: read-only-editor +
mobile-editor "9 visible menu titles").
Two-part fix in pcbjam_theme:
- early-out when the requested theme AND the wx dark-chrome flag are
already applied — the every-boot re-send (still needed for warm
relaunches with stale MEMFS settings) becomes a true no-op;
- after a REAL apply, re-assert the hidden chrome via a hook the merged
image installs (kicadSetChrome(false) when the snapshot says hidden),
queued with CallAfter from inside the fiber body so FIFO lands it
behind the CallAfter-deferred ReCreateMenuBar. Covers viewers/mobile
users toggling dark mode mid-session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y32etYBCKV6t1qDoLoGmgF
Two generic (manufacturer-agnostic) pcbnew-side subcommands for platform
manufacturing plugins: --ipc356 (IPC-D-356 netlist via IPC356D_WRITER) and
--fab-components (board metrics + per-footprint placement/BOM JSON —
absolute board coords, consumers apply their own conventions). Bumps the
kicad submodule for the diet keep-back of export_d356.cpp.
Also closes a CI cache gap: wasm-cache-hash never hashed wasm/**, so a
pure wasm/cli change would cache-hit stale output — { dir: "wasm" } is
now an input (busts the cache once on landing).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mr8PQ34yCfvEtEcwSUvLAV
Symbolized (HOIST_KEEP_NAMES=1): the trap is on the asyncify REWIND re-entering
the fiber — stack-local COROUTINE+body were destroyed when Call() returned
early on a park, so the rewind called through freed objects (latent UB in the
ORIGINAL fire-and-forget runOnFiber too). Heap-pinned FiberSlot + explicit
done flag + fiber-tail re-drain. Layer 2 (rewind interplay) still open —
fuzz stays fixme'd; pageerror stacks now captured in fuzz artifacts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5cAM9M6q34n5X4dbrfVvi
runOnFiber now runs bodies strictly one-at-a-time through a park-safe FIFO
(collab_common.h): the per-body fire-and-forget coroutine interleaved under
load — an asyncify park inside commit.Push let the event loop start the next
body, so a local commit and a remote apply ran interleaved on shared state
(s_applyingRemote is one global), silently losing applies on the actively-
editing receiver (fuzz finding #10a; B now fuzzes clean; 39-test suite green).
kicadCollabFiberBusy embind probe (merged + standalone registrations): a
bare-embind-stack scratch save during a parked fiber mis-dispatches (table
index OOB) — trio.ts modelText/drift and production drift-detect now defer
while fiber work is in flight (#10b hardening; the trap's root cause is still
open and needs a symbolized stack — fuzz stays fixme'd).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5cAM9M6q34n5X4dbrfVvi
drift-trio-scenarios.spec.ts: disjoint ping-pong, same-item interleave (no
settle between bursts), conflict pairs (move-vs-delete / value-vs-value /
move-vs-move — winner is CRDT policy, asserted only as convergence + drift
silence), undo storm, 12-edit burst churn, late-joiner adopt, and Ctrl+S mid
peer burst; per-tool adapters, marker-waits before every sweep (finding #7).
S4 exposed finding #9: mutation hooks resolved item pointers at call time and
committed later on the fiber — a remote remove in between frees the pointer
(doApplyItems) and the commit resurrects the deleted item. Phase-B mutation
hooks now re-resolve by uuid ON the fiber; a vanished item makes the mutation
lose silently. applyDeltaToY's concurrent update/delete verified coherent
(full resurrect or full remove) — no shared change needed. trio.ts: TabSet
oracles + exported startV2 for duo/late-join composition.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5cAM9M6q34n5X4dbrfVvi
17 kicadCollabTest* action primitives (eeschema: wire/junction/no-connect/
label/symbol/move/mirror/duplicate; pcbnew: track/via/text/zone/flip/
footprint-field/lock/move/duplicate), each a real SCH_/BOARD_COMMIT on the
fiber so the listener → flushDiff emit runs as for UI edits; tool-unique
names, merged-image safe. Fixes surfaced by the catalogs (0008 §10 #4–#8):
eeschema adds SetParent before staging (Push silently skips listener
notifications for unparented items), and pcbnew blobForItem now Formats
non-footprints with the FILE writer + wrapInBoardEnvelope — SaveSelection's
transfer copy cleared the locked flag, so (locked yes) never reached the doc.
drift-trio.spec.ts gains full A/B-alternating catalogs with per-step landed
gate + oracle sweep. Bumps kicad for the Duplicate child-uuid re-roll.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5cAM9M6q34n5X4dbrfVvi
The Y.Doc is the source of truth for the FILE, but both live wires wrote
KiCad's CLIPBOARD dialect — a lossy, paste-oriented format. Every
difference was permanent, unfixable drift.
- pcbnew: serialize footprint blobs with CTL_FOR_BOARD, not CLIPBOARD_IO's
CTL_FOR_CLIPBOARD, which emitted (version)(generator)(generator_version)
inside every (footprint …). Keep (locked yes).
- eeschema: aForClipboard=false — clipboard mode collapsed every symbol's
(instances … (path "/sheet")) to (path "").
- Re-supply (version) at PARSE time only (withFootprintVersion): the token
is invalid file content but load-bearing on decode — without it the
parser starts at m_requiredVersion=0 and stamps (hide yes) on every
mandatory field.
- FOOTPRINT copy ctor: restore mandatory-field uuids (EDA_ITEM::operator=
keeps the target's const m_Uuid, so Clone() rerolled all four).
- drift: classify order-only diffs as `reordered` — y-sexpr v2 reorders
legitimately; excluded from counts, report-worthiness and dedupe hashes.
Migration 0017.
- fpedit from eeschema: AsyncLoad()+BlockUntilLoaded() in initLibraryTree —
FACE_PCB starts lazily there and never preloaded its libraries.
Guards: wire-vs-file round-trip tests (pcbnew + eeschema),
fpedit-from-eeschema (verified red without the fix), symedit-from-eeschema.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016p9kjdGBdcpwUSjJ3q5xg2