Skip to content

Web UI: session.updated for off-store archived session crashes SSE event reducer -> infinite 'Thinking' + dead stop button #35157

Closed as not planned

Description

@enjoyer-hub

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 here

When 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

  1. Run opencode web, open a project in the browser.
  2. 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:
    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}}'
    (Any flow that emits session.updated with time.archived set for a session absent from the store reproduces it — e.g. archiving old sessions from another tab/TUI while the browser tab is open.)
  3. Observe the TypeError: Cannot read properties of undefined (reading 'time') in the browser console.
  4. 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.:
    if (result.found && input.store.session[result.index]!.time.archived === info.time.archived) break
    (or drop the dedupe check when !result.found and 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.0 behind 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

  1. github-actions commented on Sep 2, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions