fix(agent): keep claude_code resume across failed/cancelled runs - #36
fix(agent): keep claude_code resume across failed/cancelled runs#36CatJuly 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).
LZH-YS1998
left a comment
There was a problem hiding this comment.
Thanks for tracing the context-loss problem and for the focused tests. The root cause is valid, but the current capability policy is too broad to merge safely.
external_session_status_allows_resume() returns true for every status whenever the agent is claude_code, and _persist_session() then preserves the matching task and role token after every failed run. A durable transcript does not prove that a resume token is still usable. If the failure was caused by a missing or corrupted session, a changed workspace/CWD, an invalid --resume target, or a provider-side context/resume rejection, every subsequent Continue can remain pinned to the same bad token and fail again. That can prevent the work item from making progress.
Please revise this PR so that:
- Durable transcript capability is separated from a known-usable resume token; do not make all terminal statuses unconditionally resumable.
- An explicit provider resume failure clears the matching task/role pin and performs at most one fresh-session or context-replay fallback.
- The intended behavior remains covered for cancellation/timeouts after a real provider session has been established.
- Tests cover a missing/invalid resume token and repeated Continue attempts, proving that the same poisoned token is not retried indefinitely.
I integrated this head with the current main in an isolated checkout and the targeted suites passed (99 tests plus 6 subtests), but the new tests currently encode the unsafe all-status policy, so passing them does not close this failure mode.
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.