Skip to content

Release 2026-10-20: Arena Studio v0.97 - #262

Draft
mbreiser wants to merge 84 commits into
mainfrom
release/2026-10-20
Draft

mbreiser wants to merge 84 commits into
mainfrom
release/2026-10-20

Conversation

@mbreiser

Copy link
Copy Markdown
Contributor

Release candidate 2026-10-20 rc1 · candidate commit 8cd0148d6b · built on main 1d9b2b7750

Live on Next: https://reiserlab.github.io/webDisplayTools/next/

Tool Version
Arena Studio v0.95 → v0.97
PR Title Head CI Release notes
#256 fix(bridge): precise run-log clock on Windows with Python ≤ 3.12 7327a02524 green Arena Studio
#254 LAB-303: ROS 2 bridge graph + MCAP record, ScanImage/BIAS connectors, Studio v0.97 (candidate 2026-10-20) 6903f8a9e0 green Arena Studio

Promotion checklist

  • CI green on this PR and on every constituent PR (pixi run release checks)
  • Validated on a rig between sessions — comment Validated 8cd0148 … (pixi run tiers -- validate --rig … --by …). Any change to a constituent PR = rebuild (rc+1) = validate again.
  • Release window: Slack heads-up posted; nobody is mid-experiment (release lists recent rig runs — advisory)
  • After merge: rigs hard-refresh between sessions; if fictrac-bridge/ changed, rig PCs git pull + restart the bridge

Promote with pixi run release (merges with a merge commit — never squash — and tags release-2026-10-20). Manifest: releases/2026-10-20.json.

🤖 Generated with Claude Code release tooling (scripts/tiers/candidate.py)

cshl-2026 and others added 30 commits October 14, 2026 19:11
…ters, bridge node reusing bridge.py

Separate pixi workspace (RoboStack Jazzy + conda-forge; osx-arm64/win-64/linux-64) under
ros2-poc/; the root pixi.toml and fictrac-bridge/ are untouched so bridge 3.4 stays side by side.

- Zenoh router + rmw_zenoh_cpp with loopback-only configs (stock files, endpoint lines only)
- fictrac_udp_adapter: FicTrac UDP → /fictrac/raw (std_msgs/String JSON), FicTrac untouched
- kinefly_udp_adapter + kinefly_sim: KF/1 (mbreiser/Kinefly-MacOS) → /kinefly/raw
- bias_node + bias_sim + bias_cmd: BIAS HTTP command port ⇄ /bias/cmd,/bias/reply,/bias/status;
  timeout = unknown outcome + reconcile; video file reported as an artifact
- bridge_node: bridge.py's Hub/LogWriter/Pipeline/make_dispatcher on ROS input, same WebSocket
  schema on ws://localhost:8765, same behavior_v2 log; adds connector_status latency lines,
  source_lost/resumed watchdog, ["kf",…] rows, BIAS connector events, /arena/frame mirror
- rig.py: one `pixi run rig` command supervising router + nodes
- tests: studio_emulator (the Studio runner's exact bridge conversation), check_runlog_readers
  (JS readers accept the log), test_ros2_poc (29 checks incl. adapter SIGKILL mid-run,
  closed-loop report rejection/idx checks, convert round trip): 29/29 on Linux

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KHyggowbCA5ucBg71bU26V
…outer restart, launcher pre-flight)

- bridge_node: RosLogWriter hooks (on_open/on_close) replace the message re-parsing wrapper;
  source watchdog scoped per log file; ordered ROS shutdown (join the spin thread before
  destroy_node — removes "terminate called without an active exception" on SIGTERM)
- bias_node: worker exception guard (a malformed BIAS body no longer kills the node),
  get-status polls coalesced to one pending
- rig.py: port pre-flight with a one-line culprit per port (SO_REUSEADDR probe), router death
  stops the rig, Zenoh config paths set explicitly (Windows shells leave $PIXI_PROJECT_ROOT literal)
- nodes/_common.py: shared envelope, QoS, spin/shutdown boilerplate, wait_port
- test harness: adapter kill anchored on the runner's fictracApply line in the log; export guard
- README: soak (12 runs, flat RSS, p99 ≈ 3.5 ms), zero loss at 200 Hz + 500 Hz, router
  kill/restart with no data gap, student-mistake messages

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KHyggowbCA5ucBg71bU26V
… TerminateProcess)

Python cannot send SIGINT to another process on Windows (send_signal raises ValueError), so
Ctrl-C on the rig PC would have looked like a launcher crash. popen_group/signal_group in
nodes/_common.py give every child its own process group on both platforms and map
SIGINT → CTRL_BREAK_EVENT, SIGTERM/SIGKILL → TerminateProcess on Windows; rig.py and the
test harness use them (and catch ValueError). Linux path unchanged (setsid + killpg).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KHyggowbCA5ucBg71bU26V
…s a watch item)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KHyggowbCA5ucBg71bU26V
Mac run of the items the Linux container could not cover: pixi install 25 s
fresh, talker/listener, 29/29 acceptance, the unmodified Pages Studio against
`pixi run rig -- --sim` with a G6 controller (two Mode-3 constant-bias runs
auto-committed to the course repo, byte-identical to the bridge node's file,
idx mismatch 0.00 %, runlog-check OK), the real BIAS Mac port and the real
Kinefly fork (9600/9600 datagrams, 0 rejected, kf rows in the committed log).

bias_node: the real Mac port resolves a relative `filename` against its own
cwd and start-capture then succeeds without capturing; the reply now carries
a warning for relative names.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
WINDOWS-TEST.md scripts the one open acceptance item (install timing, the
$PIXI_PROJECT_ROOT expansion check, talker/listener, the 29-check harness,
rig + unmodified Studio + controller, real BIAS over HTTP with an absolute
video path and the unknown-outcome path, Ctrl-C cleanup, student mistakes)
and lists what to send back.

Windows' signal module has no SIGKILL, so rig.py's stop escalation, the
harness's mid-run adapter kill and _common.signal_group would have raised
AttributeError there; they now share _common.SIGKILL (getattr fallback 9).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ectors)

The web runner executed only the `log` and FicTrac plugins; every other plugin
command was a visible skip, so a protocol's `camera` plugin (BiasPlugin) never
recorded in a web run. Now:

- js/arena-runner-g6.js: a `connectorCommand` op for plugins named in
  `connectorPluginNames` (instance name → connector id, 'bias' for BiasPlugin).
  The step waits for the bridge's reply; ok → command event with the reply
  (outcome, value, artifact, rtt); error/unknown → step error, run continues
  (proceed-and-skip). A bridge that advertises no connectors (every ≤ 3.4
  bridge) yields a skip that names the bridge version. listSkippedPlugins
  takes an optional driveable set so the Edit-view note counts real skips.
- js/fictrac-bridge-client.js: connectorCommand() → {type:'connector_command'}
  and the id-matched connector_reply; never rejects (not connected → error,
  no reply in time → 'unknown', disconnect in flight → 'unknown'). hello_ack's
  `connectors` list → supportsConnector(). js/arena-session.js passes the map.
- arena_studio.html: connectorPluginNames(exp) from BiasPlugin instances,
  passed to runSequence; Run log lines show outcome · rtt · artifact; footer
  v0.95; release notes.
- ros2-poc bridge_node.py: hello_ack gains `connectors: ['bias']`;
  connector_command → /bias/cmd, the matching /bias/reply → connector_reply to
  the asking websocket; both halves logged (`via: ws` on Studio-path replies).
  bias_node.py: --video-dir resolves a relative filename (MATLAB's
  experiment-folder behavior). Emulator drives connect/startRecording/
  getTimestamp/stopRecording/bogus through the bridge; harness 32/32.

Tests: runner 512/512, bridge client 173/173, root suite 60/60 files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…95 connector path)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…erter, ROS Studio tier (LAB-303)

Frank's Plan B built as a bounded spike on the existing ROS 2 PoC graph, so the LAB-303
architecture decision can be made on measured data (ros2-poc/README.md → "Plan B spike").
Bridge 3.4 stays installed side by side; the browser protocol is unchanged.

* nodes/recorder.py + bridge_node.py: rosbag2_py records every topic per run into an MCAP
  bag — device topics as received, /arena/frame with the closed-loop node's own sample time,
  /arena/browser (every browser message, verbatim), /arena/event, /arena/session — each
  bridge-side record with the writer's emission sequence number. Profiles: crashsafe
  (default; no chunking, no cache) and rosbag2's default (one unflushed 768 KiB chunk).
* nodes/mcap_convert.py (`pixi run convert`): bag → behavior_v2 / behavior_v1 / full NDJSON,
  pure Python (mcap + a schema-driven CDR decoder, no ROS process), `--recover` for a torn
  bag, `--order seq|time`. log_export returns the converted log; the live NDJSON stays as a
  shadow during the spike and `converted == live` is checked at every export.
* interfaces/arena_interfaces (FicTracFrame, ArenaFrame, ConnectorReply) built by
  `pixi install` through pixi-build-ros; `pixi run build-interfaces` = the colcon alternative
  (install rpath fix for macOS). The adapter publishes /fictrac/frame typed; the bridge
  consumes it and publishes/records /arena/frame typed when the package imports.
* tests/test_ros2_poc.py: judged against the CONVERTED log; + bag per topic, fidelity vs the
  shadow (sequenced lines byte-identical; derived lines ±2 ms), levels, ordering, typed
  schemas, and a SIGKILL crash phase for both profiles. 46 / 46 on the Mac.
* ROS Studio Pages path: scripts/tiers/stamp.py --ros REF → /ros/ (channel "ros", magenta
  banner), deploy-pages.yml input `ros` (off by default), `pixi run ros-preview`,
  js/build-channel.js channel "ros" (+ badge, window-name suffix, tests). Production and
  Next untouched.
* protocols/bias_camera_closed_loop_poc.yaml (+ index entry): the bench protocol.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…roller + real BIAS through the MCAP-recording graph)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…client, graph tool, explainer

For tomorrow's rig test (WINDOWS-TEST.md rewritten around Studio + BIAS + bag logging):
* `typed` is now an OPT-IN pixi environment (`pixi install -e typed`): the default
  environment is String-only and always installs, so a rig PC without MSVC is never blocked;
  the interfaces build is measured separately. Nodes choose typed topics only after an
  import + serialize probe (`_common.probe_interfaces`): an importable package whose
  typesupport did not load had killed the bridge node at its first frame.
* `pixi run export` (nodes/export_log.py): the Studio's own log_export from the command line
  — closes the run's bag and writes the converted log — for rigs without a course token.
* `viz` environment (rqt_graph, rqt_topic, rqt_console, foxglove_bridge on :8766) and
  `pixi run graph` (nodes/graph_dump.py): the live graph as Mermaid / DOT / a table with rates.
* One clock sample for a connector command's browser record and its event (a 1 ms boundary
  had broken byte identity on the String path); harness derived-line gate on p99/worst.
* docs/plan-b-explainer.html + screenshots: where the PoC stands, for the lab.
Harness: default env 45/45, typed env 46/46.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…, raw browser/event/BIAS views) + doc pointers

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d at Recorder.close (mcap_index.py, pixi run reindex); Foxglove layout one plot per quantity

Bench bag 6.76 MB → 2.33 MB in 0.18 s, converter output identical, verified by re-read; harness 45/45.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…plainer; epoch label (two tares per trial)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… 25/50/100 % duty, BIAS ufmf) + its four generated patterns; Windows plan step 5c

Modeled on the lab's G4.1 Protocols 11/13 (open-loop rotating gratings CW/CCW, 20 s trials,
camera timestamp per trial, one video per run) for the four-color G6 2x10 arena and the web
runner: patterns from scripts/make-color-optomotor-patterns.js light only one color bank's
pixels on the top panel row (20 px / 36° period, 20 frames, GS2; 500 px lit per frame,
verified by decode); brightness by the per-trial duty override. Validates clean
(protocol-yaml checker: waits rule satisfied).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rived turning_dps / forward_mms in ArenaFrame

