Skip to content

fix: Claude Code loses the caller's system prompt after a resume - #203

Draft
timothee-chauvin wants to merge 1 commit into
meridianlabs-ai:mainfrom
timothee-chauvin:fix/claude-code-resume-keeps-system-prompt
Draft

timothee-chauvin wants to merge 1 commit into
meridianlabs-ai:mainfrom
timothee-chauvin:fix/claude-code-resume-keeps-system-prompt

Conversation

@timothee-chauvin

Copy link
Copy Markdown

🤖 Claude Opus 5.5 says:

Claude Code doesn't keep --append-system-prompt in its session. claude_code() passed the caller's system messages (and system_prompt=) with that flag only on a session's first launch. So every --resume (another attempt after an incorrect answer, an uncaught-error retry, a checkpoint resume, a second turn) ran on Claude Code's default prompt alone.

pin_system_prompt_filter (#142) restores the first launch's system prompt on resumed requests, but only while the request's first user message is unchanged. Claude Code's compaction replaces that message with its summary, so for a session that compacted before it resumed, the caller's prompt is gone for the rest of the sample, and no warning is logged. We hit this in a 48-hour benchmark run on 0.2.70: after an incorrect submission at 16.5h, every later model call ran without the task's instructions and rules, and the agent then did something those rules forbid.

This PR records the caller's system messages in the sample store, keyed by session id, before the session's first launch, and passes them with --append-system-prompt on every launch. It doesn't re-read them from state.messages on later launches: by then the bridge has replaced them with Claude Code's own prompt, and appending that duplicates it (the bug test_claude_code_system_prompt_not_duplicated guards against, whose fix removed appending on resume).

Against the real CLI (Claude Code 2.1.236, local fake Messages API), a --resume with --append-system-prompt sends the appended text exactly once and keeps the first launch's identity line ("You are Claude Code, …"). Without the flag, the text is missing and the identity line flips to "You are a Claude agent, built on Anthropic's Claude Agent SDK." So the identity-line difference the pin filter's docstring listed came from the missing flag; the docstring now says so.

Behavior changes:

  • Resumed launches now carry --append-system-prompt. The pin filter still applies while the first user message matches, and the first launch and resumes now send the same appended text.
  • A sample restored from a checkpoint written before this change has no recorded entry, so it records the bridged state.messages and appends Claude Code's own prompt once more on that resume.
  • Out of scope: centaur mode passes no system-prompt flags at all, before and after this change.

Validation

  • New test test_every_launch_appends_the_callers_system_prompt: it runs the real launch loop with a fake sandbox and bridge, two turns of one agent with attempts=2, and asserts every launch appends exactly the caller's prompt. It fails on main ([] == ['Task prompt']). It also fails if the prompt is rebuilt from state.messages on each turn (Claude Code's prompt plus the task, appended again).
  • pytest (fast suite, Python 3.12): 414 passed, 86 skipped, 2 failed. Both failures also fail on main here: test_bundled_catalog_tracks_live_latest (the bundled Codex catalog is stale) and test_k8s (no Anthropic credentials). test_home_isolation.py and test_trajectory_parsing.py were not collected because I didn't install the optional mini-swe-agent dependencies (the dev extra's quarto-cli build fails on this host).
  • mypy src tests: the same 8 errors as main in this environment, all missing optional packages.
  • Not run: the slow test_multi_call.py tests (need an Anthropic key and Docker).

Agent review

  • Reviewers: GPT-6 Astra via codex exec -s read-only (fresh context, different model family; model checked in its session log), and Claude Opus 5.5 subagent (fresh context), 1 pass each.
  • Findings: no correctness bugs. 4 other findings: 3 fixed (the test didn't cover the duplication regression across turns, now two turns; a comment overstated "passes both"; the docstring's identity-line claim, checked against the CLI and corrected), 1 noted above instead of fixed (pre-change checkpoints). Pre-existing issues noted but left out of scope: centaur mode omits system-prompt flags; session_id is shared by every sample using one agent instance.

Claude Code does not keep --append-system-prompt in its session, and resumed
launches omitted it, so every --resume (another attempt, an uncaught-error
retry, a checkpoint resume) ran on Claude Code's default prompt alone.
pin_system_prompt_filter restores it only while the first user message is
unchanged, which Claude Code's compaction breaks.

Record the caller's system messages before the session's first launch and
append them on every launch.

This branch has not been deployed

No deployments
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