Skip to content

[Bug]: Generic stuck-session recovery loses phase-safe abort semantics #103697

Description

@mushuiyu886

This was generated by AI during triage.

Bug type

Incorrect recovery behavior / session-state cleanup

Summary

Generic diagnostic stuck-session recovery loses the fact that an abort was initiated by the internal watchdog before the active embedded attempt decides retry/failover semantics.

On current main, recoverStuckDiagnosticSession() calls abortAndDrainEmbeddedAgentRun() with reason: "stuck_recovery", but the active-run/reply-run abort path does not preserve that reason to the attempt. The attempt therefore observes a generic external abort. A broad workaround that maps every stuck recovery to an idle timeout is also unsafe: if a potentially side-effecting tool is active, automatic retry/fallback can replay external effects.

The recovery path needs to classify the active phase before failover sees the abort, and its reply-run cancellation must preserve the existing abortability and terminal-settle cleanup invariants.

Why this is independent from #93610 / #93965

#93965 fixed the original opencode-go incident at the provider-owned raw SSE boundary by terminating stalled opencode-go streams and routing them through failover. That provider-specific fix correctly closed #93610.

This issue is downstream and provider-agnostic. It applies whenever the generic diagnostic watchdog reclaims an active embedded run, regardless of which provider or transport caused the run to stop making progress. It does not require changes to opencode-go, OpenAI-compatible stream parsing, provider payloads, or model routing.

Canonical reachability

  1. A scheduled, interactive, or provider-backed embedded-agent session remains active without diagnostic progress.
  2. The diagnostic stuck-session watchdog resolves the canonical active embedded or reply-owned run by sessionId / sessionKey.
  3. recoverStuckDiagnosticSession() invokes abortAndDrainEmbeddedAgentRun() with reason: "stuck_recovery".
  4. The embedded-run and reply-run registries abort the active attempt.
  5. The attempt must classify the currently active phase before setting idleTimedOut, timedOutDuringToolExecution, or externalAbort.
  6. The existing failover policy uses those flags to decide whether rotate/fallback is allowed.

Current main drops the distinct stuck-recovery abort reason between steps 3 and 5, so the attempt cannot make a phase-safe decision.

Expected behavior

  • An active model-call stall with no active potentially side-effecting tool is classified as a model idle timeout and may use the existing retry/fallback policy.
  • An active potentially side-effecting tool stall is classified as a tool-execution timeout and must not enter automatic retry/fallback.
  • Pre-model, post-model, stale, or otherwise unknown stuck recovery remains external-abort-like and must not enter an automatic replay path.
  • Finalizing or otherwise non-abortable reply operations reject stuck-recovery cancellation just as other protected abort paths do.
  • A running reply operation aborted for stuck recovery schedules bounded terminal settlement, so registry/lane state is released even if backend owner cleanup never arrives.

Actual behavior

  • The diagnostic layer records reason: "stuck_recovery", but the active attempt receives a generic abort and sets externalAbort=true.
  • Model-call stalls can surface Request was aborted instead of using configured fallback.
  • Treating all recovered stalls as model idle timeouts would make side-effecting tool replay possible.
  • A distinct stuck-recovery reply abort that does not share the existing terminal-settle contract can leave the reply operation registered indefinitely if owner cleanup does not complete.

Deterministic reproduction

A source/runtime reproduction does not require a live provider outage:

  1. Register a running embedded/reply operation with an abort callback that records its cancel reason.
  2. Invoke the canonical abort/drain path with reason: "stuck_recovery".
  3. Observe on current main that the active attempt does not receive a distinct stuck-recovery reason and follows generic external-abort semantics.
  4. For cleanup, create a running reply operation, abort it through the stuck-recovery path, omit owner cleanup, and advance the terminal-settle timeout; the operation must no longer remain active.

Acceptance criteria

  • Preserve a typed stuck_recovery reason through diagnostic recovery, embedded-run registry, and reply-run fallback paths.
  • Classify recovery from active model-call state and active potentially side-effecting tool state before failover observes abort semantics.
  • Give active potentially side-effecting tools precedence over model-call retry eligibility.
  • Keep unknown/stale phases out of retry/fallback.
  • Reuse the existing reply-operation abortability guard.
  • Schedule terminal settlement for non-queued running stuck-recovery aborts, with a focused fake-timer regression.
  • Add regression coverage for model-call stalls, side-effecting tool stalls, unknown/stale recovery, reply-run reason propagation, abortability, and terminal cleanup.

Scope boundary

Out of scope:

  • opencode-go or other provider stream parsing
  • provider-specific timeout wrappers
  • cron timeout-budget policy
  • fallback model selection policy
  • public configuration, schema, protocol, or persisted-data changes

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High-priority user-facing bug, regression, or broken workflow.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.maturity:stableIssue affects a taxonomy feature currently scored M4/M5.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions