Skip to content

relay#527: adopt an idle outside Claude session - #547

Merged
barelyworkingcode merged 25 commits into
epic/525-session-adoptionfrom
feat/527-claude-adopter
Oct 21, 2026
Merged

barelyworkingcode merged 25 commits into
epic/525-session-adoptionfrom
feat/527-claude-adopter

Conversation

@barelyworkingcode

Copy link
Copy Markdown
Owner

Closes #527

Adopt an idle Claude session started in a terminal on this Mac: POST /api/sessions/adopt (execute class, socket only, out of the chief-of-staff scope) ends the process and resumes the same conversation as a relay headless session in the same folder, under the project's sandbox and policy. Outside rows carry claudeSessionId, adoptable and adoptReason; no pid or start time leaves relay.

Contract: the issue's ## Contract plus amendments 1 to 4 (issue comments).

Non-obvious decisions

  • The idle check confirms only on the registry status, and vetoes on an open tool call, a stale status, or a child started after the first assistant record written after the process start. A child whose kernel executable path is /usr/bin/caffeinate never vetoes: Claude Code starts it at each turn and it lives 5 minutes. Found by the journey against a real terminal Claude, which the door refused as busy before the fix.
  • When the 256 KiB tail does not start at the file's start, the boundary comes from a streaming scan from the start; a failed scan vetoes.
  • The ladder (SIGINT, SIGTERM, SIGKILL) waits on the injected clock and on the process exit; the start time is re-read before each signal. SIGINT on Claude is undocumented; the ladder does not depend on its answer.
  • The journey starts the outside claude on its own tmux socket and without CLAUDE_CODE_ENTRYPOINT/CLAUDECODE: run from inside an agent session, claude registers as sdk-cli, which the door correctly refuses.
  • The resumed session runs on today's local path; relay#536 moves it under the runner with no adopt change.

Minor findings, not fixed (review rounds 2 and 3)

  • One transcript line over 8 MiB before the boundary vetoes idle adopts of that session with child_process, not a scan-specific reason. Fails closed; interrupt gets past it.
  • The boundary scan does not check ctx while it reads a long transcript.
  • The busy tests assert 409 busy, not which veto caused it.
  • Theoretical entries are in docs/THEORETICAL.md.

Effort

Fix attempts: review round 1 (coverage gate, long-transcript veto), round 2 (test restamp), real-app (caffeinate false-busy, two attempts: ps comm, then kernel path). Journey environment fixes: three. Tokens: about 3.5M across agents.

Found while working

relay#541, relay#542 (ideas), relay#544 (flake), relay#545 (suite passes go test's 10-minute default), relay#546 (e2e leaves a tmux server on the default socket).

Evidence

Suite: (cd e2e && go test -race -count=1 -parallel 32 ./features/) at a8222ab: ok relaye2e/features 196.699s. At a262561, two runs each failed one test of flake relay#544 only (outside-session WebSocket frame read), which also fails on the epic head; its root-cause pass is open.
Journey run: devboxverify --stack (api phase) on stack r527 built from a262561: SUMMARY pass=7 fail=0 blocked=0 notrun=2; session-adopt PASS: idle terminal Claude adopted by conversation id: 201, terminal process gone, the resumed headless session replied in the same transcript, session_adopt intent and ok completion audited.

