feat(agents): adopt idle external claude sessions via claude --resume (Phase 6)#111
Merged
Conversation
…e` (Phase 6) The headline gap from the control plane: command sessions LISA did NOT spawn — the user's own claude sessions from the app/terminal. A live session can't be tapped (the app owns its stdin/stdout pipe), but every session has a stable id and `claude --resume <id>` continues that exact transcript in a fresh process. So: an IDLE claude session in the roster gets an ⇲ adopt button → LISA spawns `claude --resume <id>` under its own PTY → you drive a continuation of that conversation (send / answer / cancel / view output); new turns append to the same transcript (visible in the app's history too). Safety (this touches the user's real session data): - Liveness guard: liveClaudeSessionIds() reads ~/.claude/sessions/<pid>.json and checks the pid; only sessions with no running owner are marked `resumable`. The start endpoint 409s on a live session — two writers to one JSONL transcript interleave and corrupt it. Smoke-tested: adopting THIS live session → 409; /api/agents/sessions correctly split 9 claude rows into 5 idle / 4 live. - Honest limit (documented): a session open+running in the app right now still can't be commanded from outside — close it, then adopt. (Live control would need the undocumented peerProtocol; not touched.) Implementation: - src/integrations/claude-code/liveness.ts — liveClaudeSessionIds() + pidAlive(). - pty.ts — PtyStartOpts.resumeSessionId → `--resume <id>` (claude only); detectClaudeBinary() prefers LISA_PTY_CLAUDE_CMD → newest app-bundled claude (version-matched) → PATH claude. - AgentSession.resumable; /api/agents/sessions enriches idle claude-code rows. - /api/agents/pty/start accepts resumeSessionId with the 409-on-live guard; task optional when resuming. - GUI: ⇲ adopt button on resumable rows (errors surface in the modal). - docs/PTY_AGENTS.md. Tests: 719 pass / 1 skip. typecheck + build clean. Snapshot updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
What
The headline gap from the control plane: command sessions LISA did not spawn — your own
claudesessions from the app/terminal.You can't tap a live session (the Claude app owns its stdin/stdout pipe — confirmed by inspecting the running processes: every session is driven over
--input-format stream-json). But every session has a stablesessionId, andclaude --resume <id>continues that exact transcript in a fresh process. So:claude --resume <id>under its own PTY → you drive a continuation (send / answer / cancel / ▤ output). New turns append to the same transcript (visible in the app's history too).Safety — this touches real session data
liveClaudeSessionIds()reads~/.claude/sessions/<pid>.jsonand checks the pid; only sessions with no running owner are markedresumable. The start endpoint 409s on a live session — two writers to one JSONL transcript interleave and corrupt it./api/agents/sessionscorrectly split 9 claude rows into 5 idle / 4 live; missing-task → 400.peerProtocol; not touched.)Implementation
src/integrations/claude-code/liveness.ts—liveClaudeSessionIds()+pidAlive().pty.ts—PtyStartOpts.resumeSessionId→--resume <id>(claude only);detectClaudeBinary()prefersLISA_PTY_CLAUDE_CMD→ newest app-bundled claude (version-matched to your sessions) → PATHclaude.AgentSession.resumable;/api/agents/sessionsenriches idle claude rows;/api/agents/pty/startacceptsresumeSessionIdwith the 409 guard (task optional when resuming).docs/PTY_AGENTS.md.Verification
719 tests pass, 1 skip (real-node-pty round-trip);
typecheck+buildclean; live endpoints smoke-tested as above.Still flagged behind
LISA_PTY_AGENTS=1(it spawns real claude under a PTY).🤖 Generated with Claude Code