Release 0.22.0#51
Open
pskeshu wants to merge 314 commits into
Open
Conversation
The Quick Start was stale for the 0.22 web-first line: it told first-timers to install Node and `npm run build` the Ink TUI (retired this epoch), used `pip install -r requirements.txt`, and documented nothing about the web UI or the admin bootstrap. Rewrite for reality: - Prereqs: drop Node/TUI; Python 3.10+, ANTHROPIC_API_KEY, optional GENTLY_STORAGE_PATH. - Setup: `pip install -e .`. - Launch: device layer + launch_gently opens the browser; document --no-browser, --offline, and the banner URL (default http://localhost:8080). - New "First sign-in (accounts)" section: viewing is open, login elevates to control; first run prints a one-time admin password to the console (never logged); how to add users (POST /api/auth/users), recover (delete users.yaml), and GENTLY_NO_AUTH=1 to disable auth. - Fix architecture note: the core store is the file-based FileStore, not GentlyStore (SQLite). Status line 0.11.0 -> 0.22.0.dev0. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
First-timers increasingly reach for uv, and the project is a standard PEP 621 pyproject so uv works against it with no extra config. Offer both paths in Setup instead of assuming pip/venv: `python -m venv` + `pip install -e .`, or `uv venv` + `uv pip install -e .` (lockfile-free — there's no uv.lock to maintain yet). Add a Launch note so uv users know to either activate .venv or prefix commands with `uv run`. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The 10-minute offline guide had the same stale steps as the README: it told readers to `pip install -r requirements.txt` and `npm run build` the terminal UI that this epoch retires. Bring it in line with the web-first reality and mirror the README's dual environment paths (venv+pip or uv), drop the Node prerequisite, and add the uv-run / Windows `set` variants to the launch step. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Fills the docs/EVAL.md TODO referenced by gently/eval/__init__.py. Documents the capture/replay substrate that already shipped (EventCapture / EventReplay / ShadowRunner / DecisionLog) and a grounded, incremental design for exercising the *real* wake-router + agent reasoning offline by replaying recorded sessions on a controllable clock. Covers the central wiring gap (replay currently targets a fresh EventBus the agent never subscribes to), four compared approaches (event-stream replay, Perceiver stub, full timelapse re-feed, shadow scoring), honest fidelity limits (recorded perception != new perception, LLM nondeterminism, wake-router coalesce/throttle vs replay clock, wall-clock reads), and a step-by-step build order. Referenced from the PR's "Where we'd especially value help" section. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ans viewer) The Quick Start got users installed and signed in but stopped at "open the URL" — it never led them into actually using the app. Add a walkthrough that takes a first user all the way through the core loop: - open the agent chat (header Agent toggle / Ctrl+J, or Home's Start button + the /wizard setup), - enter plan mode with /plan (agent as scientific collaborator, no hardware), - describe an experiment in plain language -> the agent drafts a campaign of typed plan items with concrete specs, - inspect it in the Plans tab (plan viewer): campaign card -> plan document, item statuses/specs + inspector, doc/board/graph/timeline views, versions. Notes that plan mode works fully offline (--offline), so reviewers can try the talk -> plan -> inspect loop without a microscope — which is exactly the hands-on interface feedback the PR asks for. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Relicense and update author list
Fix 500 error on every page under Starlette 1.x
- Adopt uv for env + deps; declare gently-perception (sibling repo) via [tool.uv.sources] and add python-dotenv. Refresh README setup/launch docs. - Migrate requirements*.txt into pyproject: drop the redundant requirements.txt, move device accessories (bleak/pyserial/paho-mqtt) to a [device] extra, and document the optional CUDA-torch install (kept opt-in, not a forced default). - Auto-load a project-root .env on startup; OS-aware ANTHROPIC_API_KEY message. - Add --no-api UI-only mode and an immediate startup log line before imports.
Switch environment setup to uv and add offline/UI-only launch
Pin pymmcore to device-interface 70 and restructure the environment
- config.yml: add `temperature:` block (serial backend on COM8) so the device layer registers a `temperature` device. - Web Devices header: live water-temperature readout + setpoint control - data.py: GET /api/devices/temperature/status, POST /api/devices/temperature/set - devices.js: temperature panel (poll + set); main.css pill; index.html markup - Vendor integration SDKs: MQTT (test_temperature_controller.py) and USB serial (test_temp_usb.py).
- piezo.py: harden DiSPIMFDrive with module-level hard travel limits
(F_DRIVE_MIN_UM=30, F_DRIVE_MAX_UM=25000), non-overridable from above;
configurable per-move Status timeout (120s) for slow full-travel traverses.
- device_factory.py: register the SPIM head as `fdrive` (ZStage:V:37).
- plans/acquisition.py: spim_head_focus_descent_plan (3-phase descent:
fast traverse -> coarse 1000um steps -> fine sweep-and-fit under LED),
register_views_xy_plan (dual-view XY registration), and the composite
spim_head_focus_and_align_plan. Also fix get_stage_position_plan to use
bps.rd's actual return value (it was indexed as a {name:{value}} dict).
- device_layer.py: register the new plans and mark them heavy.
- test_dispim_device_safety.py: F-drive bounds tests for the 30/25000 limits.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Integrate ACUITYnano temperature controller (config, web control, SDKs)
Add SPIM-head F-drive device, hard limits, and focus/align plans
- pyproject.toml: configure ruff (line-length=100, target-version=py310, select=E,F,I,UP,B) and add ruff/pre-commit to the dev dependency group. - .pre-commit-config.yaml: ruff --fix + ruff-format hooks, pinned to v0.15.17 (matches the installed ruff; the older v0.4.0 pin in the issue template produced inconsistent UP038/format results). - .github/workflows/lint.yml: CI job running `ruff check .` and `ruff format --check .` on every PR. - CONTRIBUTING.md: local setup instructions (uv sync, pre-commit install, pre-commit run --all-files); notes that mypy is tracked separately (#46). - Fix all ~716 pre-existing ruff violations across the codebase: E501 (wrapped long lines, line-length raised to 100), E402, F401, F841, B904, B905, B007, B023, B008, B027, UP035, E741 (ambiguous `l` -> `learning`), E722, F402, and F821 (including two real bugs: missing **kwargs in multi_embryo_calibration_session_plan, and an undefined-variable return in sam_detection._detect_with_sam). mypy was scoped out of this issue due to its size (3002 errors across 225/299 files); tracked as a follow-up in #46. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Add linting, formatting, and type checking with ruff, mypy, and pre-commit
Adds a lenient [tool.mypy] config with an explicit ignore_errors override list for the 113 modules that don't yet pass, runs mypy in the lint CI job and as a pre-commit hook, and adds mypy to the dev dependency group. New code is held to the standard; the override list shrinks as modules are cleaned up. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Annotates two implicit-Optional defaults and fixes a str/Path arg-type mismatch in create_timelapse_video. Removes the file from the mypy override list before it's even added. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Changes def f(x: T = None) to def f(x: T | None = None) to match PEP 484 / mypy's no_implicit_optional, matching the pattern already applied to bridge.py in #42. Removes 14 modules from the mypy ignore_errors override list (113 -> 99) that are now fully clean.
…errors The four ContextStore mixins (_intentions, _plans, _understanding, _ml_pipelines) call methods/attributes defined on the host class or sibling mixins (_conn, _tx, _now, _gen_id, get_plan_items, create_campaign, etc.), which mypy couldn't see on the mixins themselves. Add gently/harness/memory/_protocols.py declaring a StoreProtocol with these members and have each mixin inherit from it for typing only. Also fixes the remaining errors this didn't cover: two untyped dict literals in _plans.py needed explicit dict[str, Any] annotations, and three _ml_pipelines.py methods returning an Optional lookup right after an insert now assert the row exists. All four mixins are now mypy-clean; removes them from the ignore_errors override list (99 -> 95).
…y overrides Adds ctx_get(context, key) and retypes the require_* helpers in harness/tools/helpers.py to accept context: dict | None and return non-Optional success values, eliminating the dominant union-attr/arg-type pattern across the tool modules. Applies the same fix to memory_tools' local _get_memory and a handful of independent var-annotated/type issues. Removes all 16 gently.app.tools.* modules from the mypy override list (95 -> 79), cutting the underlying error count from 610 to 369. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…remaining type gaps - volume_tools.view_image: guard against a disconnected microscope client via require_microscope() instead of crashing on client.capture_bottom_image() - file_store/store register_volume: volume_data: np.ndarray = None -> np.ndarray | None = None (implicit-Optional missed in the earlier pass) - StoreProtocol: mark @runtime_checkable for consistency with gently.harness.protocols - TimelapseOrchestrator.start: embryo_ids: list[str] -> list[str] | None = None to match its actual None-handling and the docstring Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…onal CI installed mypy unpinned (`pip install mypy`) while the pre-commit hook pinned mirrors-mypy v2.1.0, so the two would diverge the moment a newer mypy released — "passes my pre-commit" would stop implying "passes CI". Pin all three sources to 2.1.0 (CI install, pyproject dev group, and the pre-commit rev) and cross-reference them so they move together. Also convert the one implicit-Optional missed in Phase 1: `view_image`'s `image: np.ndarray = None` in dispim/client.py (the other three params in that same signature were already converted). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Gradually introduce mypy type checking (Phases 1-3)
The header pill, the home landing line, and the agent dock each computed connection state from their own signal at their own time. home.js read state.connected exactly once at tab init — before the /ws handshake — and never corrected, so the landing showed "Offline — start the agent to connect" while the header pill showed "Online" and state.connected was true. Add a single sticky ConnectionStatus store (status-store.js) holding three distinct signals — gentlyConnected (/ws), microscopeConnected (/api/device-status poll), agentConnected (/ws/agent) — which replays its current snapshot to every new subscriber, so a late subscriber can never miss the initial state (the root of the bug). All three surfaces now read from / write to this store: - websocket.js onopen/onclose -> setGently (via updateGentlyStatus) - app.js fetchDeviceStatus -> setMicroscope; header renders via subscriber - home.js updateStatus reads the store and re-renders on every change - agent-chat.js setConn -> setAgent Verified live: after reload the home line and header pill agree (no more "Offline while Online"); no console errors. Bug #3 (idle event-count inflation) needs no code change: the high-frequency telemetry (DEVICE_STATE_UPDATE/BOTTOM_CAMERA_FRAME) is already excluded from the events table + count at websocket.js, and idle measurement showed the count is calm and dominated by LOG_RECORD. Co-Authored-By: Claude Fable 5 <[email protected]>
…irm, detect candidates Device layer + client: - Register bottom-cam focus Z (DiSPIMZstage, z_stage) in device_factory; poll it + the SPIM-head F-drive in the slow position stream. - Fenced read+nudge handlers for bottom_z and fdrive (reject out-of-range; the device classes hard-enforce limits; F-drive floor is sacrosanct). - Inject a focus score (analysis.core.calculate_focus_score on the full frame) into the bottom-cam + lightsheet stream payloads. - Client: get_bottom_z/nudge_bottom_z/get_fdrive/nudge_fdrive. Web routes (data.py): - detect_embryos now RETURNS candidates (no auto-register) for the marking canvas. - POST /api/devices/embryos/confirm — agent-free register of operator-confirmed markers into experiment.embryos (single source of truth), fires EMBRYOS_UPDATE. - Proxies: stage/bottom_z[/nudge], spim/fdrive[/nudge]. RIG-NOTE: z_stage MMCore axis assumed ZStage:Z:32 + limits (50,250)µm — rig owner to confirm. Nudges use direct device.set() (mirrors camera-exposure handler). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
New device-tab 'Operate' view (1st in the switcher, hotkey 'o'): a single guided flow mirroring the physical workflow, replacing the scattered detect/mark/SPIM UX. - Zone 1 Survey & mark: enlarged bottom-cam live + fenced bottom-Z focus nudge + live focus score; Detect (SAM candidates) or click-to-mark on a FROZEN frame; positions only (no forced roles); Confirm → register into the canonical list. - Zone 2 Embryos: the single source of truth (EMBRYOS_UPDATE) with per-embryo state chips (marked→centered→focused→imaged) + select. - Zone 3 Acquire: Center (stage move) → lower SPIM head (F-drive, fenced) → inline SPIM focus (lightsheet live + galvo/piezo/LED nudge + focus score) → Acquire volume. Professional, theme-aware styling (operate.css). DevicesManager activates/ deactivates OperateManager on view switch (streams only run while visible). Verified in-browser end-to-end (detect→mark→confirm→list→per-embryo cycle). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
On Confirm, persist the annotated bottom-cam frame + per-marker pixel & stage coords (+ source, capture pose, transform) as a labelled snapshot via the existing FileStore.register_snapshot sidecar — every confirmation becomes a training/eval example for a future SAM replacement. Best-effort: needs a client-supplied frame + active session; never blocks embryo registration. Frontend sends image_b64 + frame meta + pixel coords with the confirm payload. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- Device layer: _log_focus_trace appends (t, source, focus_score, bottom_z,
fdrive, piezo) to {session}/focus_traces.jsonl on every broadcast frame —
the operator's manual focusing is captured passively (no autonomous Z moves)
as autofocus-validation data. Best-effort, off the broadcast hot path.
- gently/analysis/focus_validation.py: offline replay — load traces, segment
sweeps, compare each focus metric's argmax to the human's resting Z, report
error stats + interior-peak/contrast quality (a flat curve = no safe autofocus).
- 8 unit tests (synthetic Gaussian sweeps) — all green.
Earns autofocus safety offline before any Z move near the objective.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…e workflow Replaces the three always-on ~340px cards (cluttered, no visible logic) with a single height-filling four-region surface, from an expert-driven redesign (diSPIM-ops + microscopy-UX + C. elegans-bio + critique → 3 candidates → synthesis): - Thin HEADER: a real phase stepper (Survey: Focus→Mark │ Acquire: Center→Lower→ Focus→Acquire) giving 'you are here', + an always-on safety status strip (HEAD/floor/LED/LASER/live-cam) that persists even when controls are hidden. - LEFT SPINE: a read-only dish mini-map (state-colored pins + stage crosshair) + the embryo worklist board (per-row 4-node progress track), the loop's spine. - CENTER: ONE live viewport whose camera source swaps per step, with decision instruments overlaid (markers / centre reticle+FOV box / floor gauge). - RIGHT RAIL: renders ONLY the active step's controls (progressive disclosure); a single renderStep() drives header+spine+viewport+rail from one state so they can't disagree. Exactly one camera + one step live at a time. Bug fixes from the critique: (1) 'focused' is earned at the SPIM-focus step, not on a stray F-drive nudge (new 'lowering' state); (2) LED force-closed on step-leave and view-leave (no more leak); (3) down-nudges auto-grey near the floor and XY centering is blocked while the head is lowered. Pure IA/reveal/gating over existing endpoints/streams/SSOT — no backend change. Verified end-to-end in-browser (focus→mark→confirm→per-embryo center/lower/focus/ acquire→retract); stepper, status, board, and mini-map all track in lockstep. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ves only in Operate Finishes the migration: the Map view keeps its read-only embryo dots; the Detect / mark / per-embryo Center panel (an interim home) is removed now that the Operate view owns that workflow. Drops the detect panel markup, its OperateManager- duplicating JS in devices.js (renderEmbryoListPanel/runDetection/centerOnEmbryo/ removeEmbryo/setupDetectWiring), and the orphaned CSS; keeps the top-right rail + XY readout. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Expert Opus workflow study + synthesis. Decisions: build all 3 phases; adaptive default monitoring = idle; live run monitored in-Operate (rail flips to run-spine). Tactics are the unifying object; new keystone = a deterministic Tactic Executor (first caller of resolve_scope_embryos). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
After Confirm, Operate no longer dead-ends into the manual loop; the stepper advances to a new ③ Run node and the rail shows a Run chooser. Tactics are the unifying object — every run mode targets the marked set. - Stepper: ① Focus → ② Mark → ③ Run; renderStep() drives a new c0 (chooser) and running (run-spine) state alongside a1/a2/b1-b5. - Role chip strip (marked default → subject; flip to reference) → POST /api/embryos/roles (new thin route: sets EmbryoState.role + fires EMBRYOS_UPDATE). Load-bearing: expression_monitoring scopes to role=='test'. - Mode A Manual → the existing per-embryo loop. Mode B Adaptive timelapse (interval/stop/monitor, default idle) → POST /api/devices/timelapse/start with embryo_ids=[subjects]. Mode C3 Hand-to-agent → AgentChat with the roster. Library/plan modes stubbed (wired in Phase 3). - Live run-spine in the rail (reads GET /api/operation_plan, falls back to a summary card) + Pause/Stop/Resume (new /api/devices/timelapse/stop|pause|resume routes) + Open-in-Operations. Verified in-browser: mark→confirm→chooser→roles→adaptive start→run-spine, and Manual→b-loop, survey→a1. Backend timelapse start is rig/session-gated (503 w/o orchestrator); UI flow + routes verified. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- /api/devices/timelapse/start now seeds the session Operation Plan with a standing_timelapse tactic (+ a reactive_monitor layered_on when a monitoring mode is active) scoped to the marked set, transitioned active — closing the 'UI timelapses skip plan linking' TODO. Best-effort (needs a live session + context store); never blocks the start. So the Operate run-spine + Operations tab now show a real tactic for a UI-started timelapse. - start_adaptive_timelapse gains tactic_id (transitions the tactic active on success) — lifecycle symmetry with stop/pause/enable_monitoring_mode/queue_burst. Note: no tactic-schema change needed — _validate_tactics copies structure verbatim, so cadence_s/stop_condition/monitoring_mode/interval are already allowed. Seeding is rig/session-verified (no session in the hardware-free shim). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
… modes Keystone: gently/app/orchestration/tactic_executor.py — execute_tactic(agent, tactic) resolves scope (the FIRST caller of resolve_scope_embryos) and dispatches by kind to the orchestrator (standing_timelapse[+monitoring] / reactive_monitor / exclusive_burst / oneshot), then marks the tactic active. One kind→action map the agent and the UI both go through. 8 unit tests (mocked orchestrator/roster), all green. - POST /api/operate/run-tactic — append a tactic (or instantiate a saved one via library_id) re-scoped to the marked set, then execute it via the executor. - Operate Run modes wired: From-library → run-tactic(library_id); Manual → fires a cosmetic oneshot so the sweep shows on the spine; Continue-a-plan & Hand-to-agent → AgentChat hand-off (the agent owns plan resolution + composed tactics). - Run-spine now renders the real Operation-Plan tactic cards (GET /api/operation_plan). Verified in-browser: chooser→library select→run-tactic→running run-spine shows the tactic; run-tactic route 200 end-to-end in shim. resolve_scope_embryos is no longer orphaned. (No tactic-schema change needed — structure is free-form.) Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Adversarial review (Opus workflow, 8/8 confirmed) → all fixed:
- HIGH: Run chooser was a one-time gate (unreachable after a manual run or
Survey/mark-more). The 'Run' stepper node is now a clickable re-entry to the
chooser; finishing a manual sweep returns to the chooser, not Focus.
- MED: timelapse/start seeded scope embryos:[] when embryo_ids omitted (run images
ALL) → now records {mode:'global'} so the plan matches the run.
- MED: B1 (Center) showed a frozen frame — the chooser stops all cameras and B1
had no Start button. Bottom cam now auto-starts at B1 (live centering feedback).
- MED: adaptive 'after N timepoints' degraded to manual (never stops) — now sends
the combined 'timepoints:N' / 'duration:Nh' form the parser understands.
- MED: stop/pause/resume never reconciled the seeded tactic + every Start appended
a new active one → accumulating stale/duplicate active timelapses. Now: seeded
tactics are tagged + linked to the run; stop→done, pause→paused, resume→active;
prior active operate tactics are retired on a new Start; skip seeding when start
was a no-op ('already running').
- LOW: run-tactic 500 on a malformed tactic → 400 + append_tactic_to_plan defaults
kind='custom'.
- LOW: run-spine fallback hard-coded the adaptive shape ('undefineds' for a library
run) → branches on mode, guards the interval interpolation.
- LOW: manual mode discarded chooser role toggles → applyRoles() now runs for every
mode (moved to the top of startRun).
Verified in-browser (chooser re-entry, 400 path) + 16 unit tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…-all' into feature/temperature-operations-all
… visibility Opus audit: settings panel is 100% localStorage display prefs; the thermalizer connection (serial/MQTT/mock) is YAML-only + device-layer-restart. Design: a server-backed Hardware/Thermalizer section (Test + live hot-swap w/ restart fallback, sidecar persistence, secrets redacted), Mock dev-only, + a read-only effective-config viewer. Decisions: full build; live-swap w/ restart fallback; Mock dev-only. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ewer The Settings panel was 100% localStorage display prefs. Adds a server-backed 'Hardware / Thermalizer' section to view + edit the controller connection, and a read-only effective-config viewer. Device layer (aiohttp): GET /api/temperature/config (redacted), POST /api/temperature/config/test (transient probe, non-committing), POST /api/temperature/config (live hot-swap: 409 if RunEngine running / ramp lock held, build-new-before-swap, close old, sidecar-persist; restart-required fallback on connect failure). config.local.yml sidecar merged over config.yml at boot (keeps comments; 0600 perms for the MQTT password). Validator + password redaction. Client: get/set/test_temperature_config. Viz proxies (require_control on write/test) + GET /api/config/effective (settings.py + config.yml, secrets shown as present/absent booleans; restart-required note). UI: Hardware nav group; Thermalizer section (Serial/MQTT; Mock dev-only via ?dev=1) with Test + Apply (explicit, no auto-save), applied-live vs restart-required banner; a separate ThermalizerSettings JS module isolated from the localStorage SettingsManager; read-only Effective-config viewer. Relabeled the Vitals 'Temperature model' → 'Developmental-timing reference' to kill the naming trap. Verified in-browser (render, serial↔MQTT toggle, Mock-hidden, Test graceful 502, effective-config populated). Rig-only: real serial/MQTT connect + live-swap need the vendor SDK + device layer (shim shows 'not available'/502 gracefully). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- MED: Test connection now preserves the stored MQTT password (shared _preserve_temp_password helper), so Test probes the same credentials Apply commits — a valid live config is no longer falsely reported broken. - MED: thermalizer/effective edits no longer trigger SettingsManager's localStorage auto-save or its false 'Settings saved' toast (change listener now skips #section-thermalizer/#section-effective). - LOW: the device-layer 409 (run/ramp active) is flattened to 200 by the proxy, so the UI detects it via a body 'blocked' flag instead of the dead res.status===409 branch — shows 'Blocked:' not a generic 'Failed:'. - LOW: sidecar config.local.yml is created 0600 atomically (os.open O_CREAT 0o600) so the plaintext MQTT password is never briefly world-readable. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…red editors Completes the settings-panel Phase 2 alongside the effective-config viewer. Dashboard-pref defaults (server-backed): GET/PUT /api/config/dashboard-defaults (JSON in config/). settings.js now layers effective config as hardcoded < rig-wide server defaults < per-browser localStorage, plus a defaults bar — Save-as-rig-defaults, Reset-to-defaults, Export, Import — so prefs aren't trapped per-browser. Restart-required editors: an allowlisted set of settings.py knobs (timeouts, mesh timing, ML, ux_v2, NCBI) editable via GET/PUT /api/config/settings-overrides (require_control), persisted to config/settings.local.yml. settings.py now merges that file into the environment at import (os.environ.setdefault, so a real env var still wins) — every entry point picks it up on the next restart; the frozen settings singleton is never live-mutated. New 'Advanced' panel section renders the knobs with a 'Save (restart required)' banner. Also: made the sticky settings footer opaque (var(--bg-dark) + top border) with the defaults bar laid out inline, fixing the transparent-overlap on long sections. Runtime override files gitignored. Verified in-browser: server-defaults layering, Advanced render (13 knobs) + save round-trip to settings.local.yml, fresh-import applies the file while a shell env var still wins, footer no longer overlaps content. No console errors. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…dvanced UI
Auditing the exposed knobs showed several with ZERO runtime readers — editing them
would be a silent no-op, and one was a vestige of the removed RPyC layer:
- Removed settings.timeouts.rpc_call entirely (RPyC-era; 0 readers repo-wide; the
HTTP client uses its own aiohttp ClientTimeout). Dropped from the effective viewer.
- Pruned the Advanced editor to knobs with verified live readers: timeouts
{volume_acquisition, api_call}, mesh {broadcast/stale/dead}, ui.ux_v2, api
{ncbi_tool, ncbi_email}. Omitted timeouts.plan_execution + ml.{batch,epochs,lr}
(0 readers today — no-ops).
- Grouped the Advanced section (Timeouts / Mesh network / Interface / NCBI) with
subheadings instead of a flat list.
Verified in-browser: 8 grouped knobs, rpc_call absent, save round-trip intact.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
store_prediction computed the next prediction_id by _read_jsonl()-ing and JSON-parsing the ENTIRE predictions.jsonl on every append — O(n) per prediction, quadratic over a long timelapse, and a synchronous blocking read on the app event loop (the perception-persist path). Replace with _last_jsonl_record(): read a bounded 64 KB tail window and take the last record's prediction_id + 1. Ids stay sequential (we only ever append in order); robust to a trailing partial line from an interrupted write. Verified: identical ids, 20k-record file read in 0.26 ms. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Code-grounded map of how gently manages concurrent work: the two-process split (app/viz :8080 <-> device-layer :60610 over HTTP + shared filesystem), one asyncio loop per process, hardware serialized behind a single plan queue + executor (and pymmcore g_core_lock), how polling coexists with long experiments (to_thread offload, split pollers, reference-counted pause_state_updates, telemetry bypassing the queue), filesystem image transfer, fire-and-forget perception/VLM + EventBus, backpressure/failure isolation, and known limits (incl. the now-fixed O(n) prediction write). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…fline These read-only proxy GETs are polled by the manual/settings pages. When the device layer isn't running, _resolve_client() returns a client that is not connected, so the call raised ConnectionError and logged a full ERROR traceback on every poll — noisy for an expected state (UI open without the device process). Pre-check client.is_connected and return a quiet 503 instead; genuine failures (connected but call errors) still log at ERROR. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Control routes 403 in account mode when no operator is logged in — previously the only signal was a bare console 403 and a button that silently did nothing. Add a single global fetch wrapper (control-auth.js) that detects a 403 on a mutating /api/ request and shows a throttled 'Control required — Log in' toast (reusing showGentlyToast) whose action navigates to /login. Reads only res.status, never consumes the body, so callers are unaffected. Verified end-to-end in account mode: real 403 -> toast with the message + Log in button -> navigates to /login. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Vendor granted permission to redistribute their serial transport, so it's now bundled under gently.hardware.vendor instead of requiring a per-machine install. temperature.py loads it via _load_vendor(), which prefers a system-installed copy of the same package name (letting an official vendor update override) and falls back to the bundled copy. The MQTT transport (acuitynano_precision_thermalizer_api) is deliberately NOT bundled: it embeds broker credentials. Install it on the device-layer machine for backend: mqtt. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Cherry-pick of 87a52c5 (gently-project/development): the device-layer stack (pymmcore/bluesky/ophyd + bleak/pyserial/paho-mqtt) moves from the base deps and the `device` *extra* into a `[dependency-groups] device` group that is default-on via `[tool.uv] default-groups = ["dev", "device"]`. Dependency groups are not pruned by `--extra` selection, so `uv sync --extra sam --extra torch-gpu` no longer strips pyserial/paho-mqtt/ bleak (which broke the temperature serial/MQTT backends and the SwitchBot room light). Drop the stack on a no-microscope machine with `uv sync --no-group device`. Lock regenerated locally rather than taking the upstream uv.lock (branch has diverged); dependency set is unchanged, only group markers move. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Two regressions in the operate-tab detect/mark flow that made calibration image empty field: 1. Marking stage origin. The bottom-cam stream carried no stage position, so the operate canvas converted clicked pixels to stage coords relative to [0,0] instead of the true stage XY — embryos landed hundreds of µm off and calibration saw blank buffer. The device layer now stamps the real XY onto every bottom-cam frame (_current_stage_xy, never [0,0]); operate.js prefers that, falls back to the position stream, and BLOCKS marking (rather than silently using [0,0]) when neither is known. confirmMarks() guards the same. 2. SAM room-light fallback. detect_embryos forced the camera LED off before trying the room light; a missing/failed SwitchBot then left the frame near-black and SAM found nothing. Now the LED is only disabled once the room light is actually on; otherwise it falls back to the camera LED. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The operate spine went B3 focus → B4 acquire with no calibration, so volumes
were acquired without a per-embryo piezo↔galvo fit. Add a "Calibrate" step
(node bc) between Focus and Acquire:
- Frontend: new stepper node + rail group, a 'calibrated' state ranked between
focused and imaged, wired into the single renderStep() driver (step map,
camera map, stepper order, board/minimap colors). Focus now advances to bc;
Calibrate (or Skip) advances to Acquire. Acquire's guard relaxes from
==focused to >=focused so a calibrated embryo can be imaged.
- Backend: POST /api/devices/embryos/{id}/calibrate runs the agent's proven
calibrate_embryo tool via the registry (Claude-vision edge detection +
adaptive sweep, which sets the light-sheet laser config per snap) with an
injected agent/client context — no LLM orchestration. Result persists on
embryo.calibration; the rail shows slope + R².
Engine choice: reuses calibrate_embryo rather than the bare
calibrate_piezo_galvo_plan, which defers to focus_sweep_plan and does not
clearly set the laser config (risking the dark-frame failure just fixed).
Not yet hardware-verified end to end — the step + wiring are in place for a
rig test.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The Devices > Map viewBox was computed from only the firmware fence box and the current stage marker — embryos were never included. SAM/marked embryos often sit hundreds of µm outside that frame (e.g. a detection cluster near stage X≈-800 while the fence box straddles origin), so they rendered at the map edge or off-screen and looked mis-positioned, even though their stored coords are correct absolute stage µm. - computeViewBox() now expands the bounds to include every embryo's resolved XY (fine if aligned, else coarse). - handleEmbryosUpdate() now always recomputes the viewBox (via computeViewBox's changed-return) and does a full renderMap() when the frame widened, instead of only reframing on the first-ever load. Newly-arrived embryos now pull the frame out to include them. Not a coordinate bug: verified the SAM path records absolute stage coords (stage_position + pixel offset), consistent with the map's stage→SVG transform. This is purely framing. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…WS disconnect spam Two robustness fixes for long-running sessions: - registry.execute() now coerces incoming string arguments to their annotated int/float/bool types before invoking the handler. Tool inputs arrive as JSON from the model (and sometimes as UI form strings), so a param annotated `float` could show up as "120" and crash on a numeric comparison — e.g. set_embryo_cadence raised '<' not supported between instances of 'str' and 'int'. Coercion is conservative: only string values whose annotation resolves to a scalar are touched; unparseable values pass through for the tool to validate. - log_config now silences the `websockets` library's ERROR-level "data transfer failed" tracebacks (raised on every ungraceful client disconnect / WinError 121). These are routine and flooded the console, burying real errors. Set below CRITICAL so genuine faults still surface. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Resolve conflicts: - gently/app/agent.py: union of both sides — keep development's typed monitor annotations + TYPE_CHECKING imports alongside this branch's temperature_sampler, operation_plan_updater, lightsheet_monitor - uv.lock: regenerated with 'uv lock' from the merged pyproject.toml
The merge of development made mypy check function bodies the PR branch had left untyped (development annotated the enclosing defs), surfacing 11 cross-branch errors; both parents were individually clean. - agent.py: annotate temperature_sampler / operation_plan_updater / lightsheet_monitor attributes; guard seed_operation_plan_from_plan_item against a None session_id - client.py: assert _session before lightsheet stream GET (house pattern); type the live-params body dict - device_layer.py: annotate self.config and the sidecar 'existing' dict - data.py: annotate new_tactics - ruff format over 7 files the two branches formatted differently
…ions-all UX v2 · Temperature + Operations/Tactics + Operate (bottom-cam→SPIM) suite
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release 0.22.0 — promotes
developmenttomain.Bumps the version (
0.22.0.dev0→0.22.0) acrosspyproject.tomland the README, adds thev0.22.0changelog entry, and gitignores the strayD:/runtime dir.Highlights since v0.9.2
FileStore/FileContextStorereplaceGentlyStore/agent_mind.db. All state is human-browsable YAML/JSONL/TIFF.Merge method: merge commit (preserve history). Tag
v0.22.0+ GitHub Release to follow on the mergedmain.🤖 Generated with Claude Code