Skip to content

Session appears stuck after read tools, then persists as empty aborted assistant message #27

Closed

Description

@OpenCodeEngineer

Summary

A session can look stuck immediately after a successful pair of read tool calls, but the persisted state shows a different failure mode: the next assistant turn starts, never emits any parts, stays open for about 16.7 minutes, and then is stored as MessageAbortedError with an empty assistant message.

This does not look like a tool-result deadlock. The last completed tool turn is clean. The hang appears to be in the follow-up model stream / session-turn lifecycle after the tool results are already persisted.

Related: #24 covers restart recovery leaving orphaned assistant messages. This report looks different because there is no restart-specific abort message and no orphaned tool parts.

Session investigated

  • Session ID: ses_30acd1e73ffeo2CfsBDMfUg1ey
  • Session title: CDP a11y extractor wiring
  • Model: github-copilot / claude-opus-4.6

What the UI looked like

The session looked stuck right after the agent had reached the "Explored 2" state and performed another pair of read calls on webpage-related code.

Persisted evidence

From the local SQLite store (opencode-local.db):

Last clean assistant tool turn

Assistant message msg_cf98d17840018Ucq4aNqWeJ04I completed normally:

  • finish = "tool-calls"
  • completed at 2026-03-17T02:08:24.254Z
  • two successful read tool parts:
    • toolu_vrtx_018q8BECJMjEm7NBc1wXgeJ7
    • toolu_vrtx_01Jwp4JZs5DeZojP7KXSgrDV
  • both reads targeted lib/agent/PageContentProvider.ts

Follow-up assistant turn that looked stuck

Immediately after that, a new assistant message msg_cf98d2ac0001ltRD6xGVR9b5Sv was created and later finalized as:

  • error.name = "MessageAbortedError"
  • error.data.message = "The operation was aborted."
  • time.created = 2026-03-17T02:08:24.256Z
  • time.completed = 2026-03-17T02:25:05.327Z
  • parts = []
  • tokens = 0
  • cost = 0
  • finish = null

Important negative evidence

  • There are zero parts attached to msg_cf98d2ac0001ltRD6xGVR9b5Sv.
  • There are zero pending or running tool parts left in the session.
  • So the session was not stuck waiting on a tool result in persisted state.

Likely fault path

The cleanest reading is:

  1. The assistant completed a tool-calls turn successfully.
  2. The loop started the next assistant message immediately afterward.
  3. That follow-up stream never reached text-start, tool-input-start, or step-finish.
  4. The request remained in-flight until an abort surfaced.
  5. Persistence then closed the message as a generic aborted assistant turn with no parts.

That makes the UI look like it got stuck right after the second read, even though the actual failure is later in the turn lifecycle.

Relevant code paths

  • packages/opencode/src/session/processor.ts
    • follow-up turn processing and finalization
    • abort cleanup for unfinished tool parts
  • packages/opencode/src/session/message-v2.ts
    • maps AbortError to MessageAbortedError
  • packages/opencode/src/session/llm.ts
    • streaming request lifecycle and provider abort propagation

Expected

If a follow-up assistant stream aborts before emitting any parts, the session should not present as an unexplained post-tool hang. We should either:

  • surface the aborted follow-up turn clearly and promptly in the UI, or
  • detect / timeout this earlier so the stuck state does not sit for ~17 minutes.

Suggested follow-up

  • Add a regression test for: successful tool-calls assistant message -> next assistant message created -> stream aborts before any parts are emitted.
  • Check whether provider chunk timeout / abort propagation is missing or too long for this path.
  • Make the UI explicitly distinguish "tool turn completed, next assistant turn aborted before output" from "tool call still running".

Activity

  1. OpenCodeEngineer commented on Mar 17, 2026

    @OpenCodeEngineer
    Author

    Additional forensic notes from the live investigation.

    The key distinction is that the session does not end with unfinished tool parts.

    Raw state summary

    Session row:

    • session.id = ses_30acd1e73ffeo2CfsBDMfUg1ey
    • session.title = CDP a11y extractor wiring
    • session.time_updated = 1773713304294

    Aborted assistant message row:

    • message.id = msg_cf98d2ac0001ltRD6xGVR9b5Sv
    • message.role = assistant
    • message.parentID = msg_cf98c5c13001isct58xi64GQou
    • message.providerID = github-copilot
    • message.modelID = claude-opus-4.6
    • message.time.created = 1773713304256 (2026-03-17T02:08:24.256Z)
    • message.time.completed = 1773714305327 (2026-03-17T02:25:05.327Z)
    • message.error.name = MessageAbortedError
    • message.error.data.message = The operation was aborted.
    • message.parts count = 0

    Last successful assistant tool turn before the abort:

    • message.id = msg_cf98d17840018Ucq4aNqWeJ04I
    • finish = tool-calls
    • completed at 2026-03-17T02:08:24.254Z
    • contained two completed read tool parts:
      • prt_cf98d24db001nq4jG0TgQyz6RR
      • prt_cf98d2977001Sb67qu823Zp1Lo
    • both targeted lib/agent/PageContentProvider.ts

    Negative checks:

    • select count(*) from part where message_id='msg_cf98d2ac0001ltRD6xGVR9b5Sv'; => 0
    • select count(*) from part where session_id='ses_30acd1e73ffeo2CfsBDMfUg1ey' and json_extract(data,'$.state.status') in ('pending','running'); => 0

    Why this seems different from #24

    Issue #24 is about restart recovery leaving interrupted assistant messages under-described.

    This case looks separate because:

    • the stored error is the generic The operation was aborted. instead of the restart-specific message
    • there are no dangling tool parts to recover
    • the prior tool-calls turn completed cleanly
    • the next assistant message seems to have been created and then stalled before the first emitted part

    Suspect boundary

    This points more toward the transition between:

    1. completing a tool-calls turn, and
    2. starting the next streamed assistant turn

    than toward tool execution itself.

    The code paths I would inspect first are:

    • packages/opencode/src/session/processor.ts
    • packages/opencode/src/session/llm.ts
    • packages/opencode/src/session/message-v2.ts

    Especially interesting would be whether a provider-side stream timeout / abort can happen after message creation but before any text-start / tool-input-start / step-finish event reaches persistence.

  2. dzianisv commented on Mar 17, 2026

    @dzianisv
    Owner

    Implementation is up in PR #28: #28

    What it does:

    • retries only the zero-part MessageAbortedError case from this issue
    • does not retry aborts after partial assistant output, so it avoids duplicated text or repeated tool side effects
    • adds focused regression coverage for both retry and no-retry paths

    Local validation passed:

    • cd packages/opencode && bun test test/session/retry.test.ts test/session/processor.test.ts
    • cd packages/opencode && bun typecheck

    CI is running on the PR now.

  3. OpenCodeEngineer commented on Mar 29, 2026

    @OpenCodeEngineer
    Author

    Investigated another reproduction (ses_2c8399c5bffeHbC41i3Zin61FK) and the failure mode is now clearer.

    This is not a tool-result deadlock. In the persisted session data, tool calls complete normally, and the later assistant turns abort with MessageAbortedError / Tool execution aborted after the next async turn has already started.

    Root cause

    • packages/opencode/src/project/instance.ts
    • Instance.provide() incremented the instance refcount, but inside try/finally it returned context.provide(...) without await.
    • Because of that, the finally ran immediately after returning the promise, so refs could drop to zero while async work was still running.
    • Idle/limit disposal could then tear down Instance.state for a live session/tool execution, which explains the later aborts.
    • Detached/background work from /:sessionID/prompt_async was especially vulnerable because it also needed to re-enter the instance/workspace context explicitly.

    Fixes applied

    • packages/opencode/src/project/instance.ts
      • changed return context.provide(...) to return await context.provide(...) so refs are held until async work settles.
    • packages/opencode/src/server/routes/session.ts
      • wrapped prompt_async detached execution in WorkspaceContext.provide(...) and Instance.provide(...) with InstanceBootstrap, so background prompts keep the correct context and instance lifetime.
    • Added regressions:
      • packages/opencode/test/project/instance.test.ts
        • holds refs until async work settles
      • packages/opencode/test/server/session-messages.test.ts
        • prompt_async keeps an instance ref until detached work finishes
    • Hardened timing-sensitive tests exposed by this fix:
      • packages/opencode/test/question/question.test.ts
      • packages/opencode/test/permission/next.test.ts
      • these now wait for pending requests/questions to actually enqueue before asserting.
    • Removed a flaky slow subprocess from packages/opencode/test/tool/bash.test.ts so the full suite stays stable under package-wide load.

    Verification

    • bun test test/project/instance.test.ts
    • bun test test/server/session-messages.test.ts
    • bun test test/question/question.test.ts
    • bun test test/permission/next.test.ts
    • bun test test/tool/bash.test.ts
    • bun test
    • bun typecheck

    Final package verification is green:

    • 1480 pass
    • 8 skip
    • 0 fail

    This should cover the “looks stuck on tool call, then later aborts” class of failures caused by instance disposal racing active async work.

  4. dzianisv commented on Jun 21, 2026

    @dzianisv
    Owner

    Session recovery (Session.recover()) now patches orphaned messages. The specific 16.7-minute hang scenario was a provider-level timeout issue — now handled by SSE timeout + retry infrastructure in session/retry.ts. Closing as addressed by current architecture.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions