From 930126b35c37ceca546475664806db71ffcc122b Mon Sep 17 00:00:00 2001 From: Michael B Reiser Date: Thu, 15 Oct 2026 16:41:04 -0400 Subject: [PATCH 1/3] Studio: stop the run when the FicTrac bridge is lost mid-run (BRIDGE_LOST, LAB-303 S1) ww18 2026-10-15 (step 5.7): the bridge was hard-killed 10 s into a closed-loop epoch. The Studio only showed "bridge disconnected"; the run carried on with no frames. - fictrac-bridge-client.js: an OPEN socket that closes without disconnect() asking is a loss. Fail closed first (apply off, pending index dropped, every pending request settled at once via _failPending: log export rejects "bridge lost", level ack waiters resolve null), then emit 'lost' {code, apply, logFile, ...}. disconnect() clears _ws before close() so a requested close is never a loss; a superseded socket closing after a reconnect no longer clobbers the new one. New logFile (log_control_ack `file`) and lastLoss getters. - arena-runner-g6.js: onBridgeLost(info) stops the run when a closed loop was applying, when a step acts through the bridge (sequenceUsesBridge), or when the run's record lives on it (runSequence {bridgeRequired}); otherwise a test run continues with a bridge-lost event. The stop is the ordinary stop() (the serial link is fine: real STOP, LED off, loop disarmed); summary.bridgeLost, not a fault. - arena-session.js: routes 'lost' to the runner, re-emits 'bridgelost' {info, action, running}; stale-cache runner fallback = stop(). - run-log.js + studio-runlog-adapter.js: outcome BRIDGE_LOST, bridge-lost transcript line. - arena_studio.html: persistent #runAlert (role=alert), recorded runs pass bridgeRequired, BRIDGE_LOST is not auto-committed, a push refuses a log_export whose name is not this run's bridge file (a restarted bridge serves another file), soak ends with reason bridge-lost; cache tokens bumped on the changed scripts. Tests: client 183, runner 529, session 106 (incl. end to end: real client + runner + session + run log, socket killed mid epoch -> BRIDGE_LOST in < 2 s, pending export fails fast, STOP sent), run-log 39, adapter 34; suite 60/60; studio-smoke 0 failures. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 13 ++ arena_studio.html | 96 ++++++++++-- js/arena-runner-g6.js | 124 +++++++++++++++ js/arena-session.js | 36 ++++- js/fictrac-bridge-client.js | 105 +++++++++++-- js/run-log.js | 13 ++ js/studio-runlog-adapter.js | 10 +- tests/test-arena-runner-g6.js | 204 +++++++++++++++++++++++++ tests/test-arena-session.js | 229 ++++++++++++++++++++++++++++ tests/test-fictrac-bridge-client.js | 129 +++++++++++++++- tests/test-run-log.js | 10 ++ tests/test-studio-runlog-adapter.js | 39 +++++ 12 files changed, 981 insertions(+), 27 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 24d2f417..b9225d6b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -342,6 +342,19 @@ fix flows to every page automatically; two hand-written HTML pages never will. `write_rows`, verbatim `["cc"|"cf"|"cs", …]`). The record schema lives in ONE place — the header comment of `js/arena-telemetry.js` — mirror any firmware change there + in `wedge-scan.py` (`CTL_TAGS`/`CTL_STATE_KINDS`). Never drain while the post-mortem owns the link (its `canPoll`). +- **Bridge-loss lifecycle (LAB-303 S1, ww18 2026-10-15) — the other fail-closed rule:** an OPEN + bridge socket that closes without `disconnect()` asking is a LOSS (`disconnect()` clears `_ws` + BEFORE `close()` — that is the discriminator; keep it). `FicTracBridgeClient` then forces apply + OFF, calls `_failPending('bridge lost')` (THE one place every request/reply waiter settles — + register any NEW request type there, e.g. #254's connector_command waiters) and only then emits + `'lost'`. `ArenaSession` → `runner.onBridgeLost(info)`: it STOPS the run when apply was on, when + `sequenceUsesBridge` finds a bridge-routed plugin command (today `fictracPluginNames`; add any new + bridge-routed plugin set there), or with `runSequence({bridgeRequired})` (the Studio passes it for + recorded runs) — via the ordinary `stop()` (serial link is fine → real STOP), never a controller + fault (no post-mortem). Otherwise a test run continues with a `bridge-lost` event. Outcome + `BRIDGE_LOST` (`summary.bridgeLost`; `run-log.js` + adapter) is NOT auto-committed, and a push + refuses a `log_export` whose name ≠ the run's `log.bridgeFile` (a restarted bridge serves another + file). Studio: persistent `#runAlert` (`Studio.showRunAlert`), not the auto-hiding `#banner`. - **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 0cd898fb..76753923 100644 --- a/arena_studio.html +++ b/arena_studio.html @@ -813,6 +813,11 @@ #ghQuickLinks .ql-link:hover{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 8%,transparent)} .banner{background:color-mix(in srgb,var(--warn) 12%,transparent);border:1px solid var(--warn);color:var(--warn);border-radius:8px;padding:8px 12px;margin:10px 20px;font-size:12px;display:none} .banner.show{display:block} + /* Run alert: a safety event the operator must SEE (bridge lost mid-run) — stays until + dismissed or the next run starts, unlike #banner, which later messages replace. */ + .run-alert{background:color-mix(in srgb,var(--danger) 12%,transparent);border-color:var(--danger);color:var(--danger);font-weight:600;align-items:center;gap:12px} + .run-alert.show{display:flex} + .run-alert button{margin-left:auto;flex:0 0 auto;background:transparent;border:1px solid currentColor;color:inherit;border-radius:5px;padding:1px 8px;cursor:pointer;font:inherit} /* ── Edit view: tab bar (Designer | YAML) + YAML pane ───────────── */ .edit-view .edit-tabs{display:flex;border:1px solid var(--border);border-radius:5px;overflow:hidden;flex:0 0 auto} @@ -2472,6 +2477,7 @@ +