Skip to content

feat(adr-18): phases 8-10 — ptyId persistence, UI-only update, daemon default-on#122

Merged
MenachemBarak merged 5 commits into
mainfrom
feat/daemon-keepalive
Apr 29, 2026
Merged

feat(adr-18): phases 8-10 — ptyId persistence, UI-only update, daemon default-on#122
MenachemBarak merged 5 commits into
mainfrom
feat/daemon-keepalive

Conversation

@MenachemBarak
Copy link
Copy Markdown
Owner

Summary

  • Phase 8: Frontend ptyId persistence and reconnect. TerminalPane now accepts onPtyReady prop and stores the daemon-assigned ptyId back into the spawn tree via updatePaneSpawn in app.jsx. On WS close, if window._daemonToken is set the pane retries up to 8 times (2s gaps) before marking exited. Stale ptyId (server replied "not found") is cleared automatically so the next reconnect triggers a fresh spawn. UpdateBanner uses POST /api/update/apply-ui-only in daemon mode and calls window.pywebview.api.close_for_update(). New DaemonDisconnectedBanner polls /api/health every 5s and surfaces a reconnect button.
  • Phase 9: Two-binary split. _PywebviewApi.close_for_update() exposed to JS via pywebview js_api. updater.DAEMON_MODE flag makes download_and_stage write to state_dir/staged-ui.exe and enables apply_ui_only_update() which swaps only AgentManager.exe while the daemon keeps running. /api/update/apply-ui-only wired to the real implementation (was 501 stub). daemon/__main__.py sets DAEMON_MODE=True. New pyinstaller-daemon.spec builds AgentManager-Daemon.exe (headless, no webview). CI builds + renames the daemon exe and publishes it as a release asset alongside the UI exe.
  • Phase 10: AGENTMANAGER_DAEMON default flipped to "1" — daemon mode is now opt-out (=0 disables it).

Test plan

  • Start daemon (python -m daemon), open UI — DAEMON chip visible in title bar
  • Open a terminal tab, note ptyId stored in layout state (viewer-terminal-state.json)
  • Restart UI only — terminal tab reattaches and replays ring buffer with no interruption
  • Kill daemon while terminal running — DaemonDisconnectedBanner appears; Reconnect button clears it when daemon comes back
  • Stage a fake update (POST /api/_test/seed-update-state), click "Restart & apply" in daemon mode — apply-ui-only endpoint called, close_for_update fires
  • Set AGENTMANAGER_DAEMON=0 — legacy in-process mode starts normally

🤖 Generated with Claude Code

…-mode default-on

Phase 8 (frontend): TerminalPane stores ptyId via onPtyReady prop; WS
reconnect logic retries up to 8 times on daemon disconnect; stale ptyId
cleared automatically when server returns "not found". app.jsx wires
updatePaneSpawn helper + handlePtyReady callback so spawn trees carry
live ptyId. UpdateBanner forks apply path for daemon mode (apply-ui-only
+ close_for_update). DaemonDisconnectedBanner polls /api/health every 5s
and surfaces a reconnect button.

Phase 9 (backend): _PywebviewApi exposes close_for_update to JS via
pywebview js_api. updater.DAEMON_MODE flag routes staged path to state_dir
in daemon mode and enables apply_ui_only_update(). /api/update/apply-ui-only
endpoint wired to real implementation. daemon/__main__.py sets DAEMON_MODE=True
before uvicorn starts. pyinstaller-daemon.spec builds AgentManager-Daemon.exe
(no webview, console=True). CI workflow builds + renames daemon exe and
publishes it as a release asset.

Phase 10: AGENTMANAGER_DAEMON default flipped to "1" (opt-out via =0).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread backend/cli.py
import webview as _wv

_wv.destroy()
except Exception:
MenachemBarak and others added 4 commits April 29, 2026 17:10
…ontract

webview.destroy() exists at runtime but is absent from pywebview's mypy
stubs; add type: ignore[attr-defined] to silence the false positive.

The phase-7 stub test expected 501 until Phase 9 shipped; now that
apply-ui-only is implemented it returns 200/ok=false in non-daemon test
env, so update the assertion accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When onPtyReady fired, the parent wrote ptyId back into spawn, changing
JSON.stringify(spawn) and re-triggering the WS effect. The cleanup closed
the WS which destroyed the PTY under Phase-5 rules (fresh-spawn PTYs are
torn down on WS close), making the subsequent reattach fail → loop.

Fix: use spawnNonce (spawn without ptyId) as the effect key. ptyId changes
never re-run the effect. For the "ptyId not found → need fresh spawn" case,
increment freshSpawnCount state instead of relying on the parent removing
ptyId from spawn. All spawn.xxx refs inside the effect now read from
spawnRef.current so they see the latest value without stale closure issues.

Fixes: split-preserves-pty remount, shell-wrap-runtime no-input, and
legacy-layout-migration re-persist failures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The source-code contract test checked for spawn?._autoResume — after
the spawnRef refactor it's now spawnRef.current?._autoResume.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
reuseExistingServer: true locally caused tests to hijack the user's
running app on port 8769, injecting fake sessions into it. Setting it
to false ensures tests always get a clean isolated backend.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@MenachemBarak MenachemBarak merged commit 946d91c into main Apr 29, 2026
15 checks passed
@MenachemBarak MenachemBarak deleted the feat/daemon-keepalive branch April 29, 2026 16:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants