Skip to content

Studio: stop the run when the FicTrac bridge is lost (BRIDGE_LOST) + ?p= links never wait on GitHub (LAB-303 S1, S2) - #257

Open
mbreiser wants to merge 5 commits into
mainfrom
claude/hopeful-archimedes-1bab85
Open

mbreiser wants to merge 5 commits into
mainfrom
claude/hopeful-archimedes-1bab85

Conversation

@mbreiser

@mbreiser mbreiser commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes finding S1 from the LAB-303 Plan B Windows test on ww18 (2026-10-15, step 5.7): the FicTrac bridge was hard-killed 10 s into a closed-loop epoch, the Studio showed only "bridge disconnected" in the bridge panel, and the run carried on with no frames. Bridge 3.4 dying mid-run takes the same path. Now a bridge loss during a run is loud, and the run stops when it needs the bridge.

How it works (connection ownership stays in the classic substrate):

  • js/fictrac-bridge-client.js: an open socket that closes without disconnect() asking is a loss. The client fails closed first: apply goes off, the pending frame index is dropped, and every pending request settles at once through one _failPending('bridge lost'). Log export now rejects with "bridge lost" instead of waiting out its 15 s timeout, and level-ack waiters resolve to null. Only then does it emit 'lost' {code, apply, logFile, …}. disconnect() now clears _ws before close(), which is how a requested close is told apart from a loss; a superseded socket closing after a reconnect no longer clobbers the new one. New getters: logFile (from log_control_ack) and lastLoss.

  • js/arena-runner-g6.js: onBridgeLost(info) stops the run in three cases:

    • a closed loop was applying;
    • any condition has a bridge-routed plugin command (sequenceUsesBridge; today that is FicTrac). This catches a loss in an open-loop ITI before a closed-loop epoch.
    • the run's record lives on the bridge (runSequence({bridgeRequired}), which the Studio passes for recorded runs; those are already gated on the bridge at start).

    It stops through the ordinary stop(): the serial link is healthy, so the display gets a real STOP, the LED goes off and the loop is disarmed. It records summary.bridgeLost. This is not a controller fault, so no post-mortem runs. Otherwise a test run that needs nothing from the bridge continues and records a bridge-lost event.

  • js/arena-session.js: routes 'lost' to the runner and re-emits 'bridgelost' {info, action, running}. If a stale cached runner has no onBridgeLost, an active run is stopped with stop() instead.

  • js/run-log.js, js/studio-runlog-adapter.js: new outcome BRIDGE_LOST (CONTROLLER_FAULT still wins if both happen) and a transcript line ⚠ BRIDGE LOST (code 1006) — closed loop was running — run stopped.

  • arena_studio.html:

    • A persistent #runAlert (role="alert", ✕ to hide, cleared at the next run). The shared #banner auto-hides after 8 s, which is easy to miss at a rig.
    • BRIDGE_LOST runs are not auto-committed: the bridge that wrote the file is gone.
    • ⇪ Push log refuses a log_export whose filename is not this run's bridge file. A bridge restarted after a crash serves a different file, and before this change the push would have committed it under this run's name.
    • The soak ends with reason bridge-lost.
    • Cache tokens are bumped on the five changed scripts.
  • CLAUDE.md: a short "Bridge-loss lifecycle" rule next to the controller-fault one, and a "never await a remote call before the ?p= load" rule.

Item 3 of the brief (pending connector_commands fail fast). connector_command exists only on the spike #254 (4dd129f), not on main. As agreed, this PR provides the one fail-fast place (_failPending) and the bridge-dependence hook (sequenceUsesBridge). The ~4-line delta for #254 (settle connector waiters in _failPending, add the connector plugin names to sequenceUsesBridge, add a "stop the BIAS recording by hand" line to the alert, and a test) is written up in the LAB-303 session notes. The browser cannot reach BIAS without the bridge, so after a loss BIAS keeps recording whatever the runner does; the alert is where that gets said.

