Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .agents/skills/protocol-yaml/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ it does NOT advance the protocol clock. Only `wait` commands do. Therefore:
Editing only ONE of the pair is **masked** by the other — "the timeline won't update"
is almost always a half-edited pair, not a render bug.
- **Bind both fields to one anchor** (`duration: *show_dur` on both) so they can never
drift — the `open_dur`/`cl_dur` pattern from `protocols/fictrac_direction_test`.
drift — the `open_dur`/`cl_dur` pattern from `fictrac_direction_test` (private
`reiserlab/arena-experiments`, `protocols/shared/`).
- Waits between two `trialParams` in one condition define how long the FIRST one
actually shows (the second overrides it). Waits longer than the duration = intentional
hold/blank; fine, but mean it.
Expand Down Expand Up @@ -163,7 +164,7 @@ rejection). Authored as an added rotational **velocity**:
A negative frequency is a harmless no-op (warned): negate `bias_amplitude` instead.
- `stopClosedLoop` clears the bias automatically. Reference:
`docs/development/closed-loop-bias.md`; worked example:
`protocols/fictrac_bias_test.yaml`.
`protocols/shared/fictrac_bias_test.yaml` in the private `reiserlab/arena-experiments`.

**Per-trial start position = the trialParams `frame_index` (Studio v0.91 / bridge 3.4).** A
closed-loop trial opens on the Mode-3 `trialParams` `frame_index` (0-based, wraps mod the pattern's
Expand Down
9 changes: 6 additions & 3 deletions arena_studio.html
Original file line number Diff line number Diff line change
Expand Up @@ -3426,7 +3426,7 @@ <h2 id="modalTitle">Import error</h2>
</div>

<footer id="footer">
<span class="foot-left">Arena Studio v0.100 | 2026-10-28 18:40 ET · <a href="https://github.com/reiserlab/webDisplayTools" target="_blank" rel="noopener" title="webDisplayTools on GitHub: source, issues, and release notes (docs/development/arena-studio-release-notes.md)">GitHub</a></span>
<span class="foot-left">Arena Studio v0.101 | 2026-10-29 16:45 ET · <a href="https://github.com/reiserlab/webDisplayTools" target="_blank" rel="noopener" title="webDisplayTools on GitHub: source, issues, and release notes (docs/development/arena-studio-release-notes.md)">GitHub</a></span>
<!-- Course-repo quick-links: open protocols / logs / patterns in a new tab.
Hrefs built from the configured repo + bench id (updateGhQuickLinks). -->
<span id="ghQuickLinks" title="Open the course repo on GitHub (new tab)">
Expand Down Expand Up @@ -10764,7 +10764,10 @@ <h2 id="modalTitle">Import error</h2>
const key = [st.records, st.dropped, st.gaps, st.errors, st.notStored].join('/');
if (key === Studio.telemetry.lastReport) return;
Studio.telemetry.lastReport = key;
Studio.rawLog('telemetry ring: ' + st.records + ' records · ' + st.blocks + ' blocks · ' + (st.bytes / 1024).toFixed(1) + ' KiB · dropped ' + st.dropped + ' · gaps ' + st.gaps + ' · errors ' + st.errors + (st.notStored ? ' · refused by bridge ' + st.notStored : '') + (st.heapCollision ? ' · RING DISABLED (heap collision)' : '') + (st.survivedReboot ? ' · ring survived a reboot (boot #' + st.bootCount + ')' : ''), 'dim');
// `dropped` in the block header is the controller's LIFETIME eviction count (since
// its boot, mostly from before this page connected); only the growth since our
// first block is in-session loss. Say both, lead with the one that matters.
Studio.rawLog('telemetry ring: ' + st.records + ' records · ' + st.blocks + ' blocks · ' + (st.bytes / 1024).toFixed(1) + ' KiB · evicted this session ' + st.droppedSession + ' (controller lifetime ' + st.dropped + ') · gaps ' + st.gaps + ' · errors ' + st.errors + (st.notStored ? ' · refused by bridge ' + st.notStored : '') + (st.heapCollision ? ' · RING DISABLED (heap collision)' : '') + (st.survivedReboot ? ' · ring survived a reboot (boot #' + st.bootCount + ')' : ''), 'dim');
}, 60000);
} else {
Studio.telemetry.drainer.rearm(); // NO_ACK first: learn the ring incarnation before freeing anything
Expand Down Expand Up @@ -10826,7 +10829,7 @@ <h2 id="modalTitle">Import error</h2>
r.kind === 'cmd' ? 'CMD 0x' + r.cmd.toString(16) + ' st' + r.status + ' @' + r.tUs :
r.kind === 'frame' ? 'FRAME idx' + r.idx + ' sd' + r.sdLoadUs + 'us spi' + r.spiUs + 'us' + (r.reqAgeUs != null ? ' age' + r.reqAgeUs + 'us sup' + r.superseded + (r.contiguous ? ' contig' : '') : '') + ' @' + r.tUs :
r.kind === 'state' ? 'STATE ' + r.stateName + ' ' + (r.codeName || r.code) + ' ' + r.arg + ' @' + r.tUs : '?');
const summary = { tag: tag || 'drain', records: recs.length, blocks: blocks.length, survivedReboot: d.stats.survivedReboot, bootCount: d.stats.bootCount, dropped: d.stats.dropped, gaps: d.stats.gaps, heapCollision: !!d.stats.heapCollision, tail };
const summary = { tag: tag || 'drain', records: recs.length, blocks: blocks.length, survivedReboot: d.stats.survivedReboot, bootCount: d.stats.bootCount, dropped: d.stats.dropped, droppedSession: d.stats.droppedSession, gaps: d.stats.gaps, heapCollision: !!d.stats.heapCollision, tail };
Studio.rawLog('telemetry dump (' + summary.tag + '): ' + recs.length + ' records' + (d.stats.survivedReboot ? ' — ring survived the reboot' : '') + (tail.length ? '\n ' + tail.join('\n ') : ''), recs.length ? 'warn' : 'dim');
const b = Studio.session.bridge;
if (b && b.logging) b.log({ event: 'telemetry_dump', summary, raw_blocks: rawBlocks });
Expand Down
2 changes: 1 addition & 1 deletion course/cshl-2026/docs/fictrac.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ Closed-loop and the live scope need the **bridge** running on the rig computer:
bridge"* to a live trace once data flows. Closed-loop trials show a green
**CLOSED LOOP** tag.

**Quick check:** load [`fictrac_direction_test`](protocols/README.md) — it runs
**Quick check:** load [`fictrac_direction_test`](protocols/README.md) (since 2026-10-29 in the lab's private `reiserlab/arena-experiments`, `protocols/shared/`) — it runs
open-loop motion first, then a closed-loop block that the fly steers. If the
closed-loop block responds to nudging the ball, the bridge is working.

Expand Down
3 changes: 2 additions & 1 deletion course/cshl-2026/docs/protocols/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ Animated pattern previews are embedded on all four protocol pages.
FicTrac): full-field brightness, panel map, a moving bar, and LED levels
2/5/10/20/40%. Use it to confirm a rig works after setup.
- **`fictrac_direction_test`** — open-loop motion then a closed-loop block, to
confirm the [FicTrac](../fictrac.md) bridge is working.
confirm the [FicTrac](../fictrac.md) bridge is working. Since 2026-10-29 it lives in
the lab's private `reiserlab/arena-experiments` (`protocols/shared/`), not in the site library.
- **`optomotor_led_test`** — a slow grating with the LED switched on for the
last few seconds; the reference example of "fire a command partway through a
trial."
Expand Down
9 changes: 9 additions & 0 deletions docs/development/arena-studio-release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ The Studio's footer used to carry the full changelog inline; it now shows one li
history lives here. Newest first. (Per-session engineering detail stays in
`arena-studio-handover.md` and the design docs — this file is the user-facing what-changed list.)

## v0.101 (2026-10-29) · Experiments move to the private repo; pixi run qc on rig PCs; rig-nodes 0.4.0; Studio overwrite-guard fix

<!-- #288 -->
- Rig-specific protocols and the per-rig records moved to the lab's private repos (`reiserlab/arena-experiments`, `reiserlab/scanimage-2026-bergamo`); `protocols/MOVED.md` says where each one went. The site library keeps the validation and demo protocols.

<!-- #289 -->
- **Saving over an existing repo protocol always asks first.** A browser-cache collision could skip the "Overwrite?" prompt after opening or promoting a protocol; it no longer can.
- **The telemetry stats line now separates in-session evictions from the controller's lifetime counter**, so a large number after connecting to a long-running controller no longer looks like data loss.

## v0.100 (2026-10-28) · rig/ installs rig-nodes 0.3.0 with the host monitor; okey-cokey rig; imaging code moves to the private 2P repo

<!-- #281 -->
Expand Down
2 changes: 1 addition & 1 deletion docs/development/rig-software-repos-and-runners.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ stay as history.
| **pyDisplayTools** | `tools/patfile.py` (pattern-file reader; the repo already has `pyPatternTools`), `nodes/arena_display.py` (Python port of `js/panel-color.js`, okey-cokey branch), the pure functions of `fictrac-bridge/bridge.py` (`Pipeline` mapping, `frame_schema_line`, `compact_arena_command`), `fictrac-bridge/fictrac_sim.py` moves to rig-nodes as a device simulator |
| **stays** (gateway, export, composition) | `nodes/bridge_node.py`, `nodes/mcap_convert.py`, `nodes/_rows.py` (behavior_v2 `kf` rows), `nodes/export_log.py`, `nodes/rig.py` (arena composition; its generic preflight/supervision helpers are extracted to rig-nodes first), `pixi.toml`, `pixi.lock`, `.gitignore`, `tests/studio_emulator.py`, `tests/check_runlog_readers.js`, the gateway half of `tests/test_ros2_poc.py` (node-level tests move with their nodes) |
| **stays** (arena visualization) | `nodes/viz_node.py`, `tests/test_viz_node.py`, the arena derivation in `nodes/foxglove_json.py`, `foxglove/arena-fictrac.json`, `foxglove/arena-bias.json`, `docs/foxglove-arena-layout.json`, `tools/tiff_run_to_mcap.py`; from okey-cokey: `nodes/stimulus_node.py`, `nodes/stimulus_core.py`, `nodes/dashboard_replay.py`, `tests/test_stimulus.py`, `tests/test_stimulus_live.py`, `foxglove/arena-okey-cokey.json`, `tests/fixtures/protocols-2026-10-14/*` |
| **stays** (rig-specific config and records) | `README.md`, `WINDOWS-TEST.md`, `SCANIMAGE-DAY.md`, `scanimage-day-2026-10-16.md`, `windows-results-ww18-2026-10-15.md`, `docs/plan-b-explainer*.html`, `docs/build-standalone-explainer.py`, `docs/img/*`; from okey-cokey: `config/bias/okey-cokey-bfs-u3-51s5m.json`, `tools/okey-cokey-start.ps1`, `tools/bias-ufmf.ps1`, the `okey-cokey-*.md` run sheets, `HANDOVER-okey-cokey-2026-10-21.md`, `docs/okey-cokey-foxglove-dashboard-plan.md` |
| **stays** (tool code and generic records) | `README.md`, `WINDOWS-TEST.md`, `docs/plan-b-explainer*.html`, `docs/build-standalone-explainer.py`, `docs/img/*`, `foxglove/arena-okey-cokey.json` (a layout `rig.py --foxglove` loads), `tools/ufmf.py`, `tools/patfile.py`. **Moved 2026-10-29 (LAB-227) to the private `reiserlab/arena-experiments` `rigs/<rig-id>/`:** `windows-results-ww18-2026-10-15.md` → `rigs/ww18/`; from okey-cokey → `rigs/ww14/`: `config/bias/okey-cokey-bfs-u3-51s5m.json`, `tools/okey-cokey-start.ps1`, `tools/bias-ufmf.ps1`, the `okey-cokey-*.md` run sheets, `HANDOVER-okey-cokey-2026-10-21.md`, `docs/okey-cokey-foxglove-dashboard-plan.md`. `SCANIMAGE-DAY.md` and `scanimage-day-2026-10-16.md` went to the private 2P repo with #284. |

#### Amendment 2026-10-28: the imaging work is private

Expand Down
4 changes: 2 additions & 2 deletions docs/development/telemetry-logging-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,10 @@ Studio v0.79 (`arena_studio.html`) or the named module; the bridge only adds the
| `arena_command` | `js/arena-session.js:391` | > 8-byte requests (TRIAL_PARAMS) | v1 object, see §2.2 |
| `trial_quality` | `arena_studio.html:8264` ← `js/trial-quality.js:246` `summary()` | **once, at run end, before export** (v0.78) | `threshold_us`, `target_us`, `trials[]`, `counts {pass, fail, unknown, open}`, `worst_gap_us`, `flagged_trials[]`, `unknown_trials[]`, `duplicates_dropped`, `records`. Per trial: `trial`, `pattern`, `status` (`pass`\|`fail`\|`unknown`), `duration_s`, `cmds70`, `index_changes`, `reads`, `frames`, `slow_reads`, `stalls`, `max_read_ms`, `age_gaps`, `max_age_ms`, `over_target`, `superseded`, `coverage[]` |
| `display_gap` | `js/trial-quality.js:101` `gapEvent` → `arena_studio.html:8479` | one per gap > threshold, as it happens | `trial`, `pattern`, `seq`, `t_us`, `ms`, `kind` ∈ `sd_slow` (+ `phase`, `read_error`) · `frame_read` (+ `idx`; a slow read seen only in the FRAME record) · `frame_age` (+ `idx`; `req_age_us` > threshold). Absent from a clean run |
| `soak` | `arena_studio.html:8317` `logSoak` | soak driver phases | `phase` ∈ `start` · `iteration-start` · `iteration-end` · `paused` · `end`; `iteration-end` carries `iteration`, `outcome`, `fault`, `stop_acked`, `postmortem`, `applied_0x70`, `duration_s`, `telemetry` (drainer stats: `ackSeq`, `lastSeq`, `records`, `blocks`, `bytes`, `gaps`, `dropped`, `errors`, `notStored`, …). **`dropped` is the controller's lifetime evicted-record counter from the block header** (1157 with `gaps: 0` in the sample = eviction before the drainer started, not in-run loss) |
| `soak` | `arena_studio.html:8317` `logSoak` | soak driver phases | `phase` ∈ `start` · `iteration-start` · `iteration-end` · `paused` · `end`; `iteration-end` carries `iteration`, `outcome`, `fault`, `stop_acked`, `postmortem`, `applied_0x70`, `duration_s`, `telemetry` (drainer stats: `ackSeq`, `lastSeq`, `records`, `blocks`, `bytes`, `gaps`, `dropped`, `droppedSession`, `errors`, `notStored`, …). **`dropped` is the controller's lifetime evicted-record counter from the block header** (1157 with `gaps: 0` in the sample = eviction before the drainer started, not in-run loss); `droppedSession` (2026-10-29) is its growth since the drainer's first block of the ring incarnation, the in-session loss, and what the Studio's once-a-minute stats line leads with ("evicted this session N (controller lifetime M)") |
| `sd_diag` | `arena_studio.html:8543` | `Studio.setSdDiag()` and the v0.79 connect banner | `flags`, `in_force` (GET_SD_INFO byte 29), `label` |
| `probe` | `js/studio-postmortem.js:112` | post-mortem after a controller fault | `name`, `phase`, `cmd`, `req`, `sd`, then `dt`, `resp`, `status`, `ok`, `decoded` — or `error`, or `skipped: "capability X absent"`. The health read appears here as `name:"health"` with `decoded` |
| `telemetry_dump` | `arena_studio.html:8580` | post-mortem | `summary {tag, records, blocks, survivedReboot, bootCount, dropped, gaps, heapCollision, tail[]}`, `raw_blocks[]` (hex) |
| `telemetry_dump` | `arena_studio.html:8580` | post-mortem | `summary {tag, records, blocks, survivedReboot, bootCount, dropped, droppedSession, gaps, heapCollision, tail[]}`, `raw_blocks[]` (hex) |
| `crash_report` | `arena_studio.html:8554` | post-mortem, fw flag bit 3 | `present`, `fault`, `ret_addr`, `cfsr`, `hfsr`, `bfar`, `mmfar`, `raw_hex` (junk after a HalfKay reflash — known) |
| `health` | `arena_studio.html:5587` | **Console `chealth` only** | `source:"console"`, `health` (decoded 0xCA) |
| `analog_loopback_sweep` | `arena_studio.html:5303` | Console Analog In panel | `channel`, `rows`, `fit`, `verdict` |
Expand Down
21 changes: 19 additions & 2 deletions js/arena-telemetry.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,9 @@
blocks: 0,
bytes: 0,
gaps: 0, // seq discontinuities seen (should equal controller-reported drops)
dropped: 0, // controller's cumulative drop counter (last value)
dropped: 0, // controller's LIFETIME eviction counter (header value, since its boot)
droppedBase: null, // that counter at our first block of this ring incarnation
droppedSession: 0, // evictions since then = loss this drainer could have prevented
errors: 0,
notStored: 0, // rows the sink refused (ack withheld, will be re-read)
duplicates: 0, // records re-delivered after a rearm (already stored, dropped)
Expand Down Expand Up @@ -339,14 +341,18 @@
break;
}
const rx = now();
let freshRing = false;
// Ring incarnation check (review finding: an old ack after a power
// cycle would free unseen records). A different boot_count with the
// ring NOT marked survived ⇒ fresh ring: forget the cursor. Same
// incarnation after a rearm ⇒ drop records we already stored.
if (peekNext) {
peekNext = false;
if (st.bootCount != null && block.bootCount !== st.bootCount) {
if (!block.survivedReboot) st.lastSeq = null;
if (!block.survivedReboot) {
st.lastSeq = null;
freshRing = true;
}
st.incarnations++;
}
if (st.lastSeq != null) {
Expand All @@ -361,6 +367,15 @@
st.bytes += block.bytes;
st.lastTNowUs = block.tNowUs;
st.dropped = block.dropped;
// The header counter is the controller's lifetime eviction count: it
// includes everything the ring evicted before this drainer ever asked
// (a Studio connected hours after boot sees thousands with gaps 0).
// Only the growth since our first block of this incarnation is loss
// the host could have prevented, so that is what the UI reports.
if (st.droppedBase == null || freshRing || block.dropped < st.droppedBase) {
st.droppedBase = block.dropped;
}
st.droppedSession = (block.dropped - st.droppedBase) >>> 0;
st.bootCount = block.bootCount;
st.survivedReboot = block.survivedReboot;
st.heapCollision = block.disabledHeapCollision;
Expand Down Expand Up @@ -417,6 +432,8 @@
function reset() {
st.ackSeq = NO_ACK;
st.lastSeq = null;
st.droppedBase = null;
st.droppedSession = 0;
peekNext = false;
}

Expand Down
12 changes: 12 additions & 0 deletions js/studio-github.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,10 +361,22 @@
// Thin executor: run one descriptor with an injected fetch. Returns
// {ok, status, data}. Never logs the token. Kept tiny so the builders stay
// the tested surface.
//
// `cache: 'no-store'` (2026-10-29, found on ww18 under LAB-227): the JSON and
// the raw form of ONE contents URL differ only in the Accept header and
// GitHub gives both the same ETag. After a raw read (runBytes: ?p= open,
// promote, previews) Chrome revalidated the stored raw entry for the later
// JSON request, got a 304 and handed back the raw YAML; res.json() then
// threw, `data` was null and every "does this file exist?" check
// (resolveRepoSaveTarget, the silent-fork guard) answered "no" — a changed
// protocol silently overwrote the repo copy with no "Overwrite?" prompt.
// Bypassing the HTTP cache for the JSON executor closes it in both orders:
// nothing JSON is ever stored, so runBytes can keep caching the bytes.
async function run(fetchImpl, req) {
const res = await fetchImpl(req.url, {
method: req.method,
headers: req.headers,
cache: 'no-store',
body: req.body != null ? JSON.stringify(req.body) : undefined
});
let data = null;
Expand Down
Loading
Loading