Repository navigation
Description
Web UI: new sessions stuck on "Thinking" forever + stop button appears dead — unguarded index access in session.updated archived handler kills the SSE event loop
Describe the bug
In opencode web (browser client), a single session.updated event for an archived session that is not present in the client's session store throws an uncaught TypeError inside the global SSE event reducer. Because the exception propagates out of the event handler, all subsequent SSE events for that directory stop being applied. From the user's perspective:
- New messages never render — the UI shows "Thinking" indefinitely, even though the server finished the response in seconds (confirmed via server logs / API).
- The stop button appears broken — the abort request reaches the server (log shows
cancel), but the UI never updates. - Reloading the page shows the completed reply, which confirms the server side is healthy and it's purely a client-side event-processing crash.
Console error:
TypeError: Cannot read properties of undefined (reading 'time')
at D2e (assets/index-B-ada5Lh.js:1015:15384)
at ...
at FH.emit (...)
Root cause
packages/app/src/context/global-sync/event-reducer.ts, session.updated case:
case "session.updated": {
const info = (event.properties as { info: Session }).info
const result = Binary.search(input.store.session, info.id, (s) => s.id)
if (info.time.archived) {
if (input.store.session[result.index]!.time.archived === info.time.archived) break
// ^^^^^^^^^^^^^^^ result.found is NOT checked hereWhen the archived session is not in the store (e.g. it was archived earlier, trimmed by the retained limit, or archived in another client/via API), Binary.search returns found: false and result.index is an insertion point — so input.store.session[result.index] can be undefined (the non-null ! hides this from the compiler). Reading .time on it throws.
Note the sibling session.created / non-archived session.updated branches all check result.found before indexing; only the archived branch dereferences the element unguarded.
Steps to reproduce
- Run
opencode web, open a project in the browser. - Have at least one session in that directory that is already archived (so it is not in the client store). Re-trigger an archive update for it via the API:
(Any flow that emits
curl -X PATCH "http://127.0.0.1:PORT/session/SESSION_ID?directory=%2Fpath%2Fto%2Fproject" \ -H "Content-Type: application/json" \ -d '{"time":{"archived":1783099999999}}'
session.updatedwithtime.archivedset for a session absent from the store reproduces it — e.g. archiving old sessions from another tab/TUI while the browser tab is open.) - Observe the
TypeError: Cannot read properties of undefined (reading 'time')in the browser console. - Now send a message in any session (new or existing) in that tab: the response streams on the server but the UI stays on "Thinking" forever, and the stop button does not visibly work. Reload shows the finished reply.
Expected behavior
- The archived branch should guard the store access, e.g.:
(or drop the dedupe check when
if (result.found && input.store.session[result.index]!.time.archived === info.time.archived) break
!result.foundand proceed straight to cache cleanup). - Ideally the SSE dispatch loop should also catch per-event reducer errors so a single malformed/unexpected event cannot permanently kill live updates for the whole directory.
Environment
- opencode v1.17.13 (also reproduced on v1.17.8 sessions),
opencode web --port 4096 --hostname 0.0.0.0behind basic auth - Server: Ubuntu 24.04, x86_64
- Client: Chromium-based browser
Verification
I binary-patched the served bundle on my server, replacing
input.store.session[result.index].time.archived === info.time.archived semantics with an optional-chained access (?.time.archived ==), restarted the service, and:
- The repro PATCH above no longer throws in the console.
- New sessions render replies live without reload.
- The stop button interrupts streaming and shows "Interrupted" immediately.
So the one-line guard fully resolves the user-visible "infinite Thinking / dead stop button" symptom.
Activity
github-actions commentedon Sep 2, 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.