Repository navigation
Description
Summary
The TUI session picker silently hides root sessions on projects that accumulate many subagent children. Sessions still exist in opencode.db but never appear in the picker, even when launched from the project worktree root.
Reproduction
On a project with many root sessions and a much larger number of subagent children (in my case: 23 roots, ~680 children for one project), launch the TUI from the worktree root and open the session picker. Older root sessions are missing from the list. SQLite confirms they exist with the correct project_id, parent_id IS NULL, recent time_updated, and no path filter.
Root cause
The server-side SQL in Session.listByProject applies LIMIT 100 ordered by time_updated DESC before filtering subagent children. Children are only excluded at the SQL level when input.roots is truthy:
https://github.com/sst/opencode/blob/main/packages/opencode/src/session/session.ts#L992-L1045
The TUI strips children client-side but never passes roots: true to the SDK, so subagent children compete with root sessions for the 100-row budget. When children dominate recent updates, older roots fall outside the cutoff before the TUI ever sees them.
Two TUI call sites are affected:
-
Bootstrap list —
packages/tui/src/context/sync.tsx:156-160function listSessions() { return sdk.client.session .list({ start: Date.now() - 30 * 24 * 60 * 60 * 1000, ...sessionListQuery() }) .then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id))) }
-
Picker live search —
packages/tui/src/component/dialog-session-list.tsx:36-43const [searchResults, { refetch }] = createResource( () => ({ query: search(), filter: sync.session.query() }), async (input) => { if (!input.query) return undefined const result = await sdk.client.session.list({ search: input.query, limit: 30, ...input.filter }) return result.data ?? [] }, )
The TUI's existing client-side parentID === undefined filters at dialog-session-list.tsx:136,158 and local.tsx:461 make the resulting list look correct in low-volume cases, which is why this hasn't surfaced widely.
Proposed fix
Pass roots: true from both TUI call sites. The SDK already supports it (packages/sdk/js/src/v2/gen/types.gen.ts declares roots?: boolean | "true" | "false") and the server already filters on it (session.ts:1022-1024 adds isNull(SessionTable.parent_id)). With roots: true, the SQL excludes children before LIMIT, so the 100-row budget is spent on root sessions only.
Diff:
--- a/packages/tui/src/context/sync.tsx
+++ b/packages/tui/src/context/sync.tsx
@@
return sdk.client.session
- .list({ start: Date.now() - 30 * 24 * 60 * 60 * 1000, ...sessionListQuery() })
+ .list({ start: Date.now() - 30 * 24 * 60 * 60 * 1000, roots: true, ...sessionListQuery() })
--- a/packages/tui/src/component/dialog-session-list.tsx
+++ b/packages/tui/src/component/dialog-session-list.tsx
@@
- const result = await sdk.client.session.list({ search: input.query, limit: 30, ...input.filter })
+ const result = await sdk.client.session.list({ search: input.query, limit: 30, roots: true, ...input.filter })Verification
bunx tsgo --noEmitinpackages/tuipasses with zero errors.- Manually verified: previously-missing root sessions reappear in the picker after the change. Subagent children remain hidden as before.
Alternative considered
Server-side: always add parent_id IS NULL in listByProject, or call it whenever the HTTP handler is reached. Rejected because it changes semantics for any non-TUI consumer of GET /session who legitimately wants both roots and children. Client-side opt-in is the lower-blast-radius fix.
Happy to open a PR with the diff above.
Activity
github-actions commentedon Jun 11, 2026
This issue might be a duplicate of existing issues. Please check:
LIMIT 100result window; fix is to passroots: truefrom both TUI call sites.adamdama commentedon Jun 12, 2026
Yes it is a duplicate. I'll close.