Three different embryo counts appear on screen simultaneously, and the image count is wrong for the whole session.
From the recording, one frame at 05:30: the panel says EMBRYOS 5, the toast says Registered 4 embryos, the header strip says 0 embryos · Connected and the footer says 0 embryos · 0 images · 9 events. Later (25:30 onward) the header says 1 embryo while the panel lists 4 and the agent says "All 4 embryos are currently unassigned." The footer reads 0 images for all 48 minutes while the Calibration page shows embryo_1 / 48 images.
Cause
There is one server truth — agent.experiment.embryos (harness/state.py, observer hook at :918), served identically by GET /api/embryos/current (routes/data.py:203-222) and the EMBRYOS_UPDATE event (app/agent.py:1044-1049 → connection_manager.py:177-189).
There are six client caches of it, and only three subscribe to the event that changes it. The header (shell.js:29) and footer (app.js:42-45) both read the global state.embryos, which nobody updates on EMBRYOS_UPDATE; the footer is additionally never re-rendered when embryos change.
The 1 embryo reading has its own cause: viewer.js:96-99 pushes into state.embryos on image arrival, so the header counts image provenance rather than registration.
0 images is literally true of its own array: the footer counts only state.snapshots (app.js:43), a set disjoint from state.calibration, and a calibration-heavy session puts every frame in the latter.
The Atrium gauges inherit both bugs verbatim — atrium.js:83-96 reads the same stale globals. Not fixed there.
Done when
Every count on screen derives from one client mirror of the server roster, written in one place.
Smallest fix: one line in websocket.js, in the msg.type === 'event' branch and placed before the bus emit at :126 so listener order cannot matter — write state.embryos from the payload and call updateStatusbar(). Then delete the competing writer at viewer.js:96-99 (nothing consumes state.embryos as an image-provenance set; the only readers are count renderers). For images: app.js:43 → count snapshots + calibration, and call updateStatusbar() at the end of handleNewImage.
Evidence: 2026-08-07 walkthrough, throughout; clearest at 05:30, 10:00, 25:30, 44:00. Not previously captured.
Three different embryo counts appear on screen simultaneously, and the image count is wrong for the whole session.
From the recording, one frame at 05:30: the panel says
EMBRYOS 5, the toast saysRegistered 4 embryos, the header strip says0 embryos · Connectedand the footer says0 embryos · 0 images · 9 events. Later (25:30 onward) the header says1 embryowhile the panel lists 4 and the agent says "All 4 embryos are currently unassigned." The footer reads0 imagesfor all 48 minutes while the Calibration page showsembryo_1 / 48 images.Cause
There is one server truth —
agent.experiment.embryos(harness/state.py, observer hook at:918), served identically byGET /api/embryos/current(routes/data.py:203-222) and theEMBRYOS_UPDATEevent (app/agent.py:1044-1049→connection_manager.py:177-189).There are six client caches of it, and only three subscribe to the event that changes it. The header (
shell.js:29) and footer (app.js:42-45) both read the globalstate.embryos, which nobody updates onEMBRYOS_UPDATE; the footer is additionally never re-rendered when embryos change.The
1 embryoreading has its own cause:viewer.js:96-99pushes intostate.embryoson image arrival, so the header counts image provenance rather than registration.0 imagesis literally true of its own array: the footer counts onlystate.snapshots(app.js:43), a set disjoint fromstate.calibration, and a calibration-heavy session puts every frame in the latter.The Atrium gauges inherit both bugs verbatim —
atrium.js:83-96reads the same stale globals. Not fixed there.Done when
Every count on screen derives from one client mirror of the server roster, written in one place.
Smallest fix: one line in
websocket.js, in themsg.type === 'event'branch and placed before the bus emit at:126so listener order cannot matter — writestate.embryosfrom the payload and callupdateStatusbar(). Then delete the competing writer atviewer.js:96-99(nothing consumesstate.embryosas an image-provenance set; the only readers are count renderers). For images:app.js:43→ countsnapshots + calibration, and callupdateStatusbar()at the end ofhandleNewImage.Evidence: 2026-08-07 walkthrough, throughout; clearest at 05:30, 10:00, 25:30, 44:00. Not previously captured.