Release 0.22.0#51
Open
pskeshu wants to merge 95 commits into
Open
Conversation
…tages EmbryoState now carries position_coarse (bottom-camera detection or manual map placement) and position_fine (future SPIM-objective alignment) as separate fields; stage_position becomes a derived property (fine ?? coarse) so every existing call site keeps working. FileStore round-trips both stages; legacy position_x/position_y on disk backfill into coarse on read. Phase 1 of the Map-as-embryo-home arc -- schema only, no UI changes yet. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> (cherry picked from commit 3e41058)
Adds an EMBRYOS_UPDATE event type and wires ExperimentState's mutations (add_embryo / remove_embryo / assign_nickname / batch clear / editor finish) to publish a full embryo-list snapshot through the agent. The viz server's existing wildcard subscription forwards it to all browser clients, so Phase 3 can render embryos on the Map without polling. ExperimentState stays bus-agnostic via an on_embryos_changed observer hook; the agent wires the publisher at init. Phase 2 of the Map-as-embryo-home arc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> (cherry picked from commit 617e54c)
Adds an embryo layer to the Map between the axes and the live stage marker. Each embryo renders at its resolved XY (fine if SPIM-aligned, else coarse): coarse-only as an outlined lavender ring, fine-calibrated as a filled disc, both labelled with the embryo number. The layer is a pure read of EMBRYOS_UPDATE events plus an initial /api/embryos/current snapshot so a Map page opened mid-session shows existing embryos without waiting for the next mutation. Read-only at this phase; click / drag / delete will land in Phase 5. Phase 3 of the Map-as-embryo-home arc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> (cherry picked from commit 144d9fc)
…ol routes Adds Phase 4 of the Map-as-embryo-home arc. Auth (4a) --------- New gently/ui/web/auth.py introduces a two-role model: localhost is always control, remote callers default to view and need X-Gently-Token matching GENTLY_CONTROL_TOKEN to upgrade. Bottom-camera stream start/stop POST routes now Depends(require_control), so a remote browser can watch the stage but cannot drive hardware until an operator provisions the shared token. Marking canvas seeded (4b) -------------------------- VisualizationServer.start_marking_session takes initial_markers (pixel positions from SAM); they're seeded into the session state and the marking_image broadcast so the canvas opens with SAM detections pre- placed. wait_for_marking now also computes stage_x_um / stage_y_um from the operator-confirmed pixel positions, so callers can drop the result straight into agent.experiment.add_embryo. marking.js renders the seeded markers immediately and adapts the instruction string. detect_embryos -> web (4c) -------------------------- The agent tool now SAM-detects with open_editor=False (napari path bypassed), then hands off to the web Marking canvas via agent.viz_server.start_marking_session(initial_markers=...) and awaits wait_for_marking. Confirmed embryos land in agent.experiment, which broadcasts EMBRYOS_UPDATE -> Devices > Map shows them as coarse rings. Falls back gracefully if viz_server is unavailable or the operator never confirms. edit_embryos / manual_mark_embryos still use napari; deferred to a later phase. gently/ui/napari_viewer.py kept intact for offline use. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> (cherry picked from commit 4fbb9ed)
Map becomes the home for embryos rather than a viewer.
Backend (data.py)
-----------------
PUT /api/embryos/{id}/position {x, y} updates position_coarse and CLEARS
position_fine -- the operator overriding the sighting invalidates any
prior SPIM-objective alignment derived from the old coarse, so it must
be re-run. DELETE /api/embryos/{id} removes via ExperimentState. Both
endpoints Depends(require_control), so only the diSPIM box (or a remote
session with X-Gently-Token) can mutate the embryo list. Both fire
EMBRYOS_UPDATE through the observer hook for live Map refresh.
Frontend (devices.js + main.css)
--------------------------------
First click on an embryo selects it (dashed lavender ring, brighter
label -- the "picked up" state). Click on empty map space drops it
there with a confirm prompt; Delete/Backspace removes with confirm;
Escape clears the selection. New embryos still go through the bottom-
camera Marking canvas -- the Map is a schematic, not a satellite, so
adding without a visual reference would be guessing.
Keyboard handler is tab-aware (Devices tab + Map view only) and
ignores keystrokes while an input/textarea/select has focus so it
doesn't hijack the chat composer.
Smoke-tested end-to-end via ASGI: PUT clears fine correctly, DELETE
fires notify, error paths return 400 / 404 / 503.
Phase 5 of the Map-as-embryo-home arc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
(cherry picked from commit 8f6553e)
Tiger persists JoystickEnabled in non-volatile card settings. If a prior session ever ran SaveCardSettings while the joystick happened to be off, every subsequent boot inherits that state and the physical controller is dead. We don't run SaveCardSettings ourselves, so the only way to recover the joystick was a manual property write -- and there was no way to know the state had drifted until the operator tried to use it. DiSPIMXYStage gains enable_joystick(True) that writes JoystickEnabled + verifies read-back (same pattern as set_firmware_limits). device_layer calls it at boot right after the firmware soft limits are applied. Failure is non-fatal: agent can still drive the stage; we just log loud so the operator knows the joystick is unavailable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> (cherry picked from commit 808fe81)
Two improvements to the bottom-camera live thumbnail on the Map. Crosshair (FOV reticle) ----------------------- A centre crosshair anchored to the image, not the viewer rect. SVG sibling of <img>; an inner <g> receives the same translate/scale as the image (in viewBox units, via the SVG transform attribute), so the lines track the FOV centre through zoom/pan instead of staying pinned to the container centre. Transform sits on <g> rather than the SVG element so the renderer re-rasterises at each zoom step -- otherwise 1px strokes get bitmap-scaled and go blurry. vector-effect: non- scaling-stroke keeps them 1px at any zoom. Default colour amber (var(--map-warm)). Zoom / pan ---------- Scroll-wheel over the camera stage zooms in/out (1x to 8x, ~15% per notch) centred under the cursor. Click and drag pans when zoomed. Double-click resets to 1x. Pan is clamped so the image centre stays inside the visible window. Stream stop also resets the transform so the next session starts at 1x. wheel listener is passive:false so the page doesn't scroll under the operator's hand. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> (cherry picked from commit f7a13d6)
Substrate for testing candidate orchestrator architectures without
running real hardware. Three layers, all under gently/eval/:
EventCapture
Wildcard-subscribes to an EventBus; appends every Event to a per-
session events.jsonl (D:/Gently3/sessions/{id}/events.jsonl).
High-volume telemetry (DEVICE_STATE_UPDATE, BOTTOM_CAMERA_FRAME) is
filtered out by default so 12-hour sessions don't drown the
meaningful events under polling noise. Auto-starts in agent init.
Handles non-JSON-native payloads (numpy, Path, datetime, set,
Enum, dataclass, bytes) via a fallback serialiser.
EventReplay
Reads events.jsonl back; publishes via EventBus.publish_event() so
original timestamps survive (candidates can reason about historical
cadence). Fast mode (no sleep) and real-time mode with optional
time_scale. event_types() for cheap pre-flight histogramming.
DecisionLog + Decision + DecisionTrigger
Per-session decisions.jsonl record. Each Decision captures WHY the
agent woke up (trigger + detail), WHAT it saw (context summary,
recent event ids, prompt hash), WHAT it did (tool calls, response
text), and HOW it went (duration, error). Substrate for diffing
candidate decisions later.
ShadowRunner + OrchestratorCandidate + NoOpCandidate
Candidates subscribe to an EventBus alongside production but their
decisions are LOGGED, not enacted -- never permitted to touch
hardware by construction. ShadowRunner hosts a set of candidates,
isolates candidate failures from each other and from the live bus.
NoOpCandidate ships as worked-example and proof-of-life.
scripts/replay_session.py
CLI: replay a session by id-prefix, with optional --candidate
attachment, --real-time + --time-scale, and --histogram pre-flight.
15 unit tests in tests/test_eval.py covering capture filter, non-JSON
payloads, thread safety, replay round-trip (event_type / source /
data / correlation_id / timestamp all preserved), real-time cadence,
time_scale, malformed-line tolerance, decision-log round-trip,
shadow forwarding to multiple candidates, candidate-failure
isolation, and event-type whitelisting.
Phase 6 of the Map-as-embryo-home arc, unlocking offline iteration on
the world-model + decision-moment work (operator-action events,
wake triggers, tiered context).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
(cherry picked from commit d69cc21)
Completes the second half of the shadow-mode substrate. The agent now
writes a Decision row to a per-session decisions.jsonl every time
ConversationManager.call_claude returns — success or error. Pairs with
the events.jsonl from Phase 6a so a candidate replay can be diffed
against production turn-by-turn.
What a production Decision captures
-----------------------------------
- trigger always USER_MESSAGE for now (event/tick triggers land
with the wake-router phase)
- trigger_detail user message excerpt (200 chars)
- tool_calls aggregated across the multi-step tool loop — every
tool_use block Claude emitted during this turn
- response_text final assistant text
- prompt_hash short SHA-256 of (system_prompt, conversation_history)
snapshotted BEFORE the tool loop appends to history.
Same hash = same input; safe to compare candidate
decisions against this one.
- duration_ms wall time of the whole turn
- error set on the failure path; the exception still re-raises
to the caller so the existing error UX is unchanged
Wiring
------
- gently/eval/decision_log.py
new prompt_hash() helper (shared by production + candidates so the
fingerprint format stays consistent)
- gently/harness/conversation.py
ConversationManager gains decision_log field; call_claude collects
tool_use blocks across every Claude round, then writes one Decision
in both success and except branches. Best-effort: a DecisionLog
write failure never breaks the live agent.
- gently/app/agent.py
_init_decision_log opens session_dir/decisions.jsonl and assigns
to self.conversation.decision_log; stop_decision_log mirrors
stop_event_capture for shutdown cleanliness.
- tests/test_eval.py
+5 tests: prompt_hash stability and shape-tolerance; success path
captures tool_calls + response + prompt_hash + duration; error path
captures error + re-raises; no-log path is a clean no-op.
Phase 6f. 20/20 tests green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
(cherry picked from commit 75d7c9d)
Two pieces of the closed-loop paradigm, tightly coupled.
Operator-action events (vocabulary)
-----------------------------------
Three new EventType values for human-driven mutations. They're distinct
from EMBRYOS_UPDATE because they carry INTENT, not just state delta —
candidates can reason about "the operator just did X" without typing
that fact into chat.
OPERATOR_EDITED_EMBRYO PUT /api/embryos/{id}/position
payload: embryo_id + old/new coarse +
fine_position_invalidated
OPERATOR_REMOVED_EMBRYO DELETE /api/embryos/{id}
payload: embryo_id + last_position
OPERATOR_MARKED_EMBRYOS detect_embryos web-editor finish
payload: embryo_ids + count + stage_origin +
pre_edit_count
Map-edit routes publish via server.agent_bridge.agent._event_bus.
detect_embryos publishes only when the operator actually confirmed via
the web canvas (operator_marked flag) — if the editor was skipped, the
SAM list still landed in experiment.embryos but it wasn't operator-
confirmed, so no operator event.
ReactiveCandidate (first real candidate)
----------------------------------------
gently/eval/candidates.py — pure-rule shadow orchestrator with a tiny
world model (embryos + last stage + last error). Reacts to:
EMBRYOS_UPDATE ingest, silent
STAGE_MOVED ingest, silent
OPERATOR_EDITED_EMBRYO propose recalibrate_embryo if fine was invalidated
OPERATOR_MARKED_EMBRYOS propose calibrate_all_embryos for the new set
OPERATOR_REMOVED_EMBRYO propose forget_embryo for cache tidy-up
ERROR_OCCURRED escalate first occurrence, suppress same msg
within 30s
The thesis being tested: a rule-based responder can do the routine
bookkeeping that today only happens when the operator chats with Claude.
Shadow mode will tell us how often that thesis holds in practice.
Tests
-----
+7 ReactiveCandidate tests covering silent ingest, conditional
recalibrate, marked-set proposal, removal tidy-up, error
escalate/suppress, and a full event-stream-through-replay smoke that
proves the captured jsonl alone is sufficient input to drive a candidate
to a decision log. 27/27 green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
(cherry picked from commit 0a97563)
Distillation of the design conversation that produced the
paradigm/closed-loop branch:
- The four orchestrator roles and which one creates the friction
- Web/chat reconciliation patterns A/B/C/D
- Why 'turn' is the wrong unit and 'decision moment' is right
- Wake-router model (events + schedule + user input)
- Tiered world model (snapshot / digest / pull tools / lazy summariser)
- Five testing primitives ranked by payoff
- Coarse-vs-fine schema as 'measurement provenance'
- Map as collaborative world model
- Revolutionary trajectories: plans-as-goals, compounding learning,
collaborative world model, reverse-mode microscopy, continuous shadow
- What is built (commit table), what is not yet, and the open
questions for the next iteration
Future-self / new-collaborator reference, not a transcript.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
(cherry picked from commit 938baf8)
The console output from launch_gently.py is the most informative surface
in the system -- calibration progress, plan executor state changes,
perception decisions, MMCore callbacks. Until now it lived only in the
terminal and on-disk gently_*.log. This bridge fans it onto the EventBus
too, so the Events page in the viz server mirrors what the operator
would otherwise have to alt-tab to a terminal to see.
Backend
-------
gently/core/log_bridge.py
LogToBusHandler(logging.Handler) -- emit() publishes EventType.LOG_RECORD
with {level, level_name, logger, message, module, func, line, ts_ms,
exc_text?}. Per-thread re-entry guard prevents a subscriber's own log
call from spawning a cascade. Loggers in gently.core.event_bus and
gently.core.log_bridge are never bridged.
configure_log_bridge() reads three env vars:
GENTLY_LOG_BUS off/on (default on)
GENTLY_LOG_BUS_LEVEL threshold (default INFO)
GENTLY_LOG_BUS_INCLUDE_THIRDPARTY also bridge aiohttp/uvicorn/
bluesky/anthropic/httpx/httpcore
(default off -- keeps the page
readable; durable copy still in
gently_*.log)
Idempotent: re-attaching is a no-op.
gently/core/event_bus.py
EventType.LOG_RECORD added, plus inclusion in _NO_HISTORY_TYPES (log
records can fire hundreds-per-minute and would crowd out the bounded
history deque used for "meaningful" events).
launch_gently.py
configure_log_bridge() runs right after configure_logging() in main().
Single line, env-controlled, no API changes.
Frontend
--------
gently/ui/web/static/js/events.js
addEventToTable() branches on LOG_RECORD vs everything else. Log rows
render with a level-coloured badge (DEBUG / INFO / WARN / ERROR), the
logger name greyed before the message, and click-to-expand reveals the
full payload including stack traces.
gently/ui/web/static/css/main.css
Four new .event-type-badge.log-{debug,info,warn,error} classes
matching the existing badge palette. Monospace font for log message,
red tint for exception lines.
websocket.js already forwards everything except DEVICE_STATE_UPDATE /
BOTTOM_CAMERA_FRAME to the events table; LOG_RECORD inherits that
behaviour automatically.
Tests
-----
tests/test_log_bridge.py: 10 tests covering pass-through, level threshold,
exception capture, re-entry guard, bridge-internal logger skip,
GENTLY_LOG_BUS=off path, default attach, idempotency, third-party
exclusion default, opt-in third-party. 10/10 green; full paradigm suite
42/42.
Phase 9. On paradigm/closed-loop only.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
(cherry picked from commit 6318691)
SwitchBot Bot (WoHand) as a Bluesky/ophyd-protocol device over `bleak`
(set('on'|'off'|'press') -> Status, read/describe). Controls the diSPIM
room light used for bottom-camera imaging. Adds a standalone FastAPI test
GUI under diagnostics/ (buttons + morse blinker). Dep in requirements_device.txt.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
(cherry picked from commit a790b02)
Bluesky/ophyd-protocol device for the ACUITYnano Peltier controller. set(target) blocks until "[ SYSTEM LOCKED ]"; read() reports water temp, setpoint, state. Serial + MQTT transports plus a mock backend for hardware-free testing. Deps in requirements_device.txt. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> (cherry picked from commit 5f7912e)
… the agent
Config-gated registration of the SwitchBot and ACUITYnano devices alongside
the MMCore devices. Adds /api/temperature/{set,status} REST endpoints +
client methods, and set_temperature/get_temperature agent tools so the agent
can hold or shift sample temperature (C. elegans development rate).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
(cherry picked from commit 249ae4a)
New EventType.EMBRYO_TERMINATED fires whenever an embryo's imaging stops for any reason (no_object terminal, configured stop condition, errors, user removal). The orchestrator emits it from both the no_object terminal path and the per-condition stop check. TimelapseStateTracker handles it by marking the embryo complete and carrying the completion_reason through for the UI. Single source of truth for "an embryo has stopped" — downstream listeners (filmstrip terminated badge, summary stats) now have one event to subscribe to instead of polling embryo state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ive view The compact SPIM thumb in the metrics strip is now a button. Click opens a draggable, fixed-position popout (~560×480) that mirrors the same live frame stream with bigger imagery, the embryo label, and a close affordance. Hover/focus on the thumb shows a ⤢ chip hinting the interaction; the chip is hidden until a frame actually arrives. The popout reuses SpimLivePreview's apply-on-render plumbing so no new stream is opened — same data path, second render target. Keeps the calibration profile compact by default while letting the operator pull a properly-sized view when they need to read fine structure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The board's metric columns are restructured around developmental
time, the actual question this view answers ("is this embryo on
pace?"):
- 'clock' — elapsed wall-clock time in the current stage
- 'stereo' — stereotypic developmental position at 20 °C reference
- 'pace' — clock / stereo ratio; 1.0× means on reference pace
These replace 'confidence' (never populated meaningfully) and 'rate'
(misleading for slow embryos). 'eta' is now hatch-time, pace-corrected.
Migration: dashboardConfig loaded from localStorage runs an idempotent
filter that drops 'confidence' / 'rate' from the saved column list and
inserts the three new columns in the right slots. Existing user
configs upgrade silently on next load.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Opens the v0.21 development cycle on this branch. Targets per the KANBAN roadmap: cross-session resume, sacrificial vocab alias, campaign template loader (Path B), LDM Phase 1 MVP. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Modern SwitchBot Bot firmware (≥ v4.x) replies to press/on/off with a 3-byte status frame: 0x05 + battery% + flag bits. Older firmware returned the bare 0x01 success byte. The strict 0x01-only check raised SwitchBotError for any current-production Bot even though the press had landed (visible on the controlled load). Widen _RESP_OK to accept either prefix. Both indicate the command reached the actuator. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the BLE-attached SwitchBot Bot that toggles the room light into the device-layer config so DeviceLayerServer registers it on boot. Plans address it via `bps.mv(room_light, 'on')`. MAC is the bot already mounted on the rig. Reached over BLE via the TP-Link UB500 dongle on this desktop — RSSI -70 dBm, well within reliable range. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a dedicated status query (BLE 0x57 0x02) that returns battery percentage and firmware version without touching the actuator. Result is cached on the device instance and surfaced through read() / describe() as `<name>_battery_pct` and `<name>_firmware`, so the device-state stream picks them up automatically once polled. Verified on a Bot v4.2 over the TP-Link UB500: response `01 64 42 64 00 00 00 66 00 10 00 00 00` parses as battery 100%, firmware 0x42 (v4.2). Importantly, action-command responses are NOT used as a battery source — their byte-1 field looks like battery (an empirically 0x48-shaped value) but isn't: the dedicated query on the same bot reads 100%, so byte 1 of an action response is some other firmware-internal counter. Documented inline so the next reverse-engineer doesn't fall into the same trap. Periodic polling cadence is left to the caller; hourly is plenty for a battery that moves over months. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Begin the TUI->web convergence: - Floating agent-chat window in the web UI (agent-chat.js/.css, wired into index.html) connecting to /ws/agent: streaming text/thinking/tool calls, choice pickers, applied-spec cards, slash-command routing. All untrusted text is escaped before insertion. - Single-driver control lock in agent_ws.py: only the holder may drive the agent; other clients are observers with a "Take control" banner. Fixes the latent shared-conversation corruption when >1 client connects. - launch_gently.py no longer spawns the Node TUI. It starts the agent + viz server, prints a launch banner (URL, device status, storage, Ctrl-C), auto-opens the browser (--no-browser to suppress), and serves until interrupted. Removes the Node/dist requirement; --resume resolves to latest (interactive picking deferred to the browser). TUI source kept in-tree (reversible). Auth not yet added: the browser is now the only control path and is unauthenticated on the LAN. Bind to 127.0.0.1 or trust the LAN until self-managed accounts land. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consolidated plan from the codebase audit: robustness gaps, biologist-UX gaps, complexity audit (legitimate vs refactorable + ~4000 lines of dead duplicate code), frontend audit, startup/topology, multi-user auth + single-driver control arbitration, a 5-day plan, the web-only convergence roadmap (milestones A-F), and progress to date. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hat/control-lock/launcher work)
Auth — self-managed accounts gating the control surface: - accounts.py: file-backed user store (PBKDF2 password hashing, HMAC-signed session cookies, first-run admin bootstrap); roles viewer/operator/admin. - auth.py: resolve_role recognizes the session cookie in account mode (operator/admin -> control, else view); legacy localhost/token path kept when no accounts are configured. - routes/auth_routes.py: /login page + /api/auth/login|logout|me and an admin-gated /api/auth/users. - pages.py: main page redirects to /login when accounts require it. - agent_ws.py: /ws/agent authenticates via the session cookie; only operators/admins may hold/take the control lock, viewers watch only; the holder label is now the username (fixes ambiguous "a browser window"). - launch_gently.py: initializes the account store and prints first-run admin credentials in the banner (GENTLY_NO_AUTH=1 disables auth). - templates/login.html: clean on-brand sign-in page. Chat UX: - Activity indicator: instant "Working…/Thinking…" feedback with animated dots across the stream lifecycle; tool rows show spinner -> check. - Professional restyle (Inter Tight / JetBrains Mono, role labels, status pill); brand cell/embryo favicon replaces the Gemini-like sparkle; dropped the "Microscope assistant" subtitle; header shows signed-in user + Sign out. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The serve loop blocked on a bare asyncio.Event().wait(), which the Windows Proactor loop won't interrupt on Ctrl-C, leaving the server unstoppable. Install SIGINT/SIGTERM handlers (loop.add_signal_handler, falling back to signal.signal + call_soon_threadsafe on Windows) and poll a stop Event on a short interval so the interrupt surfaces and shutdown runs cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Opening the dashboard no longer redirects to /login — viewing is open to
everyone (the "watch like it is now" model). Login is an elevation to the
control role, not a gate on the app.
- pages.py: drop the /login redirect on the main page.
- agent_ws.py: anonymous clients may connect to /ws/agent and *watch*; only
authenticated operators/admins can hold/take the control lock (drive
actions stay gated). No more close-on-unauthenticated.
- agent-chat.js: distinguish anonymous ("Viewing — sign in to control",
with a Sign in button) from a viewer-role account ("view-only"); header
button is Sign in / Sign out accordingly.
API model: observable (read) endpoints + watching the agent need no auth;
only inputable (control) actions are gated via require_control / the control
lock — auth is not attached to every endpoint.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Complete the inputable-action gating beyond the REST routes already covered in data.py: - chat.py: the per-timepoint VLM follow-up (POST /api/perception/chat/...) now requires the control role — it spends API budget and writes traces, so anonymous viewers can't trigger it. - websocket.py (/ws): marking actions (embryo_marked / marking_update / marking_done / marking_redetect) are gated to control-role clients via the session cookie; pure read/presence messages stay open so anyone can watch. Deliberately NOT gated here: device-layer ingest (POST images/volumes — a machine trust domain, would break under account mode where localhost is no longer auto-control) and campaign mutations (their own mesh scope auth). These need a separate machine-token pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The conversation is now the same for every client of a session — operators and viewers, live and on reconnect/refresh. - Broadcast: user messages and the agent's streamed reply (text/thinking/ tool calls/choice requests) go to ALL connected /ws/agent clients via a raw-websocket registry, not just the driver. Observers watch live. - History: a display transcript is accumulated server-side and persisted to <session>/chat_display.json (user/agent/tool turns, capped to 500). On connect each client is sent a "history" message and rebuilds the transcript, so refreshes and late joiners see the full conversation. - Choice pickers are interactive only for the control holder; observers see them read-only and only the holder's choice_response resolves. - Client: handles "history" (rebuild) and "user_message" (live echo with author); stops double-echoing the sender's own chat (it now arrives via the broadcast); slash commands still echo locally (not broadcast). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
…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) <noreply@anthropic.com>
Gradually introduce mypy type checking (Phases 1-3)
Drop the .dev0 pre-release marker, add the v0.22.0 changelog entry, and gitignore the literal D:/ runtime storage dir that gets created on Linux when GENTLY_STORAGE_PATH resolves to a Windows path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collaborator
Author
|
Tested on hardware |
subindevs
reviewed
Jun 16, 2026
| _VISUALIZATION_AVAILABLE = False | ||
|
|
||
| __version__ = "0.20.0" | ||
| __version__ = "0.22.0.dev0" |
Collaborator
There was a problem hiding this comment.
@pskeshu version number is different than the release
Collaborator
Author
|
@subindevs good catch — |
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