Repository navigation
Description
Sessions show permanent "Thinking..." spinner after stream interruption or server restart
Problem
When an assistant message's stream is interrupted (API error, server restart, network timeout), the message is left with incomplete parts (e.g., step-start + reasoning but no step-finish) and no time.completed set.
The web UI's pending memo identifies these as still-active messages:
// message-timeline.tsx:239
const pending = createMemo(() =>
sessionMessages().findLast(
(item): item is AssistantMessage => item.role === "assistant" && typeof item.time.completed !== "number",
),
)This causes:
- Permanent spinner on the session in both the main chat area and sidebar
- "Thinking..." display that never resolves
- Users cannot tell if the session is stuck or just slow
Root cause
-
The AI SDK stream can die mid-response (API timeout, rate limit, network error). When this happens, the assistant message never gets
time.completedset in the database. -
Server restarts leave the same orphaned state: messages in-flight at shutdown time never get completed.
-
Even with database-level recovery (setting
time.completedon orphaned messages), the frontend's in-memory store may be stale if the page was loaded before recovery ran. No SSE event is emitted when recovery modifies messages.
Observed behavior
After server restart, sessions show:
- Sidebar: spinner icon on sessions that are fully idle
- Chat: "Thinking..." with a spinner that never resolves
- Session status API returns
{}(idle) — confirming the session is not actually active
Last message structure from API after recovery:
{
"info": {
"time": { "completed": 1773622653848 },
...
},
"parts": [
{ "type": "step-start" },
{ "type": "reasoning", "text": "..." },
{ "type": "reasoning", "text": "..." }
]
}Note: time.completed is set correctly, but the message has no step-finish part. If the frontend loaded this data before recovery, the store still has time.completed as undefined.
Suggested fix
Two-sided fix:
1. Frontend (message-timeline.tsx): Make the pending memo also consider session status. If the session is idle with no pending permissions, no message should be considered "pending" regardless of time.completed:
const pending = createMemo(() => {
if (sessionStatus().type === "idle") return undefined
return sessionMessages().findLast(
(item): item is AssistantMessage => item.role === "assistant" && typeof item.time.completed !== "number",
)
})2. Server (session/index.ts): Emit Bus events after Session.recover() so connected frontends update their stores immediately.
Environment
- opencode serve (long-running, multiple sessions)
- Multiple sessions interrupted by server restarts
- Web UI accessed via browser
Activity
fix(webui): prevent stale Thinking spinner on idle sessions
github-actions commentedon Mar 16, 2026
This issue might be a duplicate of an existing issue. Please check:
time.completednull after process kill/restart causing permanent stale UI state via the samependingmemo logic. That issue reports the "QUEUED" badge bug in the TUI; this issue reports the "Thinking..." spinner bug in the Web UI, but the underlying orphaned-message problem and suggested fixes are essentially identical.If you believe these should be tracked separately (e.g. the fix needs to happen in different parts of the codebase), please let a maintainer know.
dzianisv commentedon Mar 16, 2026
Fixed in our fork: dzianisv@4d4605a4c
The fix moves
sessionStatusmemo abovependingand short-circuits: when the session is idle, no message is considered pending regardless oftime.completedstate. This handles both stale store data and recovery-before-SSE scenarios.dzianisv commentedon Mar 16, 2026
I dug further into this on a live broken session and the primary problem was in persisted state, not just frontend staleness.
What I confirmed:
Session.recover()already patches orphaned tool parts toerror, but for orphaned assistant messages it only force-completestime.completederror, recovery left it aserror: nullparts: [],error: null) that looked permanently stuck in the UII repaired one broken live row directly and verified the right behavior is to persist:
MessageAbortedError("Server restarted while the response was in progress")I also patched this locally by changing restart recovery to mark orphaned assistant messages as aborted during startup, with a regression test covering it.
So for this class of failure, the source-of-truth bug is backend recovery:
The frontend can still be more defensive, but the persisted row itself was wrong in the live repro I investigated.
fix(webui): prevent stale Thinking spinner on idle sessions
fix(app): stop using orphaned assistant messages for working state
github-actions commentedon Jul 7, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.