S2 (?p= link loaded only after a refresh): fixed (second commit). initFromUrl awaited registerRepoSharedPatternPreviews(), a GitHub Contents API listing of the course repo's patterns/ with no timeout, before it loaded ?p=. A course repo is configured by default, so this happened on every bench. Headless check with api.github.com held: the page was still empty after 9 s and loaded within 0.1 s once that request failed. (The registry fetch was already cache: 'no-store', so caching was not the cause.)

  • initFromUrl now starts the shared and colocated preview listings without awaiting them.
  • That is only safe because preview byte sources no longer rely on "last write wins". The new js/studio-preview-sources.js keeps them in layers (builtin < shared < colocated < uploaded) and every write names its layer. A late shared listing therefore can't override a protocol's own _patterns/; the replay path already had that race. When shared listings overlap, the newest request wins.
  • After the fix, with GitHub held: ?p= loads in 0.09 s, previews wire once the listing answers, and there are no console errors.

Found in passing (not touched): ArenaSession.runSequence does not forward degPerFrame or defaultCoupling to the runner (since #211). A legacy plugin-config gain: -1.8 protocol therefore runs at coupling +1 in the Studio. Proposed as its own follow-up.

Tier

Next candidate.

Release notes

Studio

  • Losing the FicTrac bridge mid-run now stops the run and says so. If the bridge closes during a closed-loop run, a run with FicTrac steps, or a recorded experiment, the run stops (the display gets STOP, the closed loop turns off). A red alert stays on screen until you dismiss it, and the run log records the outcome as BRIDGE_LOST. A test run that needs nothing from the bridge keeps going, and its log notes that bridge logging stopped.
  • A shared protocol link (?p=…) opens right away, even when GitHub is slow. It used to wait for the course repo's pattern list from GitHub and could stay empty until you refreshed. Pattern thumbnails still appear as soon as GitHub answers.
  • Run-log pushes can no longer commit the wrong file after a bridge restart. ⇪ Push log checks that the bridge serves this run's own log file and refuses otherwise. It tells you where the file is on the bridge PC.

Test plan

  • pixi run test: 61/61 files. New checks:
    • client 183 (loss vs requested close vs failed connect vs stale socket; fail-closed order; pending export rejects "bridge lost" in < 1 s)
    • runner 529 (stop mid epoch; a loss in an open-loop step before a closed-loop one; a recorded run; a test run that continues; sequenceUsesBridge)
    • session 106, including an end-to-end case: real FicTracBridgeClient (fake socket) + real ArenaRunner + ArenaSession + run log, socket killed mid closed-loop epoch → BRIDGE_LOST in < 2 s, pending export fails fast, STOP sent
    • run-log 39, adapter 34
    • new test-studio-preview-sources 23: precedence in all 24 arrival orders, plus a guard that only same-origin calls are awaited before the ?p= load. It fails on main's Studio, as it should.
  • pixi run studio-smoke: 0 failures (layout at all widths, 5 themes + axe, replay interlock).
  • Headless check of the alert in all 5 themes: shown, axe contrast clean, ✕ hides it. The 390 px overflow is unchanged (pre-existing).
  • Headless S2 check with api.github.com held: ?p= and ?p=&rig=&mode= load in < 0.1 s; preview sources wire once GitHub answers.
  • Bench, on Next: open a ?p= link on the rig PC (should load at once). Start a closed-loop run, taskkill /F the bridge mid-epoch. Expect the red alert, the run to stop with outcome BRIDGE_LOST, the display to stop, and ⇪ Push log to refuse after a bridge restart.

🤖 Generated with Claude Code

mbreiser and others added 2 commits October 15, 2026 16:41
…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 <noreply@anthropic.com>
ww18 2026-10-15: a tab opened at ?p=<key> stayed empty until a refresh. initFromUrl
AWAITED the course repo's GitHub patterns/ listing (registerRepoSharedPatternPreviews,
no timeout; a course repo is configured by default) before loading the protocol.
Reproduced headless: with api.github.com held the page was still empty at 9 s and
loaded 0.1 s after that request failed.

- initFromUrl starts the shared + colocated preview listings without awaiting them.
- That is only safe because preview byte sources no longer rely on "last write
  wins": new js/studio-preview-sources.js keeps them in layers (builtin < shared <
  colocated < uploaded) and every write names its layer, so a late shared listing
  can no longer override a protocol's colocated patterns (the replay path already
  had that race). Overlapping shared listings: the newest request wins.
