Supervisor pivot: keep one healthy remote-control server per folder #28
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
hiten/claude-code-pwa!28
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/supervisor-pivot"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Turns the PWA from a session launcher into a supervisor that keeps exactly one
claude remote-controlserver (an anchor) alive per declared folder, restarting on crash/reboot and raising an attention banner or web-push when a folder needs a human.Highlights
supervisor.py: desired-state persistence, trust seeding, session-scoped tmux discovery, single-lock reconcile loop, status-aware Kuma heartbeat with stall watchdog. All writes atomic; a malformedsupervised.jsonkeeps the last-good state instead of collapsing to[]./api/supervised*CRUD + restart + adopt-from;POST /api/sessions(the old launcher) and dormant/history/shell code removed.?environment=env_…deep link.needs_attention/unhealthy, tag-collapsed so a stuck/loginonly lights up once.sup-*window sweep.Regression fixes on top of the initial pivot
list-panes -s(session-wide, not active-window-only).#{window_id}match, notis_adoptable()— the claude proc isn't a BFS child of pane_pid on the very first tick after spawn, and treating that as "not ours" spawned duplicates.new-window -P -F "#{window_id}"for atomic wid capture; send-keys targets the wid so a same-named orphan cannot intercept._READY_REextended to match the real CLI 2.1.263 banner (Connected · <name> · <branch>/Continue coding in the Claude mobile app/Capacity: N/M).restartscounts respawns only, not the initial spawn.Tests: 210 pass; adds unit + API + reconcile + push + Playwright E2E coverage for the whole flow.
Docs: README + AGENTS + BookStack Overview/Operations (pages 81, 82) rewritten for the supervisor model. Manual step: first-time folders may still need
/loginaccepted once via the in-app terminal.Verified live on the box against three folders (RadioShake, IronPulse, NASDocker) — no window disruption across multiple service restarts, all three land at
status: ok, capacity + branch surface on the card.https://claude.ai/code/session_01RXuwdoZzGeWqZGih5iehQH
`claude remote-control` (CLI 2.1.263) rejects --model with "Unknown argument: --model" — the model is chosen per-session inside the Claude app once it connects to this anchor. - supervisor.spawn_command(name, claude_bin): no more model arg - SupervisedAdd / SupervisedPatch: drop model field - POST /api/supervised, PATCH /api/supervised/{sid}, adopt-from, _seed_supervised(): stop writing model - Frontend: remove model <select> from add-folder form, drop model from POST body, drop model span from supervised card render, delete loadModels() dead code - Tests: spawn_command call sites; new assertion that --model is never emitted; state fixtures unchanged (loader tolerates a legacy `model` key on disk) - SW cache bumped v32 → v33 Old supervised.json entries with a `model` key still load — the field is just ignored on the next save. Claude-Session: https://claude.ai/code/session_01RXuwdoZzGeWqZGih5iehQHFour bugs, all producing the same visible symptom (a supervised folder staying "starting" while the tmux session filled with orphan sup-<name> windows on every reconcile tick): 1. discover_anchors ran `tmux list-panes -t <session>` without `-s`, which scopes to the session's CURRENT window only. As soon as focus moved (e.g. after spawning a second folder), the first folder's anchor vanished from discovery and reconcile spawned a duplicate. (Fix already applied by the E2E subagent; kept.) 2. reconcile_one's "already tracked?" branch gated on is_adoptable() of the live window. On the first tick after a fresh spawn, `claude` isn't yet a BFS child of pane_pid (bash still reading the send-keys line), so cmdline=[] → is_adoptable=False → fall through to spawn-fresh → duplicate window. Fix: wid match IS ownership; only readiness of the pane text matters. 3. Spawn-fresh path resolved the new window id by re-listing windows and matching on name — ambiguous whenever a same-named orphan existed, sending send-keys to the wrong pane (or failing with "can't find window") so the new pane stayed empty. Use `new-window -P -F "#{window_id}"` for atomic wid capture and target send-keys by wid. 4. Both branches treated "starting" as a failure. Zero-settle capture right after send-keys was always "starting", so _fail incremented consecutive_failures and scheduled a respawn on the very next tick. Fix: "starting" is transient — record window and wait. Also: - _READY_RE didn't match CLI 2.1.263's actual banner ("·✔︎· Connected · <name> · …" then "Continue coding in the Claude mobile app or https://claude.ai/code…") so a healthy anchor was never promoted to `ok`. Regex extended. - reconcile_all sweeps `sup-*` windows whose wid isn't owned by any folder (leftovers from the old bug or a delete that raced the loop). - app.py `_ensure_tmux_session()` runs `has-session || new-session` before the loop starts — the launcher path that used to create the base session was removed in the pivot, so after a reboot the loop had nothing to `new-window -t` into. - SW cache v33 → v34. Verified live on the box: RadioShake spawns one window, promotes to `ok`, restarts stays at 0. Claude-Session: https://claude.ai/code/session_01RXuwdoZzGeWqZGih5iehQHcd92e93a51e5503c6Everything remaining from the "improvements" list except the PR-to- master step: FastAPI lifespan: - Replace @app.on_event("startup") with an @asynccontextmanager lifespan that also cancels background tasks on shutdown. Silences the deprecation warning and stops leaving orphaned supervisor sleeps that raised CancelledError in the terminal_ws suite. Ready-banner parsing on the card: - supervisor.parse_banner() pulls capacity, branch, and the https://claude.ai/code?environment=… deep link out of the CLI 2.1.263 banner. Populated on `ok`, cleared elsewhere. - Card renders `capacity N/M`, a branch tag, and (when present) wraps the folder name in an anchor tag pointing at the environment URL — one tap jumps into the Claude app for that anchor. - _READY_RE also matches Capacity / Reconnected after / New sessions will be created — these lines are only printed by a healthy server and cover a long-running anchor whose original banner has scrolled off the top of the capture buffer. - capture-pane bumped from -S -200 to -S -2000 for the same reason (tmux caps its own history_limit so the returned bytes stay bounded). - The "already tracked" AND the "claim orphan" branches now both parse the banner, so the card shows the extras on the first tick after adoption instead of the second. Web Push (VAPID): - push.py: VAPID keypair generated once and stored under ~/.claude-pwa/vapid.json, subscription list at push_subs.json, atomic writes, malformed-file tolerant. push_notify() dispatches best-effort and prunes 404/410 endpoints permanently. - New endpoints: GET /api/push/vapid-key (public), POST/DELETE /api/push/subscribe (auth required). - Supervisor loop calls _detect_and_push_transitions() after each tick and pushes ONE notification per fresh transition into needs_attention or unhealthy (repeat suppression via an in-memory {sid: status} cache; a return to ok/paused re-arms it). - Static SW handles `push` (showNotification with tag-based collapse) and `notificationclick` (focus/open PWA at the given URL). - Frontend: a 🔔 header button appears iff the browser supports Push and no subscription exists; tap → permission prompt → subscribe → POST to backend. Hides itself once subscribed or denied. - Added pywebpush + cryptography to requirements.txt. - 10 new push tests; webpush() is patched so no real HTTP goes out. SW cache bumped v34 → v35. Verified live on the box: 3 supervised folders survived the service restart with no window disruption, all `status: ok`, capacity/branch now surfaced. Claude-Session: https://claude.ai/code/session_01RXuwdoZzGeWqZGih5iehQH