9.8 KiB
Transcript Rehydration + --conversation Resume for Claude Code — Product Spec
Linear: REMOTE-1373
Summary
Two capabilities for Claude Code cloud runs that together let a Claude session pick up where it left off:
- Transcript rehydration (the foundation) — when a fresh Claude sandbox starts against an existing conversation, restore the prior transcript into
~/.claude/on disk AND make Claude actually use it. The latter requires a Claude-specific server-side system-prompt body plus a user-turn preamble that overrides Claude's baked-in prompt, which previously caused resumed sessions to ignore saved state and effectively start over (including dropping any uncommitted workspace patches from cloud-to-cloud handoff). --conversation <id>for Claude Code (the CLI surface) — lets a user deliberately trigger transcript rehydration against a finished Claude conversation, matching what--conversationdoes today for Oz. Works in bothrun-cloudand localrun. Transcript rehydration is the more important of the two: it's what makes cloud-to-cloud handoff actually survive on Claude, and--conversationis just the user-facing way to invoke it on demand.
Problem
Cloud Claude runs upload their full state to GCS (claude_code.json transcript, block_snapshot.json, handoff workspace patches), but nothing on the read side picks it back up:
- The stored transcript is never rewritten into Claude's on-disk layout, so
claude --resume <uuid>finds nothing and starts fresh. - On cloud-to-cloud handoff, the existing Oz-style rehydration system prompt gets deprioritized relative to Claude's baked-in system prompt on resumed sessions, so Claude acknowledges the instructions and proceeds as if the workspace were reset — uncommitted changes are silently lost.
--conversationis Oz-only, so a finished Claude conversation is effectively read-only.
Goals
- Transcript rehydration works on Claude — whenever a Claude sandbox is spun up against an existing conversation (cloud-to-cloud handoff, or explicit
--conversation), the prior transcript lands in~/.claude/projects/<encoded_cwd>/<uuid>.jsonlwith subagents, todos, and asessions-index.jsonentry, AND any workspace patches from the prior sandbox are applied before Claude answers the new user turn. The "AND" is the hard part: it requires server-side prompt changes that survive Claude's own system prompt. --conversation <id>for Claude Code —warp agent run-cloud --harness claude --conversation <id> --prompt "..."spawns a new cloud run that resumes the prior Claude session;warp agent run --harness claude --conversation <id> --prompt "..."does the same locally.- Saves continue in place — follow-up periodic/final saves write to the same server conversation id, same GCS objects, same Warp Drive object, same artifacts list.
Non-goals
- Transcript rehydration or resume support for third-party harnesses other than Claude Code. The abstraction is harness-agnostic (new CLIs add a
ResumePayloadvariant + their own fetch override), but no other harness implements it today. - Preserving the envelope's original cwd — we rewrite it to the new run's cwd so
claude --resumefinds the jsonl. - Forking into a new conversation id, or a
--fork-sessionbranch. - UI changes to the AI Conversation viewer.
User experience
Transcript rehydration (automatic)
Any Claude cloud run that resumes an existing conversation — whether via cloud-to-cloud handoff (REMOTE-1290) or via explicit --conversation — goes through the same rehydration path:
- The sandbox CLI downloads the transcript envelope from
GET /harness-support/transcript. The endpoint runs under the harness-support workload-token middleware and resolves the conversation server-side from the current task'sagent_conversation_id, so the caller doesn't pass a conversation id. - Envelope's
cwdis rewritten to the current working directory; main jsonl, subagent jsonls, per-agent todos, and asessions-index.jsonentry are written under~/.claude/projects/<encoded_cwd>/.... - Claude is launched with
--resume <session_id>so it picks up the on-disk transcript. - On every turn,
/harness-support/resolve-promptappends a third-party-CLI-strength rehydration body to Claude's system prompt and returns a one-line user-turn preamble in the newresumption_promptresponse field. The Claude harness prepends that preamble to the user-turn prompt it pipes into the CLI, so Claude executes the pre-turn checklist (apply workspace patches, verify dirty files) before answering. Other harnesses can ignoreresumption_prompt; old clients that don't know the field is there fall through to the unmodified prompt. - Periodic and final saves overwrite
<id>/claude_code.jsonand<id>/block_snapshot.jsonin GCS so the AI Conversation viewer shows the merged state and artifacts stay attached.
--conversation <id> invocation
warp agent run-cloud --harness claude --conversation <id> --prompt "follow-up"
warp agent run --harness claude --conversation <id> --prompt "follow-up"
--harness claude is required when resuming a Claude conversation; the default --harness oz against a Claude id fails fast with an actionable error. The CLI never silently flips harness mid-flight because harness drives pre-load decisions (task config, CLI validation, server task creation).
The client validates --harness against the conversation's stored harness before any task is created, then runs the same rehydration path described above. Local runs skip step 4 (the server's rehydration prompt is a no-op because there's no prior ended execution) but still rehydrate the transcript to disk so Claude's /resume picker sees it.
Error and edge cases
- Non-existent or inaccessible conversation id: fail fast, no task created.
- Harness mismatch (either direction): fail fast with a message naming both sides, e.g.
conversation X was produced by the claude harness, but --harness oz was requested. No task created. - Claude conversation with no stored transcript:
conversation <id> has no stored transcript for the claude harness. The prior run may have crashed before saving any state. - Transient transcript fetch failures: bounded exponential backoff inside
HarnessSupportClient::fetch_transcript; permanent 4xx fails fast. claude --resumefailing at runtime (e.g. upstream session-index desync): surface the error and exit non-zero instead of silently starting a fresh session.--conversationwithout a prompt/skill: same rejection as today'shas_prompt_sourcecheck.- In-progress prior run: no special handling — same as Oz
--conversation.
Feature-flag gating
FeatureFlag::CloudConversationsoff →--conversationhidden and rejected (unchanged).FeatureFlag::AgentHarnessoff →--harness clauderejected; the Claude transcript-rehydration client path is also gated on it.CloudToCloudHandoffEnabledoff → server returns the resolved prompt without the rehydration body or user-turn preamble (unchanged gate).
Success criteria
- Transcript rehydration: a Claude cloud run whose sandbox is replaced mid-run resumes with its prior transcript visible to Claude AND its uncommitted workspace patches applied before Claude answers the next turn — verified by the next turn's tool calls / git status, not just by the agent's acknowledgement text.
- Conversation resuming:
run-cloud --conversation <id> --prompt "..."appends to the same AI Conversation in the UI and overwrites<id>/claude_code.json/block_snapshot.jsonin place;agent run --conversation <id>locally grows~/.claude/projects/<encoded_cwd>/<uuid>.jsonlwith the prior entries before the new prompt runs. - New PR / plan / file artifacts from the resumed run attach to the same Warp Drive conversation object as the original.
- Invalid inputs fail cleanly pre-launch with no side effects.
Validation
- Transcript rehydration (cloud handoff): force a sandbox replacement on a Claude run with uncommitted changes; after handoff, confirm from the next-turn tool calls that
git applyran on the expected patches and that the files the patches touched are dirty. - Conversation resuming (cloud):
run-cloud --harness claude --conversation <id>with a follow-up; UI merges the conversation, prior uncommitted files reappear, saves overwrite in place. - Conversation resuming (local):
agent run --harness claude --conversation <id>locally;~/.claude/projects/<encoded_cwd>/<uuid>.jsonlgrows and Claude's/resumepicker lists the session. - Errors: harness-mismatch id, missing-transcript id, non-existent id all fail cleanly pre-launch.
- Unit tests:
--resumevs--session-idflag selection, envelope cwd rewrite, session-index upsert,HarnessSupportClient::fetch_transcriptretry behavior, harness-mismatch + resume-state-missing error paths. - Integration tests:
GET /harness-support/transcript(Claude 307, Oz 400, no-conversation 400, unauthed 403); worker appends--conversationto CLI args when set.
Resolved decisions
- Transcript rehydration runs on any resumed Claude sandbox, not just
--conversation; the same code path serves cloud-to-cloud handoff. - Third-party-CLI rehydration prompt is stronger than Oz's and is also echoed as a user-turn preamble — on resumed Claude sessions the system prompt alone gets treated as background instead of pre-turn action.
- Same conversation id, in place (matches Oz
--conversation). - Explicit
--harness clauderequired; no auto-detect from metadata. - Reuse the envelope's Claude
session_idso the transcript grows linearly instead of fragmenting across saves. - Rewrite
envelope.cwdsilently (cloud sandboxes routinely change cwd). - Rehydrate
~/.claude/sessions-index.jsonalongside the jsonl so--resume <uuid>lookups succeed on recent Claude versions (upstream bugs claude-code#33912, #39667, #5768).