Journey: session-adopt PASS on stack r527 at a262561 (real terminal Claude on the stack's local model endpoint)
Break-and-restore: dropped the procStart comparison in AdoptReason; TestAdoptStartMismatchNeverAdopted red at g1_outside_adopt_test.go:794 (row adoptable:true); restored byte for byte (cmp against HEAD). Removed the forward scan; TestAdoptChildVetoInLongTranscript red at :710 (201, want 409 busy); restored.
Harness weakening: none (journey added; no journey, assertion, timeout or skip removed or loosened)

🤖 Generated with Claude Code

barelyworkingcode and others added 22 commits October 20, 2026 14:11
… events

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rom comments

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tail is partial

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e tail

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e outside claude

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…mpt shows No, exit

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hild

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n helper never vetoes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
barelyworkingcode and others added 3 commits October 20, 2026 18:10
…feat/527-claude-adopter

# Conflicts:
#	docs/FEATURES.md
#	docs/THREAT-MODEL.md
…wo theoretical entries

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… key per 1.5 s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@barelyworkingcode

Copy link
Copy Markdown
Owner Author

Evidence at head 258c4f4 (epic head merged in at 4230c98; review round 4, owner-approved, covered everything since 08fa14f through f6eeed4; 258c4f4 changes only the journey's trust-prompt key loop).

Suite: (cd e2e && go test -race -count=1 -timeout 30m ./features/) at 4230c98: ok relaye2e/features 642.768s. f6eeed4 and 258c4f4 change docs, one comment and the journey only.
Journey run: stack r527 from 258c4f4, devboxverify --stack --phase api: SUMMARY pass=7 fail=0 blocked=0 notrun=2.
Epic-base harness (origin/epic/525-session-adoption at fad46a6, --checkout this head, same stack): SUMMARY pass=6 fail=0 blocked=0 notrun=2.
Harness break: a scratch commit (never pushed) skipped the process stop in TakeOver; stack r527b from it: session-adopt FAIL the terminal's claude process was still alive after 201. The branch never carried the break.

Review: round 4 (owner-approved) approve, no blocking findings; its two minors fixed in f6eeed4
Journey: session-adopt PASS on stack r527 at 258c4f4 against a real terminal Claude on the stack's local model endpoint
Harness weakening: none (a journey added; no journey, assertion, timeout or skip removed or loosened)
Main harness vs head: epic-base harness at fad46a6 against 258c4f4 on stack r527: pass=6 fail=0 blocked=0 notrun=2
Break-and-restore: product: procStart check dropped, TestAdoptStartMismatchNeverAdopted red at :794, restored byte for byte; forward scan removed, TestAdoptChildVetoInLongTranscript red at :710, restored. Harness: TakeOver stop skipped in a scratch commit, session-adopt FAIL (process still alive after 201); never on the branch

@barelyworkingcode
barelyworkingcode marked this pull request as ready for review October 21, 2026 01:33
@barelyworkingcode

Copy link
Copy Markdown
Owner Author

Weakening reviewed: TestOutsideLiveRowHasNoClaudeSessionID and TestOutsideResumedSessionShowsOnceWithoutClaudeSessionID are replaced by TestOutsideLiveRowCarriesClaudeSessionID and TestOutsideResumedSessionShowsOnceWithClaudeSessionID, because contract decision 2 puts claudeSessionId on live rows; the session-adopt NOTRUN result is the contract's answer for a stack with no local model endpoint.

@barelyworkingcode
barelyworkingcode merged commit 552a068 into epic/525-session-adoption Oct 21, 2026
@barelyworkingcode

Copy link
Copy Markdown
Owner Author

merge-check: merged. approved, target, contract, evidence
Report: 3. Secrets (cmd/relay/api_credential.go); 2. The gates themselves: skip marker added result{adoptID, stateNotRun (cmd/devboxverify/journey_adopt.go) [name-based]; 2. The gates themselves: feature row lost proof G1.46 e2e:TestOutsideLiveRowHasNoClaudeSessionID, e2e:TestOutsideResumedSessionShowsOnceWithoutClaudeSessionID (docs/FEATURES.md); 2. The gates themselves: test removed TestOutsideLiveRowHasNoClaudeSessionID (e2e/features/g1_outside_claude_session_id_test.go) [name-based]; 2. The gates themselves: test removed TestOutsideResumedSessionShowsOnceWithoutClaudeSessionID (e2e/features/g1_outside_claude_session_id_test.go) [name-based]

@barelyworkingcode
barelyworkingcode deleted the feat/527-claude-adopter branch October 21, 2026 01:34
@barelyworkingcode

Copy link
Copy Markdown
Owner Author

Merged by merge-check: approved, target, contract, evidence PASS; ci and verified NA (epic child); owner and weakening reported in the run report.

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.

1 participant