- nodes/rig.py: `--foxglove` runs foxglove_bridge (ws://127.0.0.1:8766) from the `viz` pixi
  environment as a supervised child after the bridge node; one note and no bridge when
  `.pixi/envs/viz` is not installed; tcp 8766 joins the pre-flight port check when set.
- ArenaFrame.msg + bridge_node.py: two derived channels for live viewing only, `turning_dps`
  (deg/s) and `forward_mms` (mm/s, `--ball-mm` 9 default), computed per consecutive frame the
  way js/kinematics.js does (dt on the FicTrac camera clock `ft`, `t` fallback; shortest
  heading step; x/y step projected on the mid-step heading × ball radius; NaN on a run's first
  frame). ONE builder (arena_frame_msg) for the published and the recorded frame. The hub
  message to the Studio and the behavior_v1 columns are unchanged.
- nodes/_common.py: probe_interfaces tries the environment's own interfaces package before a
  colcon `interfaces/install` prefix. Found while validating: the stale colcon prefix shadowed
  the pixi build's Python class and C struct while the env's newer typesupport did the CDR
  encoding — recorded frames carried garbage in the new fields with the harness green.
- docs/foxglove-arena-layout.json: third plot (turning + forward); README graph table, typed
  row, "What broke" item 8; WINDOWS-TEST step 5b.

Validated on the Mac: `pixi install -e typed` rebuilds the package (28 s); `pixi run -e typed
test` 46/46 and `pixi run test` 45/45; `pixi run rig -- --sim --foxglove` → `pixi run graph --
--text` lists /foxglove_bridge, :8766 listening, a Studio-emulator run records 576 typed frames
(1 NaN, hand-check matches), SIGINT stops everything with no rmw_zenohd / foxglove_bridge left.
The Python deriver matches kinematics.js to 1e-13 on 200 synthetic frames.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…er pattern, duty 100 %, BIAS bracket) for rig checks

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ile-upload fixes, PRs #245 #246 #248 #250) into the Plan B spike branch

Conflicts: the Studio footer (both sides v0.95 — the release's line wins) and the release-notes
top entry (the release's entry wins; the connector notes live in PR #254's body). The LAB-303
connector path (hello_ack connectors, connector_command/_reply) and the ros channel badge are kept.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
New check in tests/test_ros2_poc.py (typed environment only; reported as info when the package
is not importable): the recorded /arena/frame messages are decoded with the converter's
pure-Python CDR decoder from the schema text IN THE BAG, then x/y/hd/ft must be finite, fc
monotonic, turning_dps/forward_mms NaN on the run's first frame and finite within the model
fly's range (|turning| < 5000 deg/s, |forward| < 200 mm/s) on at least 95 % of the rest.

Why: on 2026-10-14 a stale colcon prefix paired the old Python class and C struct with the
environment's newer typesupport; the bridge recorded 108-byte frames with garbage in the two
new fields and the harness stayed green because its typed check compared schema names only.
Dry run of the helper: tonight's good bag → ok (576 frames, 1 NaN, |turning| ≤ 423 deg/s,
|forward| ≤ 21.7 mm/s); the garbage bag from the shadowed build → fails (no derived fields in
the bag's schema).

The full `pixi run -e typed test` / `pixi run test` runs are DEFERRED: a bench smoke run owns
the rig's ports on this Mac right now; expected 47/47 and 45/45 once it is free.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…o through the graph + real BIAS: 17/17, duty on the wire, runlog-check OK); findings

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ve Foxglove path, Next merge, open items

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e script that builds it

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…for the rig-PC session

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ia winget, clone, then the plan)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ore step 4

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pixi 0.80 on win-64 leaves `$PIXI_PROJECT_ROOT` literal in [activation.env]
(its activation is a cmd.exe script), so `pixi run router` failed with
`Failed to read config from $PIXI_PROJECT_ROOT/config/zenoh-router.json5`.
`%PIXI_PROJECT_ROOT%` expands under `pixi run`; a [target.win-64.activation.env]
table overrides the two keys on Windows only. Lock file unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…env's node.exe

