Skip to content

fix(agent): keep claude_code resume across failed/cancelled runs - #36

Open
CatJuly wants to merge 1 commit into
HKUDS:mainfrom
CatJuly:fix/claude-code-resume-after-failed-run
Open

fix(agent): keep claude_code resume across failed/cancelled runs#36
CatJuly wants to merge 1 commit into
HKUDS:mainfrom
CatJuly:fix/claude-code-resume-after-failed-run

Conversation

@CatJuly

@CatJuly CatJuly commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

When a Claude Code run launched by OpenOPC fails, is cancelled by the user, or hits a timeout, the next conversation turn silently starts a brand-new provider session — the agent replies as if the conversation never happened ("I have no context of that").

Reproduction (observed in a real task-mode project):

  1. Start a task-mode conversation with claude_code as the execution agent; chat a few turns (resume works, context accumulates).
  2. Stop the running task (or let a run fail / time out).
  3. Send the next message in the same conversation.
  4. The spawned claude process gets no --resume flag, starts a fresh session with only the new Task Brief, and answers with no memory of the conversation.

In the affected project, 2 UI conversations fragmented into 8 separate Claude sessions (~/.claude/projects/<workspace>/), one of them 249 user messages deep before it was dropped.

Root cause

Cross-invocation continuity relies on --resume <session-id> seeded from the persisted external_sessions row. Resume eligibility is gated by NON_RESUMABLE_EXTERNAL_SESSION_STATUSES (failed, cancelled, hard_timeout, …) in opc/layer3_agent/external_session_identity.py, and the broker additionally clears the role token and the task's resume pin after a failed run.

That gate conflates two independent things: the run's outcome and the provider thread's resumability. Claude Code persists every transcript under ~/.claude/projects/<cwd>/<session-id>.jsonl; claude --resume <id> replays it even when the launching process died mid-run. Marking the token dead throws the whole conversation away for no reason.

Fix

  • external_session_identity.py: new DURABLE_TRANSCRIPT_AGENT_TYPES = {"claude_code"} + agent_resume_survives_run_failure(). external_session_status_allows_resume() accepts an optional agent_type; external_session_allows_resume() reads the row's own agent_type, so select_best_external_resume_session() / provider_token_from_external_session() pick up the policy without signature changes.
  • external_broker.py: _stored_provider_token_allows_resume() treats a terminal-but-failed newest row as resumable for durable agents; _persist_session() keeps the role token and the task resume pin after a failed run of a durable agent.
  • engine.py: the three bare-status call sites (checkpoint-restore gate, checkpoint token seeding, checkpoint token veto) now pass the session's agent_type.

Codex / opencode / cursor keep the existing conservative behavior (no durability claim made for them); unfinalized provider_stream tokens also keep the strict rejection rule — only terminal statuses are affected.

Testing

  • New tests/test_external_resume_survives_failed_run.py (12 tests): identity-helper policy matrix, broker restore re-seeding --resume from failed/cancelled claude_code rows, persist keeping the pin, and codex mirrors asserting the conservative path is unchanged.
  • Existing suites: test_external_session_continuity.py (all 22 pass unchanged — the codex veto/clear semantics are preserved), test_company_runtime_suspend_resume.py, test_external_agent_monitoring.py, test_metadata_ownership.py, test_company_collaboration.py, test_parallel_runtime_isolation.py, test_wake_and_delivery.py.
  • Failure list is identical to the clean origin/main baseline on the same machine (5 pre-existing environment failures, GBK-path related), i.e. no regression.

Scope

Python only — no frontend changes. Behavior changes only for agent_type == "claude_code"; other external agents are untouched.

Every conversation turn spawns a fresh `claude --print` process and
continuity relies on `--resume <session-id>` seeded from the persisted
external_sessions row. When a run ends failed/cancelled/timed-out, that
status is treated as "session dead": the next turn skips resume, starts a
blank provider session, and the agent answers as if the conversation never
happened. One stopped task is enough to silently drop hours of context
(observed: 2 UI conversations fragmenting into 8 separate Claude sessions).

Claude Code persists every transcript under
`~/.claude/projects/<cwd>/<session-id>.jsonl`, and `--resume <id>` replays
it regardless of how the launching process ended - the run outcome and the
provider thread's resumability are independent. Introduce
DURABLE_TRANSCRIPT_AGENT_TYPES in external_session_identity and thread it
through the status gates: select_best_external_resume_session /
provider_token_from_external_session now read the row's own agent_type,
broker restore accepts terminal-but-failed rows for durable agents, and
broker persist no longer clears the role token or the task resume pin
after a failed claude_code run. Codex/opencode/cursor keep the existing
conservative behavior.

Verified with tests/test_external_resume_survives_failed_run.py (12 new
tests) plus tests/test_external_session_continuity.py,
test_company_runtime_suspend_resume.py, test_external_agent_monitoring.py,
test_metadata_ownership.py, test_company_collaboration.py,
test_parallel_runtime_isolation.py, test_wake_and_delivery.py - failure
list identical to the clean origin/main baseline (5 pre-existing
environment failures, no regression).
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