Repository navigation
Description
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
readtool parts:toolu_vrtx_018q8BECJMjEm7NBc1wXgeJ7toolu_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.256Ztime.completed = 2026-03-17T02:25:05.327Zparts = []tokens = 0cost = 0finish = null
Important negative evidence
- There are zero parts attached to
msg_cf98d2ac0001ltRD6xGVR9b5Sv. - There are zero
pendingorrunningtool 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:
- The assistant completed a
tool-callsturn successfully. - The loop started the next assistant message immediately afterward.
- That follow-up stream never reached
text-start,tool-input-start, orstep-finish. - The request remained in-flight until an abort surfaced.
- 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
AbortErrortoMessageAbortedError
- maps
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-callsassistant 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
OpenCodeEngineer commentedon Mar 17, 2026
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_30acd1e73ffeo2CfsBDMfUg1eysession.title = CDP a11y extractor wiringsession.time_updated = 1773713304294Aborted assistant message row:
message.id = msg_cf98d2ac0001ltRD6xGVR9b5Svmessage.role = assistantmessage.parentID = msg_cf98c5c13001isct58xi64GQoumessage.providerID = github-copilotmessage.modelID = claude-opus-4.6message.time.created = 1773713304256(2026-03-17T02:08:24.256Z)message.time.completed = 1773714305327(2026-03-17T02:25:05.327Z)message.error.name = MessageAbortedErrormessage.error.data.message = The operation was aborted.message.parts count = 0Last successful assistant tool turn before the abort:
message.id = msg_cf98d17840018Ucq4aNqWeJ04Ifinish = tool-calls2026-03-17T02:08:24.254Zreadtool parts:prt_cf98d24db001nq4jG0TgQyz6RRprt_cf98d2977001Sb67qu823Zp1Lolib/agent/PageContentProvider.tsNegative checks:
select count(*) from part where message_id='msg_cf98d2ac0001ltRD6xGVR9b5Sv';=>0select count(*) from part where session_id='ses_30acd1e73ffeo2CfsBDMfUg1ey' and json_extract(data,'$.state.status') in ('pending','running');=>0Why this seems different from #24
Issue #24 is about restart recovery leaving interrupted assistant messages under-described.
This case looks separate because:
The operation was aborted.instead of the restart-specific messagetool-callsturn completed cleanlySuspect boundary
This points more toward the transition between:
tool-callsturn, andthan toward tool execution itself.
The code paths I would inspect first are:
packages/opencode/src/session/processor.tspackages/opencode/src/session/llm.tspackages/opencode/src/session/message-v2.tsEspecially interesting would be whether a provider-side stream timeout / abort can happen after message creation but before any
text-start/tool-input-start/step-finishevent reaches persistence.dzianisv commentedon Mar 17, 2026
Implementation is up in PR #28: #28
What it does:
MessageAbortedErrorcase from this issueLocal validation passed:
cd packages/opencode && bun test test/session/retry.test.ts test/session/processor.test.tscd packages/opencode && bun typecheckCI is running on the PR now.
OpenCodeEngineer commentedon Mar 29, 2026
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 abortedafter the next async turn has already started.Root cause
packages/opencode/src/project/instance.tsInstance.provide()incremented the instance refcount, but insidetry/finallyit returnedcontext.provide(...)withoutawait.finallyran immediately after returning the promise, so refs could drop to zero while async work was still running.Instance.statefor a live session/tool execution, which explains the later aborts./:sessionID/prompt_asyncwas especially vulnerable because it also needed to re-enter the instance/workspace context explicitly.Fixes applied
packages/opencode/src/project/instance.tsreturn context.provide(...)toreturn await context.provide(...)so refs are held until async work settles.packages/opencode/src/server/routes/session.tsprompt_asyncdetached execution inWorkspaceContext.provide(...)andInstance.provide(...)withInstanceBootstrap, so background prompts keep the correct context and instance lifetime.packages/opencode/test/project/instance.test.tsholds refs until async work settlespackages/opencode/test/server/session-messages.test.tsprompt_async keeps an instance ref until detached work finishespackages/opencode/test/question/question.test.tspackages/opencode/test/permission/next.test.tspackages/opencode/test/tool/bash.test.tsso the full suite stays stable under package-wide load.Verification
bun test test/project/instance.test.tsbun test test/server/session-messages.test.tsbun test test/question/question.test.tsbun test test/permission/next.test.tsbun test test/tool/bash.test.tsbun testbun typecheckFinal package verification is green:
1480 pass8 skip0 failThis should cover the “looks stuck on tool call, then later aborts” class of failures caused by instance disposal racing active async work.
dzianisv commentedon Jun 21, 2026
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 insession/retry.ts. Closing as addressed by current architecture.