Step 3 on ww18 (Windows 11, pixi 0.80) failed 2 byte-identity checks:
`bridge.py --convert v2->v1->v2 byte-identical` and `bag -> behavior_v1 ==
bridge.py --convert of the behavior_v2 (byte-identical)`. Text-mode writes
(`write_text` / `open(..., w)`) translate \n to CRLF on Windows, while bridge
3.4's --convert writes binary LF. mcap_convert --out, export_log and the Studio
emulator now write newline=\n (the bytes the bridge sends). The live shadow
file stays CRLF: it is bridge 3.4's LogWriter (fictrac-bridge/bridge.py, not
touched); its own export re-reads in text mode, so committed logs are LF.

The JS reader check was skipped: find_node() looked for
.pixi/envs/default/bin/node.exe, but conda's Windows layout has node.exe at the
env root. It now checks both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…topped

ww18 run 1: `conversion fidelity: derived lines ... FAIL — live 2238 vs conv
2239`. Kinefly row seq 2370 reached the recorder in the same ms as the stop
record (rel 22065 both); the live writer's callback ran after close, so the
shadow file ends at logging_stopped, while the converter's bisect_right put the
row AFTER logging_stopped on the tie. Derived lines placed after the last
logging_stopped are now dropped (report key `dropped_after_stop`). Same bag
re-converted: derived 2238 == 2238, last line logging_stopped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t on Windows

Ctrl-C on `pixi run rig` (ww18): every child exited, but the bridge node was
TERMINATED, not stopped: no `shutting down` line, the shadow file ended on a
FicTrac row (no logging_stopped) and the open run's bag got no metadata.yaml
(649 KB unindexed). rig.py stops children with CTRL_BREAK_EVENT, whose default
action ends a Python process at once, and loop.add_signal_handler is not
implemented on Windows. bridge_node now handles SIGBREAK by setting the same
stop event. After: `[bridge-node] shutting down`, `[record] closed ... 973
records, 95255 B`, metadata.yaml written, logging_stopped last, nothing lingers.

Second `pixi run rig`: the refusal listed 8765 and 60000 but not 7447 while a
router was listening. port_in_use() sets SO_REUSEADDR, which on Windows lets the
probe bind OVER a live listener (zenohd; asyncio's socket is exclusive, so 8765
was caught). Plain bind on Windows; TIME_WAIT does not block it (checked).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
First Windows run (reiserlab-ww18, pixi 0.80, VS Build Tools 2026, the
atlas-bench Atlas10 BIAS build). Harness 45/45 after four small Windows fixes
(bd2e349, aeccdce, 6250cdb, 4935732); one cross-platform start-of-run race left
open (X2); the typed build needs VS 2022 / the v143 toolset (W2). Steps 4, 5,
5c and 7 still need someone at the rig.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mbreiser and others added 29 commits October 16, 2026 11:34
…nector node (allowlisted get/set, grab/waitIdle/abort, UNKNOWN on timeout + reconnect), scanimage_cmd.py, si_json_mock.py (stock B2 behavior), pixi tasks

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…R_TOPICS table, hello_ack advertises bias + scanimage), converter derives /scanimage/reply|status events, rig --scanimage/--si-mock; harness drives the 2P sequence through connector_command incl. the UNKNOWN path (52/52)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…allowlisted set, grab, waitIdle, abort) → bridge 'scanimage' connector; the run file (and MCAP bag) closes at run end via log_control off; footer + release notes

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…color, 6 duty levels × 3 s at 20 fps, controller settings + ScanImage setup in the header

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…server (same condition table and TIFF layout; stdlib only, --dry-run/--ping)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tterns.js --gs16; no refresh-rate confound vs the 22 Sept GS16 runs), protocol points at them; SCANIMAGE-DAY.md rewritten for the JSON-server connector, the built/tested state, the rig test plan (new-arena trigger check, 20 panels, Inventory after power cycles) and what still needs the rig

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… setup, Block Z deferred, fallback trigger at 10 min)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n week, item 1)

