fix(agent): keep claude_code resume across failed/cancelled runs - #36
Open
CatJuly wants to merge 1 commit into
Open
fix(agent): keep claude_code resume across failed/cancelled runs#36CatJuly wants to merge 1 commit into
CatJuly wants to merge 1 commit into
Conversation
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).
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.
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):
claude_codeas the execution agent; chat a few turns (resume works, context accumulates).claudeprocess gets no--resumeflag, 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 persistedexternal_sessionsrow. Resume eligibility is gated byNON_RESUMABLE_EXTERNAL_SESSION_STATUSES(failed,cancelled,hard_timeout, …) inopc/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: newDURABLE_TRANSCRIPT_AGENT_TYPES = {"claude_code"}+agent_resume_survives_run_failure().external_session_status_allows_resume()accepts an optionalagent_type;external_session_allows_resume()reads the row's ownagent_type, soselect_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'sagent_type.Codex / opencode / cursor keep the existing conservative behavior (no durability claim made for them); unfinalized
provider_streamtokens also keep the strict rejection rule — only terminal statuses are affected.Testing
tests/test_external_resume_survives_failed_run.py(12 tests): identity-helper policy matrix, broker restore re-seeding--resumefrom failed/cancelled claude_code rows, persist keeping the pin, and codex mirrors asserting the conservative path is unchanged.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.origin/mainbaseline 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.