- tests/test-studio-preview-sources.js: precedence in all 24 arrival orders, layer
  replacement, Map-like reads, and source guards — everything initFromUrl awaits
  before the ?p= load is allowlisted (fails on main's Studio, as it should).
  test-studio-github.js: the lazy-token check follows the new source forms.

Headless (GitHub held): ?p= loads in 0.09 s; previews wire once the listing answers;
no console errors. Suite 61/61.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mbreiser mbreiser changed the title Studio: stop the run when the FicTrac bridge is lost mid-run (BRIDGE_LOST, LAB-303 S1) Studio: stop the run when the FicTrac bridge is lost (BRIDGE_LOST) + ?p= links never wait on GitHub (LAB-303 S1, S2) Oct 15, 2026
…3 S1)

- bridgeRequired only for a recorded run the bridge is actually logging
  (bridge.logging), not merely connected for the scope.
- A log_control_ack slower than the 800 ms wait still names the run's bridge
  file: the run subscribes to 'loglevel' once and takes `file` for THIS run.
- disconnect() and a connect() over a still-closing socket settle every
  pending request (export, level ack) themselves; the superseded socket's
  late onclose is ignored, so it must not be the only place they settle.
- The bridge-lost alert no longer hardcodes `pixi run bridge` (wrong on the
  ROS 2 graph): "restart the bridge program on the rig PC".

Tests: client 187 (two new reconnect cases), studio-shell 81 (wiring checks).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@mbreiser

Copy link
Copy Markdown
Contributor Author

Review fixes pushed (0b1d212), from the 2026-10-17 code review of this PR:

  • bridgeRequired only when the bridge is logging the run (bridge.logging), not merely connected for the scope.
  • A log_control_ack slower than the 800 ms wait still names the run's bridge file (one-shot loglevel subscription for this run), so the push check cannot be silently skipped.
  • disconnect() and a connect() over a still-closing socket settle pending requests themselves (export, level ack); the superseded socket's late onclose is ignored, so it must not be the only place they settle.
  • The bridge-lost alert no longer hardcodes pixi run bridge (wrong on the ROS 2 graph).

Tests: client 187/187 (two new reconnect cases), studio-shell 81/81, full suite 61/61 files. This PR is constituent 1 of candidate 2026-10-20 (with #256 and #254, which rebases onto it). LAB-303.

mbreiser added a commit that referenced this pull request Oct 18, 2026
… this branch's own S1/S2

#257's bridge-loss model replaces 484b187's: the client emits 'lost' only for an
OPEN socket closing unasked (failed connects and superseded sockets are not
losses), fails every pending request first, and the RUNNER decides via
onBridgeLost (apply on / bridge-routed steps / bridgeRequired). The runner's
bridgeInUse getter and the 'disconnect'-involuntary path are removed; so is the
fault('bridge_lost') outcome path (BRIDGE_LOST now comes from summary.bridgeLost).

Connector delta on top of #257 (its PR body's "item 3"):
- _failPending settles connector_command waiters (outcome 'unknown');
- sequenceUsesBridge counts connector plugin instances (BiasPlugin, ScanImagePlugin)
  as bridge-routed steps, so a loss in an open-loop ITI before a camera step stops the run;
- the bridge-lost alert says devices driven through the bridge were not told to stop.

Also: the run-end bag close in onRunFinished referenced an undeclared `bridge`
(a ReferenceError swallowed by its catch — the fix was a no-op); it now uses
Studio.session.bridge, closes before the upload, and only for this run's log.
initFromUrl takes #257's fire-and-forget listings over the grace-period race;
patByteSource keeps rc5's background-lane version with the layered preview map.

Tests: root suite 70/70 files (runner 544, client 199, session 106, adapter 34,
run-log 39, studio-shell 82, preview-sources 23).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…edes-1bab85

# Conflicts:
#	arena_studio.html
@mbreiser

Copy link
Copy Markdown
Contributor Author

Not a separate constituent of candidate 2026-10-20: this branch conflicts with main after release 2026-10-06 (the rc5 patByteSource hunk in arena_studio.html), and every commit here is already contained in #254 (0b1d212 is an ancestor of its head), which merges clean. The release notes above are folded into #254's body, so they reach the release commit. This PR will be marked merged when the release lands. LAB-303.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant