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.
Summary
A session can show the "working" indicator forever.
session_working(id)is derived purely fromsession_status(inchild-store.ts:(session_status[id]?.type ?? "idle") !== "idle"), but the frontend'ssession_statusis reconciled fromsession.status()using a baresetStoreinstead ofreconcile— 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:session.status()returns only non-idle sessions — idle ones aredeleted from the in-memory map inpackages/opencode/src/session/status.ts: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 whensession.status()no longer includesses_x, andsession_working(ses_x)staystruepermanently.When it happens
Whenever the store holds a
busy/retryentry that the backend has since cleared but the correspondingsession.idleevent 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 itbusy, so the session is stuck "working" with no way to clear it short of a full client restart.Fix
Reconcile
session_statusthe same way the adjacentconfigline already does:PR incoming.