Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
205 commits
Select commit Hold shift + click to select a range
4530ab1
UX v2 Phase 0: unify connection status across the three indicators
pskeshu Jun 11, 2026
4571a58
UX v2 Phase 1 (scaffold): add GENTLY_UX_V2 feature flag
pskeshu Jun 11, 2026
17e66cb
UX v2 Phase 1: dual-render agent asks (chat transcript + main stage)
pskeshu Jun 11, 2026
52503e7
UX v2 Phase 2: grouped rail nav + session-context strip (behind flag)
pskeshu Jun 11, 2026
288f97b
UX v2 Phase 3a: inference-first plan mode (model-driven, with provena…
pskeshu Jun 11, 2026
e947506
UX v2 Phase 3b: surface inferred imaging spec + per-field provenance …
pskeshu Jun 11, 2026
2387225
UX v2 Phase 4: co-editable shared-visibility surface (the agent's min…
pskeshu Jun 11, 2026
e8ce2bf
UX v2 Phase 4 follow-up: show an empty-state for the agent's-view panel
pskeshu Jun 12, 2026
c98a33f
Put the free-the-port command in the "port in use" error
pskeshu Jun 12, 2026
927900a
UX v2 Phase 5: Experiment view shows real tactics only — drop stubbed…
pskeshu Jun 12, 2026
f69bc4e
UX v2 Phase 6 (step 1): flip GENTLY_UX_V2 default ON, keep v1 fallback
pskeshu Jun 12, 2026
e4de3a1
UX v2: agent-first landing → in-page plan wizard (flag-gated)
pskeshu Jun 12, 2026
37bc534
Add UX v2 entry-paradigm prototype + migration plan (design reference)
pskeshu Jun 12, 2026
6c6bc70
Detectors: forced-tool structured output for hatching + verifier
pskeshu Jun 12, 2026
4910d06
Fix recurring port-8080 false positive: SO_REUSEADDR on the viz prefl…
pskeshu Jun 12, 2026
5ee9e32
chore: gitignore the stray D:/ storage dir
pskeshu Jun 12, 2026
a8fd3e5
UX v2 landing: fix welcome→plan jump, dark mode, and the entry flow
pskeshu Jun 12, 2026
1cefe2f
UX v2: agent-activity events + GFM markdown rendering
pskeshu Jun 12, 2026
64f9338
Models: migrate to Fable 5 / Opus 4.8 / Sonnet 4.6 with refusal+400 f…
pskeshu Jun 12, 2026
0457975
Add UX v2 landing screenshot for the PR
pskeshu Jun 12, 2026
b526b23
lint: conform UX v2 + model-migration code to #47 ruff tooling
pskeshu Jun 14, 2026
ec64daf
Models: revert main tier from Fable 5 to Opus 4.8
pskeshu Jun 14, 2026
d5dac56
UX v2: expandable tool cards show full (bounded) tool results
pskeshu Jun 15, 2026
a26ee99
WIP: 3D optical-space view in the Devices tab
pskeshu Jun 15, 2026
91ec6d2
docs: UX v2 interaction-flow / IA audit
pskeshu Jun 16, 2026
9edb766
UX v2 P0: live-stream the agent turn + show reasoning during the wait
pskeshu Jun 16, 2026
f209874
Fix create/update_plan_item crash when spec/references passed as JSON…
pskeshu Jun 16, 2026
144d8dd
UX v2: add a concision/communication-style section to the plan-mode p…
pskeshu Jun 16, 2026
d061e9d
UX v2: run independent read-only tool calls concurrently
pskeshu Jun 16, 2026
a779901
Fix create_plan_item crash when phase_number is a string
pskeshu Jun 16, 2026
06741eb
Restrict tool concurrency to read-only tools; nudge batched plan-item…
pskeshu Jun 16, 2026
faee181
UX v2: plan-done state + restructured THE PLAN panel
pskeshu Jun 16, 2026
f6f2ba3
UX v2: drop wrap-up reasoning litter from the plan feed
pskeshu Jun 16, 2026
b8df610
UX v2: export-plan button replaces the end-of-plan prose upsell
pskeshu Jun 16, 2026
dedc418
Design doc: active shared lab notebook (memory model)
pskeshu Jun 16, 2026
e89161f
plan: notebook foundation (Note model + NotebookStore) — increment 1 …
pskeshu Jun 16, 2026
60cd40a
feat(notebook): unified Note model + dict serialization
pskeshu Jun 16, 2026
d15607d
feat(notebook): NotebookStore write_note/get_note with atomic YAML
pskeshu Jun 16, 2026
5225811
feat(notebook): rebuildable reverse-indexes by strain/embryo/thread
pskeshu Jun 16, 2026
9ba23c9
feat(notebook): query_notes by kind/author/status/scope
pskeshu Jun 16, 2026
458810a
feat(notebook): link_notes + supersede_note (append-only history)
pskeshu Jun 16, 2026
fed6821
plan: notebook producer bridge (apply_updates -> notebook)
pskeshu Jun 16, 2026
9ca5593
feat(notebook): Observation/Learning -> Note converters
pskeshu Jun 16, 2026
19c2ff4
feat(notebook): FileContextStore.notebook lazy property
pskeshu Jun 16, 2026
9224b0f
feat(notebook): apply_updates mirrors observations & learnings into n…
pskeshu Jun 16, 2026
03ea7c6
plan: notebook read API
pskeshu Jun 16, 2026
7d51e20
feat(notebook): read API — GET /api/notebook/notes + /notes/{id}
pskeshu Jun 16, 2026
1a0121d
feat(notebook): read API — GET /api/notebook/threads with counts
pskeshu Jun 16, 2026
0760578
feat(notebook): Notebook tab (read view) in LIBRARY rail
pskeshu Jun 16, 2026
1c87896
plan: notebook live edge (Agent's view recent notes)
pskeshu Jun 16, 2026
9b28fa8
feat(notebook): limit param on GET /api/notebook/notes
pskeshu Jun 16, 2026
b643972
feat(notebook): Agent's-view live edge — recent notes section → Noteb…
pskeshu Jun 16, 2026
814e322
plan: ask the notebook (increment 2 backend)
pskeshu Jun 16, 2026
8928084
feat(notebook): ask backend — select_notes retrieval + forced-tool gr…
pskeshu Jun 16, 2026
13b4dd2
feat(notebook): POST /api/notebook/ask — grounded notebook Q&A
pskeshu Jun 16, 2026
c7be82a
feat(notebook): 'Ask the notebook' box on the Notebook tab
pskeshu Jun 16, 2026
ecd5239
feat(imaging): Run button on actionable imaging plan items
pskeshu Jun 16, 2026
71ac0ea
integration: ux-v2 + memory-model (notebook) + imaging-triggers (run …
pskeshu Jun 16, 2026
6eac3e6
deps: declare opencv-python-headless (cv2) as a runtime dependency
pskeshu Jun 16, 2026
0d01229
fix(dispim): correct stage-position read keys; bottom camera never us…
pskeshu Jun 16, 2026
9abcf6e
chore(deps): lock opencv-python-headless (uv.lock sync)
pskeshu Jun 16, 2026
33ce5b0
fix(timelapse): accept comma-separated embryo_ids string in start()
pskeshu Jun 16, 2026
abb11fb
fix(web): emit unpadded embryo ids to match the live convention
pskeshu Jun 16, 2026
68a7833
lint: wrap long lines + ruff-format notebook modules (E501)
pskeshu Jun 16, 2026
31ba4b1
Merge remote-tracking branch 'upstream/development' into integration/…
pskeshu Jun 17, 2026
d2d61b8
feat(notebook): record_note tool — agent writes human notes to the no…
pskeshu Jun 17, 2026
83b825f
feat(plans): live plan UI — PLAN_UPDATED event + campaigns.js refresh…
pskeshu Jun 17, 2026
e1f698e
fix(plans): reliable session↔item linking, one item ↔ many sessions (…
pskeshu Jun 17, 2026
a3b649c
lint: ruff-format file_store.py (fixes CI format check on integration)
pskeshu Jun 17, 2026
0d1cdeb
feat(plans): plan narrative in agent context + fix provenance spec-ta…
pskeshu Jun 17, 2026
bb54ab6
Connection D: editable/fillable imaging specs in the plan inspector
pskeshu Jun 17, 2026
4d503f8
docs(temperature): design spec for temperature interface (sub-project A)
pskeshu Jun 27, 2026
d1c881f
docs(temperature): TDD implementation plan for temperature interface …
pskeshu Jun 27, 2026
06a5883
feat(temperature): session-scoped temperature.jsonl store on FileStore
pskeshu Jun 27, 2026
3279c8f
feat(temperature): add TEMPERATURE_UPDATE event type
pskeshu Jun 27, 2026
81bba23
feat(temperature): TemperatureSampler service (poll/persist/emit @1Hz)
pskeshu Jun 27, 2026
122a580
feat(temperature): start/stop TemperatureSampler with the agent
pskeshu Jun 27, 2026
1a062c2
feat(temperature): GET /api/temperature/{session}/history backfill route
pskeshu Jun 27, 2026
a8e9817
feat(temperature): stamp latest reading into burst + volume metadata
pskeshu Jun 27, 2026
e21b567
feat(temperature): live SVG temperature graph on the Devices tab
pskeshu Jun 27, 2026
0daebda
fix(temperature): idempotent graph init + theme setpoint color + labe…
pskeshu Jun 27, 2026
1f6c7fa
docs(temperature): correct EventType auto() example in plan
pskeshu Jun 27, 2026
60e63dd
fix(temperature): events-tab exclusion, burst-stamp test, invalidate …
pskeshu Jun 27, 2026
a79a957
fix(temperature): root-level temp graph colors for portability
pskeshu Jun 28, 2026
339cc7c
docs(manual-mode): design spec for SPIM single-slice live view (B1)
pskeshu Jun 28, 2026
2041c3a
docs(manual-mode): B1 plan (7 tasks) + ASIdiSPIM two-camera model app…
pskeshu Jun 28, 2026
ec917e0
feat(manual-mode): add LIGHTSHEET_FRAME event type + frontend exclusion
pskeshu Jun 28, 2026
0ec20c2
feat(manual-mode): device-layer lightsheet live streamer (continuous …
pskeshu Jun 28, 2026
2c2cc95
feat(manual-mode): client stream_lightsheet + set_lightsheet_live_params
pskeshu Jun 28, 2026
8b86000
feat(manual-mode): LightSheetStreamMonitor bridge + agent wiring
pskeshu Jun 28, 2026
d641fa4
feat(manual-mode): require_control proxy routes for lightsheet live +…
pskeshu Jun 28, 2026
84a659d
fix(manual-mode): laser/off uses Laser ALL OFF config + expose laser …
pskeshu Jun 28, 2026
9917e12
feat(manual-mode): Manual view — SPIM live canvas, galvo/piezo/exposu…
pskeshu Jun 28, 2026
c749223
fix(manual-mode): init temperature graph per-view in switchView (sing…
pskeshu Jun 28, 2026
5e12b60
docs(manual-mode): lightsheet FPS measurement protocol + transport de…
pskeshu Jun 28, 2026
779e6c6
fix(manual-mode): brightfield-safe + focal-plane-correct acquire, seq…
pskeshu Jun 28, 2026
909aacc
docs(tactic): spec + plan for temp-change burst tactic (C, 6 tasks)
pskeshu Jun 28, 2026
7ec42d0
feat(tactic): timeline event types for temp-change burst protocol
pskeshu Jun 28, 2026
5412c30
feat(tactic): thread laser_config through BurstAcquisition + queue_bu…
pskeshu Jun 28, 2026
030a443
feat(tactic): wait_for_temperature_lock poll helper
pskeshu Jun 28, 2026
28abaa1
feat(tactic): temp-change burst protocol driver (brightfield before/d…
pskeshu Jun 28, 2026
6af4396
feat(tactic): run_temp_change_burst_protocol agent tool
pskeshu Jun 28, 2026
c986c92
feat(tactic): surface temp-protocol band + setpoint changes in strate…
pskeshu Jun 28, 2026
dc03872
fix(tactic): correct event docstrings, guard against concurrent timel…
pskeshu Jun 28, 2026
59c429a
docs(operations): spec + plan for Operations tab (D, 4 tasks)
pskeshu Jun 28, 2026
95f9126
feat(operations): rename Experiment tab to Operations (label only)
pskeshu Jun 28, 2026
380336f
feat(operations): emit burst phase in BURST_START + record in snapshot
pskeshu Jun 28, 2026
2a3d8b0
feat(operations): tactic band — protocol pill + setpoint markers + ph…
pskeshu Jun 28, 2026
6003b8e
docs(operations): redesign D as operation-spine (data-driven + scenar…
pskeshu Jun 28, 2026
b8da138
feat(operations): discard swimlane tactic-band (replaced by operation…
pskeshu Jun 28, 2026
22d9b06
docs(operations): v3 spec — agent-authored typed Operation Plan (DRAF…
pskeshu Jun 28, 2026
9d0c7d8
docs(operations): v3 plan — agent-authored Operation Plan (5 tasks, b…
pskeshu Jun 28, 2026
192fe97
feat(operations): OperationPlan store domain in FileContextStore
pskeshu Jun 28, 2026
1472dd0
feat(operations): declare_operation_plan typed agent tool
pskeshu Jun 28, 2026
f0d09bd
docs(operations): add execution-linkage tasks (transition_tactic, too…
pskeshu Jun 28, 2026
a75d23f
feat(operations): GET /api/operation_plan/{session} route
pskeshu Jun 28, 2026
84f5f17
docs(operations): add plan-item↔operation linkage tasks (tactical out…
pskeshu Jun 28, 2026
66a40d1
feat(operations): transition_tactic store helper
pskeshu Jun 28, 2026
224d865
feat(operations): link execution tools to plan tactics via tactic_id
pskeshu Jun 28, 2026
c445f6c
docs(sdd): Task 7 report — tactic_id threading
pskeshu Jun 28, 2026
442265f
feat(operations): OperationPlanUpdater — execution events transition …
pskeshu Jun 28, 2026
152837e
feat(operations): plan-item tactical outline (plan tactics with the i…
pskeshu Jun 28, 2026
16a85a3
feat(operations): seed Operation Plan goal + planned tactics from the…
pskeshu Jun 28, 2026
5d16f34
feat(operations): operation-spine renderer + plan scenario library (d…
pskeshu Jun 28, 2026
e65834d
feat(operations): live telemetry binding + event-driven refresh
pskeshu Jun 28, 2026
ab4395c
fix(operations): wire seeding + agent plan guidance + queue_burst gua…
pskeshu Jun 28, 2026
f4ab606
fix(operations): surface bound live.* telemetry, paused state styling…
pskeshu Jun 28, 2026
b98a041
chore: untrack SDD scratch doc accidentally committed
pskeshu Jun 28, 2026
3a2bda6
chore: gitignore .superpowers SDD scratch (untrack accidentally-commi…
pskeshu Jun 28, 2026
286e95e
docs(tactics-library): spec + plan (G, 3 tasks) — save/reuse typed ta…
pskeshu Jun 28, 2026
126de2c
feat(tactics-library): tactic-library store domain (save/list/get/apply)
pskeshu Jun 28, 2026
e25798d
feat(tactics-library): save/list/apply_tactic agent tools
pskeshu Jun 28, 2026
0dcfdca
feat(tactics-library): GET /api/tactic_library route
pskeshu Jun 28, 2026
4703f91
fix(tactics-library): scope round-trip test + preserve rationale + de…
pskeshu Jun 28, 2026
df7cf8e
docs(d2): embryo roles/strain + multi-embryo observability spec + wra…
pskeshu Jun 28, 2026
cc77f0f
docs(d2): implementation plan (4 tasks) — strain field, roles-as-use,…
pskeshu Jun 28, 2026
3ad9afe
feat(d2): per-embryo strain field
pskeshu Jun 28, 2026
d14c6de
feat(d2): roles-as-use — add lineaging + subject/reference class
pskeshu Jun 28, 2026
f48b206
docs(sdd): d2-task-2 report
pskeshu Jun 28, 2026
a697a68
feat(d2): /api/roles route + role-scoped tactic scope resolver
pskeshu Jun 28, 2026
b1a47ce
feat(d2): Operations roster lens — embryos by role + strain
pskeshu Jun 29, 2026
866d4e0
fix(d2): surface strain through timelapse_tracker + rename _rosterEmb…
pskeshu Jun 29, 2026
4353c10
chore(d2): untrack .superpowers SDD scratch (committed by a subagent)
pskeshu Jun 29, 2026
a7e41e5
docs(f): session↔plans link/delink spec + plan (4 tasks); base plans …
pskeshu Jun 29, 2026
775392b
feat(f): unlink_plan_item_session + get_plan_items_for_session
pskeshu Jun 29, 2026
26ea06a
feat(f): session↔plan link/delink endpoints
pskeshu Jun 29, 2026
73edb37
feat(f): Plans-tab session link/delink controls
pskeshu Jun 29, 2026
3bfe8e5
feat(f): session Linked-plans panel (link/delink)
pskeshu Jun 29, 2026
e2b7e48
fix(f): item-scoped POST response + delink-failure re-render + defens…
pskeshu Jun 29, 2026
99540bd
docs: add F + B2 to the wrap-up PR plan stack
pskeshu Jun 29, 2026
1f743ce
docs(b2): dual-camera + laser-preset + timelapse-form spec + plan (3 …
pskeshu Jun 29, 2026
340ab96
feat(b2): laser-preset browser (set proxy + manual-mode dropdown)
pskeshu Jun 29, 2026
4ba1d05
feat(b2): dual-camera config (HamCam2 + side selector)
pskeshu Jun 29, 2026
5f7011e
feat(b2): manual timelapse-config form (start proxy + rail panel)
pskeshu Jun 29, 2026
65525e9
fix(b2): style the timelapse panel (define ls-collapsible/ls-sub-* in…
pskeshu Jun 29, 2026
860f5f2
fix(manual): single LED toggle + laser on/off toggle (replace two-but…
pskeshu Jun 29, 2026
225de14
feat(operations): pin plan kind/scope/state schema in guidance + norm…
pskeshu Jun 29, 2026
a1c8170
fix(device): bound live-view memory (cap+drain+snap mode) + side-B ga…
pskeshu Jun 29, 2026
656e974
feat(manual): timelapse config as an accordion with active/inactive s…
pskeshu Jun 29, 2026
29fa9e4
feat(gallery): top-level Gallery tab + volume-acquired confirmation
pskeshu Jun 29, 2026
020feac
feat(operations): set-up-an-operation CTA in the idle state (agent-fi…
pskeshu Jun 29, 2026
537f414
style: ruff check + format — lint clean
pskeshu Jun 29, 2026
e2b03c5
fix(tests): annotate test-stub Optionals for mypy
pskeshu Jun 29, 2026
deaaffa
feat(launch): --no-auth flag + quiet temperature-sampler backoff when…
pskeshu Jun 29, 2026
c50cab0
docs: document --no-auth launch flag in README
pskeshu Jun 29, 2026
3a85449
fix(chat): ask renders main-stage only + waiting-for-answer state (no…
pskeshu Jun 29, 2026
e9900c3
feat(operations): expandable spine tactic cards (rationale/scope/stru…
pskeshu Jun 29, 2026
55cdb39
feat(operations): guidance — plan calibration/reference roles + tacti…
pskeshu Jun 29, 2026
6797e0a
fix(live-view): throttle SPIM live stream to prevent GPU TDR freeze
pskeshu Jun 29, 2026
b34143c
fix(devices): stop Δt label jitter shaking the LIVE badge / room-ligh…
pskeshu Jun 29, 2026
e2d1a7a
feat(manual-view): reorganize lightsheet controls into a no-scroll 2-…
pskeshu Jun 29, 2026
3887f97
refactor(manual-view): remove dead CAM LED button
pskeshu Jun 29, 2026
eaa1d50
fix(web): stop acquire routes returning raw numpy arrays (500 on Snap…
pskeshu Jun 29, 2026
e17280a
Merge remote-tracking branch 'origin/development' into feature/temper…
pskeshu Jun 30, 2026
60b948e
feat(devices): Map-view embryo detect + single-source-of-truth list +…
pskeshu Jun 30, 2026
6ffe1f9
docs(spec): bottom-cam→SPIM Operator surface design (sub-project A)
pskeshu Jun 30, 2026
223812c
feat(operate): backend — fenced Z read/nudge, agent-free marking conf…
pskeshu Jun 30, 2026
7a95f04
feat(operate): Operate view — guided bottom-cam → SPIM operator surface
pskeshu Jun 30, 2026
d1662dd
feat(operate): B — capture marking as localization labels
pskeshu Jun 30, 2026
47bb84f
feat(operate): C — passive focus-trace logging + offline validation
pskeshu Jun 30, 2026
ecfcb60
feat(operate): redesign as 'The Operator Spine' — uncluttered, legibl…
pskeshu Jun 30, 2026
522e25b
refactor(devices): remove duplicate Map-view Detect panel — Detect li…
pskeshu Jun 30, 2026
7bd39ce
docs(spec): Operate → tactics/timelapse integration design (Phase C Run)
pskeshu Jun 30, 2026
f8175b2
feat(operate): Phase C 'Run' — marking→imaging handoff (Phase 1)
pskeshu Jun 30, 2026
178f872
feat(operate): Phase 2 — Mode B emits a real tactic + tactic_id symmetry
pskeshu Jun 30, 2026
030feae
feat(operate): Phase 3 — Tactic Executor + library/oneshot/plan/agent…
pskeshu Jun 30, 2026
2d4e959
fix(operate): resolve 8 confirmed code-review findings
pskeshu Jun 30, 2026
765c4dd
Merge remote-tracking branch 'upstream/feature/temperature-operations…
pskeshu Jul 1, 2026
c02d52d
docs(spec): settings panel — editable ACUITYnano thermalizer + config…
pskeshu Jul 1, 2026
b47ab66
feat(settings): editable ACUITYnano thermalizer + effective-config vi…
pskeshu Jul 1, 2026
1a0f129
fix(settings): resolve 4 confirmed thermalizer code-review findings
pskeshu Jul 1, 2026
eb63cbe
feat(settings): Phase 2 — rig-wide dashboard defaults + restart-requi…
pskeshu Jul 1, 2026
703db32
fix(settings): drop dead settings.py knobs from editors + group the A…
pskeshu Jul 1, 2026
76c3224
perf(file_store): O(1) prediction_id via bounded tail read
pskeshu Jul 1, 2026
c478a40
docs: concurrency & runtime architecture overview
pskeshu Jul 1, 2026
d5d2c1a
fix(web): quiet 503 for laser-configs/cameras when device layer is of…
pskeshu Jul 1, 2026
fb0e13e
feat(web): friendly login hint when a control action is denied (403)
pskeshu Jul 1, 2026
33635d7
feat(temperature): bundle ACUITYnano serial SDK in-repo
pskeshu Jul 2, 2026
c49e524
build(deps): move hardware-control stack into default-on `device` group
pskeshu Jul 2, 2026
7136498
fix(operate): correct marking stage origin + SAM room-light fallback
pskeshu Jul 2, 2026
236e940
feat(operate): add per-embryo piezo-galvo calibration step
pskeshu Jul 2, 2026
565593c
fix(devices-map): frame marked embryos so they aren't clipped off-map
pskeshu Jul 2, 2026
d846dcd
fix(tools): coerce string tool args to annotated scalar types; quiet …
pskeshu Jul 9, 2026
8687721
Merge upstream/development into feature/temperature-operations-all
pskeshu Jul 11, 2026
08a6752
fix(lint): resolve post-merge ruff-format and mypy errors
pskeshu Jul 11, 2026
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
11 changes: 9 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -146,5 +146,12 @@ electron/
gently/ui/tui/node_modules/
gently/ui/tui/dist/

# Runtime storage accidentally created on Linux when GENTLY_STORAGE_PATH="D:/" resolves literally
D:/
# Stray local storage: GENTLY_STORAGE_PATH default (D:\Gently3) resolves
# literally to ./D:/ under the repo on Linux. Not data we track.
/D:/
.superpowers/

# Runtime operator overrides (written by the Settings panel)
config/config.local.yml
config/settings.local.yml
config/dashboard_defaults.json
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ uv run python launch_gently.py --no-api
# Don't auto-open a browser — open the printed URL yourself
uv run python launch_gently.py --no-browser

# Skip login — disable accounts (localhost-control mode; same as GENTLY_NO_AUTH=1)
uv run python launch_gently.py --no-auth

# Resume a previous session
uv run python launch_gently.py --resume # interactive picker
uv run python launch_gently.py --resume latest # most recent session
Expand Down Expand Up @@ -211,8 +214,9 @@ First-run admin account created — sign in at the URL above:
- **Lost it?** There's no reset command yet — delete
`<GENTLY_STORAGE_PATH>/auth/users.yaml` and restart to bootstrap a fresh
`admin` (this clears all accounts).
- **Just trying it locally?** `GENTLY_NO_AUTH=1` disables accounts entirely
(legacy mode: localhost gets control, remote callers need `X-Gently-Token`).
- **Just trying it locally?** Launch with `--no-auth` (or set `GENTLY_NO_AUTH=1`)
to disable accounts entirely (localhost gets control, remote callers need
`X-Gently-Token`). Handy if you've lost the admin password.

Accounts live under `<GENTLY_STORAGE_PATH>/auth/` (`users.yaml` + `secret.key`),
outside the repo.
Expand Down
171 changes: 171 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Gently — Concurrency & Runtime Architecture

How the system runs temperature telemetry, device-state polling, experiments,
image acquisition, and perception/VLM "at the same time" without stepping on
itself. The short version: **almost nothing runs truly in parallel — the design
quarantines the blocking work and serializes the hardware, then keeps both event
loops responsive by offloading and decoupling everything slow.**

> Scope: the concurrency model. For storage layout see `CLAUDE.md`; for the
> device/hardware plugin model see `docs/asi-plugin-architecture.md`.

## 1. Two processes, bridged by HTTP + a shared filesystem

This split is the load-bearing fact of the whole design.

```
┌────────────────────────────────────────┐ ┌────────────────────────────────────────┐
│ APP / VIZ process (FastAPI :8080) │ │ DEVICE-LAYER process (aiohttp :60610) │
│ gently/app/agent.py │ │ gently/hardware/dispim/device_layer.py │
│ │ HTTP │ │
│ • agent + TimelapseOrchestrator │◄──────►│ • the ONLY code touching hardware: │
│ • Perceiver (gently_perception, VLM) │ (DiSPIM│ MMCore/pymmcore, Ophyd, Bluesky RE │
│ • AsyncAnthropic clients │ Client)│ • plan queue + single executor │
│ • EventBus + WebSocket ConnectionMgr │ │ • 3 state pollers + camera/LS streamers │
│ • TemperatureSampler, DeviceStateMonitor│ │ │
└──────────────┬──────────────────────────┘ └───────────────┬──────────────────────────┘
│ shared filesystem (incoming/ TIFF staging, session store)
└──────────────────────────────────────────────────┘
```

- The **device-layer process** (`device_layer.py`) is the *only* code that ever
touches the microscope: MMCore/pymmcore, the Ophyd devices, and a Bluesky
`RunEngine`. All truly blocking hardware work is quarantined here.
- The **app/viz process** (`app/agent.py`, FastAPI+uvicorn) owns everything
cognitive and user-facing: the agent, the `TimelapseOrchestrator`, the
`Perceiver` (external `gently_perception` VLM package), the Anthropic clients,
the in-process `EventBus`, the WebSocket `ConnectionManager`, and the pollers.
- The app reaches hardware **only** through `DiSPIMClient` (`client.py`), over a
single shared `aiohttp.ClientSession`. It never imports MMCore.

Because blocking hardware calls live in a separate process, the app-side loop
stays a light cooperative-asyncio world.

## 2. One asyncio event loop per process

- **Device loop:** HTTP routes + the RunEngine driver + the plan queue/executor +
three state pollers (XY ~5 Hz, piezo/galvo ~1 Hz, full property cache ~15 s) +
subscriber-gated camera/lightsheet SSE streamers.
- **App loop:** FastAPI + the `TimelapseOrchestrator` acquisition loop +
`TemperatureSampler` (1 Hz) + `DeviceStateMonitor` + `Perceiver` calls + one
coroutine per open browser WebSocket.

## 3. The core trick: hardware is serialized, not parallelized

Every Bluesky plan — a move, a snap, a volume/burst acquisition, a focus/calibration
sweep — is submitted as a `PlanRequest` onto **one `asyncio.Queue`
(`self._plan_queue`), drained by one `_plan_executor` task**. Only one plan owns
the hardware at a time.

- `submit_plan` enqueues and `await`s a per-request `asyncio.Future`; the executor
sets the result/exception, which flows back to the awaiting HTTP handler. On
failure the executor records it in `_plan_execution_log` and continues to the
next queued plan.
- Underneath, **pymmcore's internal `g_core_lock` is the real mutex** serializing
every actual core call across pollers *and* plans. The `DiSPIMSystem` facade is
the single place the process touches MMCore.

A microscope has one stage and one camera; single-file execution is correct, not
a limitation.

## 4. How constant polling coexists with long experiments

Four cooperating mechanisms keep the loops responsive while a plan runs:

1. **Offload blocking reads.** Every MMCore read, camera grab, SAM call, and
transient temperature probe goes through `asyncio.to_thread`; the camera
sequence runs on its own `threading.Thread` returning an Ophyd `Status`. The
loop itself never sits on I/O.
2. **Split pollers by cadence.** The three device-state pollers are independent
tasks so a slow (~1.5 s) full-state-cache read cannot stall the ~5 Hz XY path.
3. **`pause_state_updates()` — a reference-counted async context manager.** Heavy
plans (a `frozenset` of names) wrap execution in it, incrementing a counter;
every poller/streamer checks `if self._state_pause_counter > 0` and *skips its
MMCore read*, emitting only ~2 s heartbeats. The plan gets full serial/camera
bandwidth instead of the pollers fighting it for `g_core_lock`. Nested heavy
sections stack and unwind cleanly.
4. **Telemetry bypasses the plan queue.** `GET /api/temperature/status` reads the
temperature Ophyd device directly (`temp.read()` over serial/MQTT — a device
wholly separate from MMCore); `GET /api/devices/state` returns the cached
`_state_latest` snapshot. Neither sits behind a running experiment, so status
polls are never blocked by a long acquisition. (MMCore push callbacks also
mirror joystick/property changes into `_state_latest` via
`loop.call_soon_threadsafe` with ~50 ms debouncing.)

### Temperature specifically
The vendor SDK backend (serial or MQTT) runs its own **background daemon thread**
that ingests the controller's 500 ms telemetry broadcast into a cache
(`self.telemetry`); `get_water_temp()`/`get_system_state()` return the cached
value (non-blocking). `TemperatureController.read()` returns that cache, and
`TemperatureSampler` (`interval_sec=1.0`) polls it at 1 Hz → persists + emits
`TEMPERATURE_UPDATE`. So we ride the telemetry indirectly, resampled at 1 Hz.

## 5. Image data travels via the filesystem, not JSON

Arrays over ~1 MB are written as **TIFF into the shared `incoming/` staging dir**;
only a small `{__file_ref__, path, shape, dtype}` dict crosses HTTP (`serialize_value`).
The client resolves the ref with `tifffile` and hands the decoded array to
`register_volume`, which renames the file into the session store and stamps it
with the latest temperature sample — avoiding a multi-GB JSON blob and a double
decode.

## 6. Perception / VLM / events are decoupled (fire-and-forget)

- **Perception never gates acquisition.** The `TimelapseOrchestrator` does
`asyncio.create_task(self._run_perception(...))` rather than awaiting it inline,
so a slow VLM call doesn't hold up the next embryo. Inside the task the
`Perceiver` and the Claude client are awaited cooperatively, and every Claude
call is wrapped in `asyncio.wait_for(timeout=30)` returning a safe fallback
instead of raising into the loop.
- **The `EventBus` publishes without awaiting handlers** (`event_bus.py`): sync
handlers run inline, async handlers are scheduled via `asyncio.ensure_future` /
`loop.call_soon_threadsafe`; high-volume telemetry types skip the bounded
history deque, so one slow WebSocket client delays only its own broadcast.

## 7. Backpressure & failure isolation

- **SSE streams** use per-subscriber queues bounded at `maxsize=4`; device-state
broadcasts drop slow subscribers, camera/lightsheet streams drop the *oldest*
frame and push the newest so steady clients keep fresh frames.
- **WebSocket fan-out** (`ConnectionManager.broadcast`) sends per client under an
`asyncio.Lock` and drops clients that error.
- **Failure = a gap, not a crash.** A failed temperature poll logs once and backs
off (`1.0 s → min(interval*10, 30 s)`); a stalled SSE forces a watchdog
reconnect after 60 s (chosen to tolerate the quiet windows during heavy plans);
volume acquisition always disables lasers on error to protect the sample.
- **Config safety.** `POST /api/temperature/config` refuses (`409`) while the
RunEngine is not idle or a ramp holds the controller lock; `health_check()` is
read-only and never flips the connected flag, so a transient status-poll timeout
can't disconnect an in-flight acquisition.

## 8. Known limits & bottlenecks

- **RunEngine on the loop.** `self.RE(plan)` is invoked synchronously on the
device-layer loop, so while a plan runs the loop is largely occupied — which is
exactly why heavy plans quiet the pollers and the app-side watchdog tolerates
60 s of silence. Whether the telemetry HTTP handlers stay fully responsive
mid-acquisition depends on Bluesky's internal threading (external package) and
is not verified from repo code.
- **Unbounded in-flight perception.** `_perception_tasks` is a plain set that
self-prunes; sustained fast acquisition against a slow VLM could grow concurrent
Claude calls without an explicit cap. A semaphore is the obvious guard if
cadence is ever pushed.
- **~~Synchronous O(n) prediction writes~~ — FIXED.** `store_prediction` used to
re-parse the entire `predictions.jsonl` on every append to compute the next id.
Now O(1) via a bounded tail read (`_last_jsonl_record`, `file_store.py`). Other
`FileStore` JSONL writes remain synchronous on the app loop but are single-line
appends.
- **External VLM internals.** How `gently_perception`'s `Perceiver` implements its
VLM call (async httpx vs sync-in-thread) is not inspectable from this repo; the
orchestrator awaits it, implying a coroutine.

## The model in one line

Two single-threaded event loops; hardware fenced into one process behind a
one-at-a-time plan queue; everything blocking pushed into threads; and the
slow/cognitive work (VLM, persistence, UI) decoupled with fire-and-forget tasks —
so the system *feels* concurrent while the microscope itself stays strictly
serialized.

---
*Generated from a code-grounded architecture pass (Claude Opus 4.8), 2026-07-01.*
112 changes: 112 additions & 0 deletions docs/HEURISTICS-AUDIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Heuristics audit — where to use the model (as a typed-output function) instead

Codebase sweep (5 parallel scanners + synthesis) for heuristics that **fake
judgment** an LLM would do better — in the spirit of the genotype→channel
refactor (drop the lookup table, let the model infer, keep a typed provenance
record + confirm-when-unsure). The flip side — logic that **must stay
deterministic** (safety, math, calibration, transport) — is listed at the end so
we don't mistakenly LLM-ify it.

The unifying move for every candidate: **LLM with a typed structured-output
schema + provenance + a confirm/UNCERTAIN escape**, never free-text-then-parse.

## Model candidates (ranked)

### High value

1. **Hatching / time-to-stage prediction** — `organisms/celegans/developmental_tracker.py`
*(the closest twin of genotype→channel; medium effort)*
Three hardcoded 20 °C lookup tables (`STAGE_TIMING_20C`, `TIME_TO_HATCHING`,
`TIMING_VARIABILITY`) plus magic `{HIGH:1.0, MEDIUM:1.5, LOW:2.0}` uncertainty
fudge factors. Structurally **can't use the rig's actual temperature** (we run
a TEC), the strain, or the embryo's observed progression rate. Let the model
produce a calibrated, explained interval; **keep the literature table as a
deterministic sanity bracket** and flag when the estimate falls outside it.
→ `{ predicted_minutes_to_hatching, low, high, basis, assumptions{temperature_c,strain,used_observed_rate}, confidence, reasoning }`

2. **Citation → PubMed query** — `harness/plan_mode/tools/research.py` (`_search_pmid`)
A regex that only handles "Surname et al YEAR …" + six hand-rolled query-
relaxation strategies + a stopword/word-position ladder that drops load-bearing
nouns. The model parses the sloppy citation and proposes relaxed queries; **code
keeps the deterministic esearch call and never fabricates a PMID.**
→ `{ author_last, year, journal, topic_keywords[], organism, pubmed_query, alt_queries[], confidence }`

3. **Lab-history retrieval** — `harness/plan_mode/tools/lab_context.py`, `harness/memory/interface.py`
Semantic recall faked by substring-OR over query tokens (matches "we"/"before",
misses every paraphrase). Feed the model the candidate records and have it
**rank/select from provided ids only** (no fabrication). Read-only, no
acquisition risk.
→ `{ matches:[{kind,id,summary,relevance,why_relevant}], answer }`

4. **Stage-label parse via 22-entry synonym dict** — `developmental_tracker.py` (`_parse_stage_name`)
*(small effort, pure robustness win)* The Vision call already classifies; the
brittleness is a plain-text `STAGE:/CONFIDENCE:` block scraped line-by-line, with
off-vocabulary phrasings silently collapsing to `UNKNOWN` (which kills the
downstream hatching prediction). Constrained-enum structured output deletes the
parser + synonym table.
→ `{ stage: enum(...), confidence: enum(high|medium|low), is_transitional, reasoning }`

### Medium value (mostly small — fix the output contract, not the judgment)

5. **Calibration Vision calls** — `hardware/dispim/claude_client.py`
Four Vision calls return positional free text recovered by `'yes' in first_line`
/ `re.search(r'\d+')` / first-valid-letter, with silent defaults (so "no, this is
not yes…" reads as *yes*). Typed output deletes the parse + silent-default layer.

6. **ML architecture ranking** — `ml/architectures.py` (`get_suitable_architectures`)
Hard feasibility gates (VRAM / dataset) are correct **and stay**; the `+2/+1/+1`
point-score ranking that follows discards the per-arch prose. Let the model rank
the *pre-filtered feasible set* (ids constrained to that set).

7. **Training label normalization** — `ml/data_loader.py` (`build_labels_from_store`)
Class space built by exact-string identity over free-text human annotations —
"1.5-fold" and "1.5 fold" become different classes. Model normalizes to the
canonical staging vocabulary, flags novel/ambiguous ones.

### Lower value

8. **"Plan has a control?"** — `plan_mode/tools/validation.py` — substring scan of a
6-word keyword set; a scientific judgment over the whole plan. Non-blocking
warning → safe for the model.
9. **CGC HTML scraping** — `research.py` (`_cgc_search`) — positional multi-group
regex over fetched HTML; structured extraction the model does better (HTTP GET
stays code; **mark strain names low-confidence to avoid sending someone to order
a hallucinated strain**).

### Cross-cutting batch (small each): typed output for the detector/verifier cluster
`harness/detection/verifier.py`, `app/detectors/hatching.py`,
`app/detectors/dopaminergic_signal.py`, `hardware/dispim/sam_detection.py` — all
already make the right model call but reconstruct the verdict via
`startswith`/regex-JSON-scraping with silent defaults. A batch move to native
structured output **strictly reduces parse-induced false negatives** without
touching the deterministic vote-tally/consensus/enum-dispatch downstream.

**Reference implementations already in the repo (imitate, don't change):**
`dopaminergic_signal`'s perceiver→classifier rubric (typed enums, UNCERTAIN
escape, conservative-on-tie) and onboarding's `_extract_with_llm` (typed
extraction, degrade-to-verbatim fallback).

## Keep deterministic (do NOT LLM-ify)
Safety, math, calibration, and transport — where a hallucinated value is unsafe
or breaks reproducibility:
- Laser-power safety limits + wavelength→MM-property map (`hardware/dispim/devices/optical.py`)
- SPIM trigger-timing arithmetic, piezo–galvo calibration, MM framing (`dispim/config.py`)
- Calibration prior EMA + R²≥0.75 slope-lock gate (`dispim/calibration.py`)
- SwitchBot GATT byte commands / status decoding (`hardware/switchbot.py`)
- Temperature setpoint bound [0,99.9] °C + stabilization I/O (`hardware/temperature.py`)
- Autofocus signal-processing, curve fitting, adaptive-sweep stop rules (`analysis/core.py`, `analysis/focus.py`)
- Classical-CV ROI detection + pixel→stage coordinate transforms (`detection.py`, `sam_detection.py` geometry)
- Timelapse rule dispatch + `confirm_timepoints` debounce + monotonic power ramp (`app/orchestration/timelapse.py`)
- Volume→b64 dark/flat calibration + fixed brightness scaling (`dopaminergic_signal._volume_to_b64` — deliberately non-adaptive)
- Wake-router debounce/throttle/stage-transition gate (`app/wake_router.py`)
- Plan hardware limits, detector-preset membership, dependency-cycle DFS, stage-order normalization (`plan_mode/tools/validation.py`)
- Ensemble vote tally + 0.70 quorum / unanimity consensus (`detection/verifier.py`)
- ML metric/aggregation math: confusion matrix, F1, federated averaging (`ml/evaluation.py`, `federated.py`)
- Core imaging geometry (max-projection, crop bounds, Euler rotations) + UI event reduction/routing/security (`core/imaging.py`, `ui/web/*`)
- Device-state SSE watchdog/staleness timers (`app/device_state_monitor.py`)
- Reference-type dispatch (PMID/DOI/URL by canonical syntax), `os.path.isfile` checks (`research.py`)

## Note
`gap_assessment.conversation_weight` (the 0.25/0.1/0.05 readiness scalar) is now
largely **vestigial** — it only returns 'heavy' (lab onboarding) or 'none' — so
it's not worth an API call. Left off the candidate list.
Loading
Loading