Resume Claude Code sessions after retryable interruptions - #123
Conversation
|
Last one for today I promise! Although there is another one I'm working on since I noticed evaluation object counts were being leaked to the agent unadvertedly, but I will finish that PR once everything else is merged to avoid things getting out of hand. The other two PRs were small enough that I could work on this one as well |
There was a problem hiding this comment.
Pull request overview
Adds resumable Claude Code runs across subscription usage-cap resets by persisting CLI session state per sandbox generation, resuming with --continue, and carrying forward remaining budget/turn limits while isolating Claude credentials/config from the operator’s live ~/.claude.
Changes:
- Persist Claude session state per sandbox generation and resume interrupted runs with
--continue(local + Docker + Apptainer). - Carry over remaining experimental
max_budget_usd/max_turnsacross rate-limit retries without accidentally converting exhausted limits into “unlimited”. - Add/extend unit + integration tests covering resume behavior, budget carry, and host-state exclusion.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/utils/test_sandbox.py | Adds tests for sandbox-local Claude config redirection and credential cleanup. |
| tests/utils/test_resume_integration.py | New end-to-end test using a fake Claude CLI to validate --continue + session persistence. |
| tests/utils/test_rate_limit.py | Extends retry-loop tests to cover carried budget/turns and unknown-cost handling. |
| tests/utils/test_docker_sandbox.py | Adds tests for extra volume mounts, credentials-only auth mounts, and Docker resume across --rm. |
| tests/utils/test_backends.py | Validates Claude CLI command includes session persistence and adds --continue only when resuming. |
| tests/utils/test_apptainer_sandbox.py | Updates auth tests to use contextmanager + validates credentials-only bind fallback. |
| src/robocode/utils/sandbox.py | Redirects local Claude runs to sandbox-local CLAUDE_CONFIG_DIR via a contextmanager. |
| src/robocode/utils/sandbox_types.py | Adds resume_previous_session to SandboxConfig. |
| src/robocode/utils/rate_limit.py | Refactors dispatch, carries remaining budget/turns, and resumes sessions across rate-limit sleeps. |
| src/robocode/utils/docker_sandbox.py | Adds session-store persistence via an extra bind mount and switches to credentials-only throwaway config fallback. |
| src/robocode/utils/claude_auth.py | New module implementing credentials-only throwaway config + sandbox-local config with credential cleanup. |
| src/robocode/utils/backends/claude.py | Enables session persistence by default and appends --continue when resuming. |
| src/robocode/utils/apptainer_sandbox.py | Mirrors Docker auth isolation + adds session-store bind mount persistence for resume. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
When a generation hits the subscription usage cap, the retry loop now continues the same CLI conversation with the budget the run had left, instead of restarting cold and re-deriving from scratch. A run that straddles a usage-window reset ends up equivalent to one that never hit the cap, rather than being penalized (lost context) or advantaged (a fresh full budget on every retry). Sessions are persisted so they can be resumed (the --no-session-persistence flag is dropped) and, on retry, the CLI is launched with --continue, which reattaches to the most recent conversation in the sandbox working directory (unique per generation). To survive the container --rm, the Claude session store is bind-mounted from a host directory under the sandbox dir into /home/node/.claude/projects for the docker and apptainer backends; the local backend uses the host store directly. The retry loop carries the remaining dollar and turn budget into each resumed attempt so total spend matches an uninterrupted run. Verified end-to-end that claude -p --continue resumes prior context in headless stream-json mode and that sessions are stored under the working-directory-hashed projects path that the mount targets.
The local backend runs the CLI directly on the host, so persisting sessions for resume would otherwise write conversation transcripts, history, and caches into the host ~/.claude, leaving state that outlives the run and can only be cleaned by reaching outside the sandbox. The local runner now points CLAUDE_CONFIG_DIR at a sandbox-local .agent_home and symlinks the host credentials file in, so the CLI authenticates while writing all of its state under the sandbox dir; removing the sandbox removes it. Token auth via CLAUDE_CODE_OAUTH_TOKEN needs no file and passes through the environment. The docker and apptainer backends already confine the store to the sandbox (bind-mounted .agent_sessions) with an ephemeral container home, so this only changes the local path. Verified end-to-end that --continue resumes under the relocated config dir and that no session is written to the host ~/.claude.
A real usage-cap interruption cannot be triggered on demand, so the test swaps the claude binary (via ROBOCODE_CLAUDE_CMD) for a stand-in that reports a rate limit on its first call and completes on the second. The full real path runs: run_with_rate_limit_retry -> run_agent_in_sandbox (subprocess, CLAUDE_CONFIG_DIR redirect, session dir) -> parse_stream -> rate-limit detection -> resume with --continue and carried budget. The stand-in completes only when it is resumed (--continue present) and can read back the session marker it persisted on the first attempt, so a passing run proves both that the flag reaches the CLI and that the session store survives the retry. Confirmed non-vacuous by mutation: dropping --continue from the command makes the test fail.
…ner --rm Complements the local end-to-end test with the docker-specific risk it cannot cover: that the .agent_sessions bind-mount actually carries the CLI session store across a fresh container. A fake claude (via ROBOCODE_CLAUDE_CMD) reports a rate limit in the first container, writing a marker into the mounted session store; the retry launches a second container that completes only if it receives --continue and finds that marker, proving the store survived the --rm. Gated on the sandbox image being present, like the other container tests, so it skips where docker is unavailable. Verified non-vacuous by mutation: dropping the session mount makes the marker vanish across containers and the test fails.
0d0ba11 to
5ceb7a2
Compare
|
validated in a live session so I feel good enough merging this |
Base status
#121 (
runtime-metrics) was squash-merged intomainas48aa4ed. This branch has been rebased onto that updatedmain, and the PR targetsmain. The former stacked-PR dependency is satisfied.Problems
Subscription usage caps
When Claude Code hits the subscription usage cap, the experiment runner sleeps until the reset and launches a new CLI/container process. Previously that process:
--max-budget-usdand--max-turns, making a timing-dependent retry a different experimental condition; andThe sandbox files survived, but the agent had to reconstruct its reasoning from disk.
Output-token overflows
Claude can also abort a run with
Claude's response exceeded the 32768 output token maximum. RaisingCLAUDE_CODE_MAX_OUTPUT_TOKENSis not a reliable fix: the model can consume the larger allowance and still exceed it. Previously this error could leave a partialapproach.pyand fail the entire experiment.Context exhaustion and current reset messages
Auto-compaction normally protects Claude's context window, but runs have also stopped with
Prompt is too long. Separately, the current subscription message includes minutes (You've hit your session limit · resets 1:30pm (UTC)), which the old hour-only regex did not match or schedule correctly.What this PR changes
Resume Claude Code context
--continue. Every generation has its own sandbox working directory and isolated session store, so “most recent session in this directory” identifies that generation./home/node/.claude/projectsunder the generation's sandbox directory, across ephemeral container recreation..agent_home.Preserve the experimental budget
0, because both CLI limits interpret zero as unlimited.Resuming is intentionally not claimed to be identical to an uninterrupted run: rebuilding context can consume uncached transcript input tokens. The retry metrics let analyses identify or exclude these runs.
Recover output-token overflows
gen_output_token_limit_hitandgen_output_token_retriesmake the condition visible inresults.json; aborted tokens and cost use the existing interruption metrics.Compact an oversized prompt before resuming
Prompt is too longis detected in assistant text, result errors, or stderr, and partial output is not evaluated./compactand focused instructions to retain requirements, decisions, failed approaches, and current implementation state while dropping verbose tool output.compact_boundaryevent. A normal sandbox “output file not found” result from the local slash command is not treated as failure when that confirmation exists; conversely, an unconfirmed/failed compaction is never assumed to have worked.gen_prompt_too_long_hitandgen_prompt_too_long_retriesexpose the condition in results.Parse minute-bearing usage resets
resets 1:30pm (UTC)format is recognized.resets 3amretain their prior behavior.Keep experiments out of the operator's Claude state
Session persistence must not add experiment conversations to the operator's normal
~/.claudehistory or session picker.CLAUDE_CONFIG_DIRto the sandbox-local state directory.Only the experiment's isolated transcript state survives between retry attempts.
Scope: OpenCode
Conversation resume is implemented only for Claude Code. OpenCode is currently used with local/open-source models in this project, where Claude subscription caps and these Claude-specific errors do not apply. OpenCode session persistence/resume semantics are deliberately out of scope rather than being guessed at in this PR.
Validation
--continue, and completion.Prompt is too long, requires the isolated/compactcontinuation andcompact_boundary, then requires a separate resumed work process before it completes.--continue, and completion.--continueretry recovers only its own prompt-specific session marker.--rmand proves the first container's session marker reaches the resumed container.git diff --checkpass.Limitations / not tested
--continueuseful after the API abort. The automated test proves our state and invocation plumbing, not the proprietary CLI/API's handling of every real mid-response failure./compactcan itself fail if the context is already too full to generate its summary. In that case this PR records the failure and stops safely; it does not silently clear the session or restart without context. The automated test covers both confirmed and failed compaction, but not a real Claude-generated summary.Worktree note
Two unrelated pre-existing formatter-only edits in
integration_tests/red_team_sandbox.pyandtests/utils/test_env_in_sandbox.pywere intentionally left out of every commit in this PR.