A bag whose writer was killed (no metadata.yaml; a torn tail) no longer needs
`--recover` by hand. mcap_index.recover_bag rewrites the .mcap whole + indexed
(torn tail dropped, original kept as .torn.bak), appends one /arena/session
{"event":"recovered"} record so the bag itself says so, and asks rosbag2's own
Reindexer for metadata.yaml (verified: it reads the crashsafe file in file
order). The bridge node runs it when a log_export meets such a bag; a freshly
started node adopts the newest unclosed bag in --log-dir for its first export,
so the Studio's export after a bridge restart gets the salvaged run. The
log_export_result carries `recovered: true` + the recovery report (and the
conversion report); the converter emits the marker as a session line; a bag
with nothing salvageable (rosbag2 defaults leave a 0-byte file) falls back to
the shadow NDJSON. `pixi run export -- --json`, `pixi run reindex -- BAG --recover`.

Harness crash phase: a restarted node's log_export recovers the bag on its own
(frames == the --recover CLI's, marker present) and leaves a whole bag
(metadata.yaml, chunk index, converts without --recover). 54 / 54.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…-303 adoption week, item 2)

Decision note docs/typed-distribution-decision.md: per-PC MSVC build (35 s + v143 +
detached-environments + an unmerged upstream fix on every PC) vs a CI-built conda
channel (5–9 s, no compiler, one sha256-pinned build so rmw_zenoh type hashes agree).
Recommendation: the channel; host = the in-repo channel/ read over raw.githubusercontent.com
at an arena-interfaces-v* TAG (T6 measured), moving to a prefix.dev lab channel when the
lab owns one. The per-PC build stays the .msg author's path.

Repo side: .github/workflows/arena-interfaces.yml builds win-64 / osx-arm64 / linux-64
(windows-2022 → v143; detached-environments C:\px for MAX_PATH; pixi v0.80.0 pinned
because the backend is not in the lock), indexes the three with rattler-index and
publishes the `arena-interfaces-channel` artifact; on a tag it also uploads to prefix.dev
when ARENA_INTERFACES_CHANNEL / PREFIX_API_KEY are set. Build-only on PRs touching the
package. The release recipe (version bump, download, tag, pixi.toml edits) is in the note.
pixi.toml is unchanged: the default String-only environment is untouched. Verified
locally on the Mac: pixi build 17 s, rattler-index fs → repodata; the Windows runner is
verified by the PR run this push triggers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ad of GitHub (LAB-303 S1/S2, item 3)

S1 (ww18 5.7: the bridge was hard-killed mid closed loop, the Studio showed only
"bridge disconnected", the run carried on and BIAS kept recording):
- fictrac-bridge-client emits 'disconnect' {involuntary, url}; involuntary = not our
  disconnect(). Emitted before pending connector waiters are rejected, so the loss
  is recorded as the reason, not a generic send failure.
- ArenaRunner.bridgeInUse: the active run depends on the bridge when a closed loop
  is armed (startClosedLoop), connector plugins are declared, or the bridge is
  recording the run. ArenaSession faults such a run with 'bridge_lost' on an
  involuntary close; a user Disconnect or an unused bridge is not a fault.
- run-log deriveOutcome → BRIDGE_LOST (the adapter now defers to it for faulted
  aborts); transcript line carries the note. Studio: banner + log line, STOP via
  the runner's finally, NO controller post-mortem (the controller is fine). The
  bridge's own record is recovered by its first export after a restart (item 1).

S2 (ww18 5: a tab opened at ?p=… showed no protocol until refreshed): initFromUrl
awaited the course repo's shared-pattern listing (a GitHub API call with no
timeout) BEFORE the ?p= load; on a slow or walled network that held the whole
init and a refresh merely got a warm connection. Now the listing starts first
and gets ≤ 1.5 s before the load goes ahead, every pre-step is guarded, and an
unknown protocol key banners instead of leaving an empty Run view.

