Skip to content

Stale "busy" in session_status never clears - session shows "working" forever (bootstrap uses bare setStore, not reconcile) #32127

@zoulukuang

Description

@zoulukuang

Summary

A session can show the "working" indicator forever. session_working(id) is derived purely from session_status (in child-store.ts: (session_status[id]?.type ?? "idle") !== "idle"), but the frontend's session_status is reconciled from session.status() using a bare setStore instead of reconcile — so an entry the backend no longer reports as busy is never removed from the store.

Root cause

In packages/app/src/context/global-sync/bootstrap.ts, two adjacent lines handle full-snapshot data differently:

// config — reconcile, correctly drops removed keys
retry(() => input.sdk.config.get().then((x) => input.setStore("config", reconcile(x.data!, { merge: false }))))

// session_status — bare setStore, cannot drop removed keys
retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!)))

session.status() returns only non-idle sessions — idle ones are deleted from the in-memory map in packages/opencode/src/session/status.ts:

if (status.type === "idle") {
  yield* events.publish(Event.Idle, { sessionID })
  data.delete(sessionID)
  return
}

SolidJS merges object keys on setStore — it does not remove keys absent from the new value. So a stale { ses_x: "busy" } in the store survives when session.status() no longer includes ses_x, and session_working(ses_x) stays true permanently.

When it happens

Whenever the store holds a busy/retry entry that the backend has since cleared but the corresponding session.idle event didn't reach the store — e.g. it was dropped during a brief disconnect, or the server restarted and lost its in-memory status map. On the next bootstrap / reconnect, session.status() comes back without that session, but the bare merge keeps it busy, so the session is stuck "working" with no way to clear it short of a full client restart.

Fix

Reconcile session_status the same way the adjacent config line already does:

input.setStore("session_status", reconcile(x.data ?? {}, { merge: false }))

PR incoming.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions