From 125c3413ad6c3dcb9823d17c24f0da23d20bf277 Mon Sep 17 00:00:00 2001 From: Michael B Reiser Date: Fri, 11 Sep 2026 22:50:59 -0400 Subject: [PATCH 01/57] =?UTF-8?q?feat(studio):=20controller-fault=20detect?= =?UTF-8?q?ion,=20post-mortem=20probes,=20soak=20driver,=20GET=5FHEALTH=20?= =?UTF-8?q?decode=20=E2=80=94=20Studio=20v0.76=20(fw=20#50)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Mode-3 controller wedge (firmware #50) used to be invisible: the closed-loop drain loop swallowed every 0x70 timeout and a dead-arena run "completed" and auto-committed. Now: - fictrac-bridge-client: rolling fault detector (>=3 failed applies in the last 10, not "3 consecutive" — a late same-opcode reply can satisfy the next request) → latched `fault` event, apply forced OFF. - arena-runner: `fault(reason, detail)` wakes the trial wait and unwinds; `summary.fault` / `summary.faultDetail` / `summary.stopAcked`; a timed-out protocol command (the 0x08 after the 0x70s) is labelled the same. - arena-session: bridge fault → runner.fault + `fault` event; runner sends go through session.send (a link facade) so trialParams/STOP land in the run log; `reconnect()` / `flushRx()`; `dt` on the monotonic clock; terminal summary + fault detail in the sanitized runner rows. - arena-link: `reconnect()` from granted ports (navigator.serial.getPorts, VID/PID match, ambiguity refused, re-enumeration poll); the response deadline now covers a stalled write; `flushRx()`. - run-log / adapter: outcome CONTROLLER_FAULT (auto-commits; ABORTED_BY_USER still doesn't); `fault` phase in the transcript. - studio-postmortem (new, classic dual-export): quiet period + rx flush → confirmation 0xC2 → probe window (5 s timeouts, typed `probe` rows with decoded + raw hex) → halt | SYSTEM_RESET → reconnect → MAC verify → post-reset probe. - wire: GET_HEALTH 0xCA encode/decode (55 B + optional 11 B slowest-op tail), GET_FRAME_POSITION 0x72, capability bit 7 `health`. - Studio: fault banner; `runOnce()` (the no-dialog half of beginRun); soak driver (File ▾ → Soak…, `?soak=1`, advanced-only; refuses without a behavior_v2 ack or bridge frames; halt-first; never auto-commits); Debug ▾ → Controller health; URL-state `soak` flag; v0.76 footer. - scripts/wedge-scan.py + tests/test-wedge-scan.py: per-run wedge/onset table over v1/v2/gz/.runlog.json (found a sixth, earlier occurrence: rig2 2026-07-11). - fictrac_sim.py: --turn-sigma / --jump-every / --jump-deg. - protocols/soak_mode3_closed_loop.yaml; docs/development/mode3-wedge-soak-plan.md (findings, hypothesis ladder, campaign spec, bench recipe); release notes; CLAUDE.md rule. Tests: full `pixi run test` green (new: fault latch, runner fault, session facade + fault routing, link reconnect/write-deadline, wire GET_HEALTH vectors, adapter/run-log outcome, postmortem lifecycle, URL soak flag, wedge-scan). Not bench-tested. Codex (gpt-6-astra) plan review reconciled in .codex-review/report-20260911-wedge-soak.md (gitignored). Co-Authored-By: Claude Fable 5.1 --- CLAUDE.md | 20 + arena_studio.html | 285 ++++++- .../development/arena-studio-release-notes.md | 23 + docs/development/mode3-wedge-soak-plan.md | 172 ++++ fictrac-bridge/fictrac_sim.py | 47 +- js/arena-link.js | 141 +++- js/arena-runner-g6.js | 70 +- js/arena-session.js | 88 +- js/arena-wire-g6.js | 114 ++- js/fictrac-bridge-client.js | 64 +- js/run-log.js | 8 + js/studio-postmortem.js | 409 ++++++++++ js/studio-runlog-adapter.js | 11 +- js/studio-url-state.js | 13 + pixi.toml | 2 +- protocols/soak_mode3_closed_loop.yaml | 130 +++ scripts/wedge-scan.py | 769 ++++++++++++++++++ tests/test-arena-link.js | 96 +++ tests/test-arena-runner-g6.js | 109 +++ tests/test-arena-session.js | 103 +++ tests/test-arena-wire-g6.js | 99 +++ tests/test-fictrac-bridge-client.js | 96 +++ tests/test-run-log.js | 5 + tests/test-studio-postmortem.js | 349 ++++++++ tests/test-studio-runlog-adapter.js | 45 + tests/test-studio-url-state.js | 23 + tests/test-wedge-scan.py | 374 +++++++++ 27 files changed, 3634 insertions(+), 31 deletions(-) create mode 100644 docs/development/mode3-wedge-soak-plan.md create mode 100644 js/studio-postmortem.js create mode 100644 protocols/soak_mode3_closed_loop.yaml create mode 100644 scripts/wedge-scan.py create mode 100644 tests/test-studio-postmortem.js create mode 100644 tests/test-wedge-scan.py diff --git a/CLAUDE.md b/CLAUDE.md index b5d41ca3..08150834 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -262,6 +262,26 @@ fix flows to every page automatically; two hand-written HTML pages never will. equal to both the silkscreen number and the 0xAA wire channel (one number everywhere; `parseRigIo` rejects `port: 0` with a warning). Label new I/O UI with the silkscreen names, not DO1/DO2/J3/J4 refdes. +- **Controller-fault lifecycle (fw #50, v0.76) — THE fail-closed rule:** a Mode-3 controller + wedge is detected in `js/fictrac-bridge-client.js` (`_recordApply`: ≥ `faultThreshold` (3) failed + applies in the last `faultWindow` (10) — NOT "3 consecutive", a late same-opcode reply can satisfy + the next request) → latched `fault` event, apply forced OFF → `ArenaSession` calls + `runner.fault(reason, detail)` (wakes the wait, `summary.fault`, `summary.stopAcked`) → the + run's outcome is `CONTROLLER_FAULT` (`run-log.js` `deriveOutcome`, adapter) and it **auto-commits** + (only `ABORTED_BY_USER`/`DISCONNECTED` skip). A timed-out protocol command (`isTimeoutError`) is + labelled the same. After the run unwinds, `runSteps` (module block) calls + `Studio.handleControllerFault(summary)` → `js/studio-postmortem.js` (`createPostmortem().run`): + quiet ≥ 1 s + `session.flushRx()` → confirm 0xC2 (2 s) → probe window (5 s timeouts; typed + `probe` rows with decoded + raw hex, because `["a",…]` rows carry no payload) → policy `halt` | + `reset-continue` (0x01 → `session.reconnect()` via `ArenaLink.reconnect()`/`getPorts()`, VID/PID + match, ambiguity refused → MAC verified → post-reset probe). Runner sends go through + `session.send` (a link facade) so trialParams/STOP are logged. New wire commands used by probes + MUST be exported + golden-tested (`GET_HEALTH` 0xCA decodes 55 B + optional 11 B slowest-op tail; + `GET_FRAME_POSITION` 0x72). **Soak driver** (`Studio.startSoak`, File ▾ → Soak…, `?soak=1`, + advanced-only) loops `Studio.runOnce(false)` (the no-dialog half of `beginRun`), refuses without + a `behavior_v2` ack or without bridge frames, halts on the first fault by default, never + auto-commits. Analyzer: `scripts/wedge-scan.py` (+ `tests/test-wedge-scan.py`, standalone + harness, no pytest). Design + campaign spec: `docs/development/mode3-wedge-soak-plan.md`. - **Wire module exports:** `js/arena-wire-g6.js` defines more than it exports — when adding encoders/decoders, add them to the export list AND a test; audit with `Object.keys(require('./js/arena-wire-g6.js'))` vs the page's `Wire.*` diff --git a/arena_studio.html b/arena_studio.html index fba4fcd6..a6b08d28 100644 --- a/arena_studio.html +++ b/arena_studio.html @@ -251,6 +251,8 @@ body.safemode #ghLock{cursor:not-allowed;opacity:.6} /* Session-rig selector stays hard-locked (its unlock padlock is hidden). */ body.safemode #sessionRigLock{display:none} + body.safemode #fmSoak{display:none} + #fmSoak.soaking{color:var(--accent)} /* Pattern WRITES greyed in safe mode: the Add ▾ menu button has no data-cmd (its items do, and are in SAFE_BLOCKED_CMDS), so grey the parent so the menu can't be opened; the per-row × delete bypasses the dispatcher (guarded in JS) @@ -2130,6 +2132,7 @@
+
@@ -2658,6 +2661,8 @@

Import error

Send raw hex
+
Health
+
Danger
@@ -2953,7 +2958,7 @@

Import error