Footer v0.97 + ET stamp, release notes. Tests: deriveOutcome, adapter outcome,
session bridge-lost hook (4 cases), runner bridgeInUse (unit + closed-loop flow).
Root suite 69/69. Prettier on the changed JS only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…g before the first acquisition (ScanImage 2026.0.0); ping/status pass on the Bergamo PC
…ws even after grabs on ScanImage 2026.0.0); laser-clocked vDAQ noted; single connector grab passes
…aded server stalled the Studio's module imports in Chrome; localhost cost ~2 s/connection on Windows)
…z on the Bergamo, not ~29); dry-run results logged
…gamo card's slots (violet 7, blue 3, green 5, yellow 9)
… colors 2-4, 640 frames cut the end of duty 255); run 1 logged
…ottling (Studio window hidden/occluded); workaround logged
…uty steps visible in both channels, per color)
… x14) + frame-continuity check of runs 1-2 (0 gaps, 29.12 Hz)
…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>
… 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>
…viz port

Studio / runner / client
- connectorCommand waits timeout_s + CONNECTOR_TIMEOUT_MARGIN_MS (default 15 s): the
  browser outlasts the device node's own deadline, so a reply at the deadline is an
  outcome, not an 'unknown'. 2P protocol's waitIdle already declares timeout_s.
- plugin-registry: ScanImagePlugin `set.value` is text (the allowlist holds numbers,
  booleans and paths); the node coerces "640" / "true" to the JSON scalar.
- protocol-yaml-v3: `variables:` written as a list of anchored scalars (`- &name v`,
  the 2P protocols) kept their identity wrong — `.anchor` was read off a primitive
  (String.prototype.anchor), hence the 2026-10-16 "Variable "function anchor() …"
  is defined more than once" warning. extractVariables/_findVariablePair handle
  both shapes; v3 round-trip suite 39 (823/823).
- runner test: connector plugin steps make a run bridge-dependent (loss in the ITI
  before a camera step → BRIDGE_LOST; without the map → continues). 547/547.
- footer stamp; release notes v0.97 extended; CLAUDE.md: ROS-era rules.

ros2-poc
- mcap_index.reindex replaces the bag atomically (os.replace), never unlink-then-rename.
- recorder: ONE constant storage sidecar per log dir (not one hidden file per run);
  optional max_bag_bytes → bridge_node --max-bag-mb (off by default: bags are archived
  on a share, not pushed to GitHub).
- scanimage_node: coerce_scalar for `set` values.
- kf_row lives once in nodes/_rows.py (no ROS imports), used by the node and the
  converter — `converted == live` depends on one encoder.
- ported from #252: nodes/viz_node.py, foxglove/*.json, tests/test_viz_node.py,
  sizing/; rig.py --foxglove also starts viz_node (+ --viz-args); pixi tasks viz,
  test-viz, fox-json (nodes/foxglove_json.py salvaged from the data branch).
- repo hygiene: colcon build logs (interfaces/log) and the one-off win-64 channel
  payload removed from the tree (+ .gitignore); the typed-prebuilt env still reads
  the pinned commit URL.
- stamp.py: one stamping path again (stamp_tree takes a skip set); tiers 109/109.

Harness: ros2-poc `pixi run test` all PASS (default env), test-viz PASS.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…p) + a G6 .pat reader

tools/tiff_run_to_mcap.py: ScanImage TIFFs (frames, channel ids and frame rate from the SI
header tag, per-frame timestamps and the DO1 aux-trigger stamps) + the run's .pat files →
one MCAP with Foxglove well-known JSON schemas: /scanimage/preview/ch<id> (observed, fixed
int16→8-bit window per channel), /scanimage/frame_ref, /scanimage/qc (whole-field mean
minus the pre-stimulus baseline), /arena/stimulus (commanded from a run log, else
RECONSTRUCTED from the first DO1 pulse + nominal step lengths), /arena/display and
/arena/scene (RECONSTRUCTED from .pat × duty × LED color), /run/events and a
/run/provenance message naming the source of every topic. Also writes a Foxglove layout.
tools/patfile.py: minimal G6PT V1/V2 reader mirroring js/pat-parser.js. `pixi run tiff2mcap`.

First use: the 2026-10-16 Bergamo color sweep, run 2 (four 800-frame TIFFs, 29.12 Hz, DO1
spans 24.0 / 24.0 / 24.0 / 23.0 s as the day's analysis found).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… connectors, Studio v0.97 (candidate 2026-10-20)
Candidate for Production. PRs: #256, #254.

Release-Manifest: releases/2026-10-20.json

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.

4 participants