Adopted sessions: free-form handover & resume (Journey 4 / Tier 4) - #23
Merged
Conversation
…4, Task 1) Adopted sessions that end a turn on a free-form question can now be taken over remotely: telecode offers a handover and, on the user's answer, forks-and-resumes the conversation as a telecode-owned continuation linked to the adopted parent. - protocol: agent.handover / handover.answer / session.chained wire types + zod schemas + a `handover` history-entry kind - db: 0006 migration adds sessions.parent_session_id (self-FK, set null) + index - relay: daemon-initiated session.chained registration (origin=launched + parent link), agent.handover awaiting-input routing + cache, parent_session_id on /me/sessions and SessionSummary - daemon: Stop hook -> non-blocking agent.handover offer (free-form heuristic, re-entrancy guard); handover.answer -> fork-resume-and-chain (resume the adopted session_id with forkSession, mark parent handed-off); forkSession threaded through the adapter to query() - web: reducer `handover` kind + states, HandoverCard, answerHandover store/client Integration tests span hook -> daemon -> relay -> fork-resume and the relay's chained registration. Gates green: typecheck 5/5, lint, format, daemon 167, relay 99 (+2 skip), web 233, protocol 98.
… 4, Task 2) When telecode takes over a free-form handover it resumes the adopted conversation with forkSession (full context, new id, own transcript). If that resume fails — the SDK can't pick up an externally-created conversation — the continuation now falls back to a fresh launch seeded with the handover context so the user's answer is never dropped. - daemon: runTurn gains resumeFallbackPrompt; a failed resume (non-abort) re-runs as a fresh, unforked launch seeded via buildHandoverFallbackPrompt(summary, question, answer) - adopt/handover-fallback-prompt.ts: the seeded prompt (pure, unit-tested) - claude-agent-adapter.test.ts: mocks query() to lock that forkSession reaches the SDK only when resuming (an ordinary turn never forks) - daemon.handover.test.ts: proves the fallback — a resume-throwing adapter still completes the child via a fresh seeded launch (adapter called twice) Gates green: daemon 174, typecheck 5/5, lint, format.
…k (Journey 4, Task 3) The Stop hook fires on every turn end, so the handover detector now uses a real heuristic to decide when a turn is asking the user something, and honors the current adoption policy before offering (a handover launches a new session). - adopt/free-form-question.ts: strips fenced code blocks, matches a trailing "?" (through closing quotes/emphasis/brackets) or a short list of high-precision solicitation cues, and bounds length; dismissible, so it favours precision - daemon: handleStopHook re-checks isAdoptionAllowed(cwd) — a repo denylisted (or adoption disabled) after adoption gets no offer, even though it was tracked - tests: 8 heuristic unit tests; 3 daemon integration tests (non-question, stop_hook_active re-entrancy, denylist mid-session all produce no offer) Gates green: daemon 185, typecheck 5/5, lint, format.
…urney 4, Task 4) The "continue here" card now shows a concise summary of what the adopted session was doing, extracted deterministically from its mirrored transcript — no extra model call (fast, private, zero cost). - adopt/handover-summary.ts: buildHandoverSummary keeps recent user/assistant text turns (skips tool calls and gates), collapses whitespace, bounds per-entry and total length, and yields '' when there is nothing to summarize - daemon: handleStopHook is now async — it re-mirrors the transcript before summarizing (a Stop fires after a turn no PreToolUse mirrored) and establishes the session cipher first so the mirror and the offer encrypt consistently - tests: 5 summary unit tests; a daemon integration test writes a JSONL transcript and asserts the offer carries the extracted context Gates green: daemon 191, typecheck 5/5, lint, format.
…rney 4, Task 5) The free-form "continue here" card now degrades honestly when the device is offline: instead of an actionable form that would fail, it shows an "answer at your device" note and returns to actionable when the daemon reconnects. - HandoverCard: new `offline` prop → pending offer renders the answer-at-device fallback; verified all states (pending / offline / submitting / submitted / closed), tokens-only, focus-visible, aria, reduced-motion, 16px textarea - Transcript + session page thread `offline` from session.status==='offline_paused' Gates green: svelte-check 0, web 233, lint, format.
… 4, Task 6) The forked continuation is now navigably linked to the adopted session it continues, and the offer's payload is proven ciphertext to the relay. - daemon test: agent.handover reaches the relay as ciphertext (non-empty nonce, string payload) on a keypair daemon — the question + summary never go cleartext (invariant #5) - web: reducer gains handover.childSessionId + SessionState.parentSessionId + linkHandoverChild; the store handles session.chained channel-wide to link parent and child across sessions; relay-api surfaces parent_session_id from /me/sessions - HandoverCard: "View the continuation →" link once taken over; the session detail page shows "← Continued from an adopted session" for a continuation - tests: reducer link + store cross-session link Gates green: daemon 192, web 236, svelte-check 0, typecheck 5/5, lint, format.
…4, Task 7) Adoption now registers a Stop hook so the daemon can detect a free-form question at turn end and offer to take it over. - hooks-install: Stop added to the telecode hook set (now PreToolUse, SessionStart, SessionEnd, Notification, Stop); still idempotent and reversible, and a user's own Stop hook is preserved alongside telecode's - doctor lists the installed events dynamically, so `telecode doctor` now reports Stop as part of the adoption check - tests: install asserts the five-event set + user-Stop preservation; doctor asserts Stop appears in the adoption detail Gates green: daemon 192, typecheck 5/5, lint, format.
- daemon: a handover is not offered while a permission gate is already pending (never stacks an offer on an active gate) - web: session.history backfills all three decision kinds (permission + question + handover) coexisting in one adopted transcript, each replaying correctly Full suite green: protocol 98, daemon 193, web 237, relay 99 (+2 skip); Playwright e2e 11/11; typecheck 5/5, lint, format.
Multi-agent review passed with no blocking issues; applying the important + cheap findings. - daemon: extract handleHandoverAnswer from the handleFrame case and guard the fire-and-forget launch with .catch() (no silently-swallowed errors); collapse runTurn's 6 positional params into a TurnOptions object; wrap the Stop-hook transcript mirror in try/catch (fail-closed with a clear log); clear pendingHandovers/pendingChainRegistrations on stop(); fix an orphaned JSDoc from the handler reorder; name the title-preview magic number - protocol: tighten sessionChainedPayloadSchema.parentSessionId to .uuid() - web: name the session-id display-length constant - schema: document the parent_session_id reverse DDL (the drizzle-generated migration is left byte-stable so the migrator content-hash stays valid) - tests: it.each for the message-type recognition; guard+typed assertion for the parent<->child link; non-null after vi.waitFor; uuid ids Gates green: protocol 101, daemon 193, web 237, relay 99 (+2 skip), svelte-check 0, typecheck 5/5, lint, format; Playwright e2e 11/11.
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
When an adopted Claude Code session (one you started yourself in the terminal / IDE, that telecode monitors via hooks) ends a turn asking a free-form question — prose, no tool call, so there's no
PreToolUsegate to answer through — telecode now offers to take it over: a "continue here" card surfaces the question + a summary; on your answer it launches a forked, telecode-owned continuation that resumes the same conversation with your answer as the next turn. The adopted session is marked handed-off and linked to its continuation.This closes the last gap in adopted-session steering: previously a free-form question was a dead "answer at your device" wall.
How it works
Stophook (added to the installed set) fires at turn end; a dismissible heuristic onlast_assistant_messagedecides if it looks like a question. Non-blocking (the idle external process is never held); honors the current adoption policy.agent.handover(daemon → web, ciphertext to the relay) surfaces the actionable card.handover.answer→ the daemon launches a continuation viaresume+forkSession(spike-proven: full context, a new session id + its own transcript, so it never contends with the still-live external process), registered with the relay viasession.chainedand linked to the parent by a newparent_session_idcolumn.The approval gate (
canUseTool) stays fully in force on the continuation; the relay only ever forwards ciphertext + routing metadata.Scope
Spike-gated (SDK resume-of-an-externally-created-conversation +
forkSession+ theStopdetector all empirically confirmed before building). Tasks T1–T8: walking skeleton → fork-resume + fallbacks → detector → summary → card (enterprise-ui, all states incl. offline) → parent↔child link + E2E-ciphertext test → installer Stop hook + doctor → variant coverage + Phase 5 hardening. Folds in the Phase 5 variant matrix.Tests & gates
0006_session_parent_id(nullable self-FK,ON DELETE SET NULL; RLS row-scoped policies cover it)Review
Multi-agent codereview (typescript · test · coding-style · supabase-security) — all passed, 0 blocking — plus the enterprise-ui self-review on the Svelte. Invariants #4 (gate), #5 (ciphertext), non-blocking Stop, and fail-soft resume all confirmed. Findings addressed in the final commit. One pre-existing flaky relay test (
variants.test.ts, unrelated to this branch) noted.