Skip to content

feat(conversations): rebuild the conversation picker on provider-native indexes - #899

Merged
Juliusolsson05 merged 31 commits into
mainfrom
feat/session-picker
Sep 12, 2026
Merged

Juliusolsson05 merged 31 commits into
mainfrom
feat/session-picker

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Problem

The resume picker capped at 20 rows, never visited worktree project dirs, could not tell orchestration children from real sessions, labelled every Codex row with the injected AGENTS.md text, ordered by file mtime, and the prompt search folded whole transcripts per keystroke. View Prompts and Rewind capped at 15 and 30. Measured evidence and the staged decomposition: docs/decomposition/conversations.md.

What this does

One Conversations picker behind Resume Session… and Search Conversations…, fed by one catalog over the providers' native indexes: Codex state_N.sqlite (read-only through node:sqlite), Claude project dirs merged across git worktree list plus ai-title records and history.jsonl, and OpenCode opencode.db. Rows carry a label with provenance, the Agent Code spoken name and title from a new durable ledger, worktree, relative time and prompt count. Orchestration children, native subagents and codex exec runs are hidden behind a counted toggle; ordering is the user's last activity. The path picker, View Prompts, Rewind and the external nativeHistory.* capabilities read the same catalog. The old listers, index, IPC and modals are deleted.

Behaviour changes

  • Codex rows are labelled by their first real prompt or the user's thread name, never the AGENTS.md injection.
  • Claude rows label by custom title, then AI title, then first prompt; a nameless session shows an italic fallback instead of being dropped.
  • Default scope is the repository across all worktrees; this folder and everywhere are one click.
  • Prompt lists are uncapped and show relative time with the absolute time on hover.
  • nativeHistory.list and nativeHistory.search now cover OpenCode and report honest coverage.

Verification

  • Corpus recorded from the author's real stores (testing/fixtures/conversations/, 1,393 conversations, redacted by a checked policy and verified by check:conversation-fixtures); expectations drafted from the unredacted mirror and reviewed with the user. Making catalog and expectations agree surfaced four corpus-fidelity gaps, each fixed in the recorder rather than in the rules.
  • System tests over the corpus: family resolution, each adapter, the ledger, the catalog listing, the service. Unit tests: wrapper unwrapping, classification and label rules, the IPC contract with the bug(resume): Codex picker can show no results while matching rollouts exist #718 evidence breadcrumb re-homed. Renderer tests: picker filters, keyboard, resume dispatch, failure versus empty, prompt list, path picker coherence.
  • Live suite on the author's machine (npm run test:live:conversations), measured while other agent sessions kept the load average near 20: cold list 762 ms (8.5 s before the Codex union and head bounds; Codex alone 7.0 s to 0.33 s, Claude 605 ms to 304 ms), warm 45 ms, first search 1.4 s, second search under 100 ms. The 500 ms cold and 750 ms first-search budgets from the decomposition are not met on that loaded machine; the suite keeps asserting them so the next measurement on a quiet machine is honest.
  • npm run typecheck (both projects), npm run check:keybindings, npm run test:contract, npm test: everything green except the two known local failures (the image attachment corpus cites a deleted local session; the remote feed integration test is the intermittent test(remote): investigate intermittent live transcript bootstrap failure #847).

Known limitations

Fixes #874
Fixes #96
Fixes #151
Fixes #718
Fixes #773
Refs #735, #739, #762

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby

Juliusolsson05 and others added 30 commits September 11, 2026 10:11
Stage decomposition for replacing the resume picker, prompt search, path
picker list and in-session prompt lists with one catalog fed by the
providers' native indexes (Codex state SQLite, Claude project dirs plus
ai-title records and history.jsonl, OpenCode's database).

Measured on the author's machine on 2026-09-11: a hard cap of 20 rows,
worktree transcripts never visited, orchestration children indistinguishable
from real sessions (16 of 57 here, 66 of 98 in bringdown), Codex rows
labelled with the injected AGENTS.md message, and 1.5-2.9 s per Codex
picker open where the index answers in 50 ms. Records the user's decisions
so no later session re-litigates them.

Refs #96, #151, #718, #735, #739, #762, #773

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
Twenty-three tasks over the five approved stages of
docs/decomposition/conversations.md, each with its failing test written
first against the recorded corpus, the implementation, the exact commands
and the commit. Real signatures throughout: the family resolver runs the
same `git worktree list` upstream Claude Code uses, the Codex adapter reads
the native sqlite index read-only with a column probe and a scan fallback,
the ledger is projected from workspace saves through a new store observer,
and the live suite is the only place the timing budget is measured.

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
The single publication gate for the recorded conversation corpus: a closed
allowlist of structural keys stays verbatim, paths rewrite to /fixture/*,
branches other than main hash, and the wrapper prefixes the catalog keys on
survive so a fixture can still prove the unwrapping rule.

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
Records this machine's Claude project directories (159 family transcripts
across 44 dirs, 105 of them orchestration children), Codex's state_5.sqlite
index (1,125 family threads plus a 25-row control) with sampled rollouts,
and OpenCode's database (94 family sessions) into testing/fixtures/
conversations, redacted through the policy, with an unredacted git-ignored
mirror for the live suite.

The family root is the first `git worktree list` entry, not the checkout the
script runs from, or a worktree run would file the main repository as
"another project". The policy grew three rules the first recording proved
necessary: object keys that are paths, relative-path keys inside
trackedFileBackups, and file names under this repo's own Claude project
directory are content, not structure. Verification re-runs redaction over
the committed corpus and requires a no-op, so every rule is idempotent.

node:sqlite is typed by a minimal ambient declaration because @types/node is
pinned to 20 while Electron 43 bundles Node 24.

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
…r's expectations

The installer materialises the committed corpus as a real ~/.claude,
~/.codex and ~/.local/share/opencode under a temp root so adapters run
with no test-only branches. The expectations file is the user's answer,
reviewed on 2026-09-11, to "which of these 1,377 conversations are mine":
357 user sessions, 355 orchestration children, 278 native subagents, 381
exec runs, 3 projected, 3 empty placeholders. The draft script that
produced it deliberately shares no code with the catalog.

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
…lution

The family is the unit the picker lists: the main checkout plus every
`git worktree list` path plus anything under them, matched case-
insensitively on darwin. Roots and candidates are compared in both literal
and realpath form (git prints /private/var while providers record /var),
and a candidate that no longer exists resolves through its nearest existing
ancestor so a pruned worktree's transcripts still belong here. Verified on a
real repository with a real `git worktree add`.

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
…lumn probe

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
…ssionIndex

The byte-range prompt reader (#735) becomes src/main/conversations/prompts/
promptFolder.ts so the catalog can list, search and show a conversation's
prompts without the discovery and search code around it; its twelve tests
move with it. One deliberate change: wrapper-prefixed texts are no longer
dropped here, because `<stt …>` is a prompt the user dictated and
`<orchestration-handoff>` carries the child's task. The legacy index keeps
its old behaviour by filtering at its two call sites until it is deleted.

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
…oject dirs

Candidate directories come from the family's sanitized roots plus their `-`
continuations (worktrees, subdirectories, pruned worktrees); membership is
then decided by each transcript's recorded cwd. The head read is record-
bounded because modern transcripts open with several kilobytes of hook and
environment attachments before the first user record; the tail read finds
ai-title, customTitle and the newest user timestamp. Activity and prompt
counts come from history.jsonl.

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
…with scan fallback

The index is Codex's own resume source: cwd, the real first prompt as
title, recency, source, subagent provenance and spawn edges. Rows whose
rollout is gone stay listed as unavailable; rollouts the index never saw are
unioned from one cached walk through a local head reader; a missing or
drifted index downgrades to that reader with a recorded reason.

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
Reads the session table with the family predicate in SQL and the first user
text parts per session, skipping synthetic compaction continuations. The
app's own placeholder title for a fresh OpenCode Terminal session is
reported as no title so an unprompted shell classifies as empty. The corpus
policy keeps that app-authored constant and the identifier keys
(message_id, parent_id, project_id, workspace_id) verbatim, and the corpus
was re-recorded so the fixture exercises both.

Refs #773, #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
…r activity

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
…source

The catalog's one entry point: sources in, rows out with kind, label
provenance and activity, hidden children counted, cursor paging, and a
search that reads prompts only when asked. The listing test replays the
recorded corpus against the user-authored expectations, and making the two
agree exposed four corpus-fidelity gaps rather than catalog bugs: empty
strings were hashed into a placeholder the catalog read as a first prompt,
a cwd-less bridge-session stub from another project was filed into the
family root, Codex rollouts carried no recorded mtime so an unindexed
synthesized rollout sorted first by extraction time, and the draft never
enumerated unindexed rollouts. The policy keeps emptiness, the extractor
files stubs by their original directory and records rollout mtimes, the
installer applies every recorded mtime, and the draft restates the last
label rung.

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
…n a ledger

Once a pane closes the workspace file forgets its title, spoken name and
orchestration role, and the only way left to tell a review child from a
real session is a prompt sniff. The ledger is a projection of every
window's sessions keyed by provider-native id, appended on each workspace
commit through a store observer and compacted on open, so the catalog can
join durable identity against any transcript on disk. Agent names are
read from the registry file without allocating: resolve() spends an
address on a miss, and a projection must never do that.

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
…m one cached service

The service is the catalog's only consumer and the only owner of caches:
discovery per family for a short freshness window so a picker keystroke
never walks the Claude project dirs twice, and prompt texts per row keyed
by activity so a second search keystroke costs nothing. Claude search
reads history.jsonl; Codex and OpenCode search is bounded to the newest
two hundred rows because their prompts live in transcripts. The worktree
lister is injected rather than imported because the git IPC module pulls
electron in on its first line and the system tests run under plain Node.

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
…C surface

Three channels replace the per-lister channels that each returned a
different row shape. A listing failure rejects instead of returning an
empty response, because an empty list means the stores were read and held
nothing; collapsing the two erased the only distinction #718 needed. Each
listing leaves a breadcrumb correlated by a per-process cwd fingerprint
that never retains the cwd.

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
…earch Conversations

Both commands now open the same surface: every conversation of the focused
repository's worktree family across all three providers, ordered by the
user's last activity, with orchestration children behind a toggle, scope
and provider filters, search by title, name or prompt, a live preview and
Enter to resume. Resume uses the row's own cwd and provider, never the
focused pane's, because the picker lists other worktrees and providers on
purpose. The preview header shows the catalog's whole-conversation prompt
count rather than the tail window's floor.

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
…r through the catalog

The path picker's resume rows are now catalog rows rendered by the same
component as the Conversations picker, so a session looks the same in
both places and naming, provenance and order are decided in main. Scope
is the typed directory alone, because a worktree's sessions listed under
the main checkout would resume in the wrong tree. The resume-target
coherence contract is unchanged: an accepted row disappears the moment
the provider toggle changes, and a listing failure is shown as a failure.

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
…d Rewind

View Prompts read a fifteen-prompt tail of the in-memory feed and Rewind a
thirty-prompt page from main; on any long session the prompt the user was
looking for was outside both windows. Both now list every prompt of the
transcript, newest first, with "3h ago" as the primary label and the
absolute time on hover, through one shared list component. The rewind
lister caps only when a caller asks for a bounded page.

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
External operators now read the same rows the in-app picker shows: every
provider including OpenCode, every worktree of a cwd's repository, ordered
by the user's last activity, with the coverage block reporting what is
actually true instead of the old 400-candidate budget. Output schemas are
unchanged so existing operators keep parsing.

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
…s not know

The first live run against the author's stores took 8.5 s to list and
never finished a search. Three causes, each now bounded and recorded in the
corpus: the Codex union keyed "unindexed" off the family-filtered query, so
nine hundred rollouts of other projects had their heads read on every cold
discovery; a rollout head with no user event was streamed to its end; and a
search folded whole rollouts (1.2 GB for the newest two hundred) while
OpenCode prompts spawned the CLI per session. The union is now keyed by
every indexed thread, head reads stop at the record limit, search folds
carry a byte budget that a later full read continues from, and OpenCode
prompts come from its database. The corpus now holds rollouts of indexed
control threads so the union regression is covered.

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
The catalog's folder already returns prompts newest first; reversing them
again showed the oldest prompt at the top.

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
Opt-in suite that lists and searches this repository's conversations
through the production wiring and asserts the timing budget from the
decomposition. It is the only place the budget is measured against real
stores, and it found the three discovery and search costs the corpus
could not show.

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
…pts through a pool

The 64 KB head block rarely holds six user texts, so nearly every Claude
transcript fell through to a stream that parsed up to two hundred records
of any size, one file after another. The head now reads at most a second
256 KB block for the four texts classification needs, and transcripts are
summarised eight at a time. Search folds are trimmed to 150 rows at 128 KB
because 50 MB of rollout tails per keystroke measured seconds under load.
Live Claude discovery on the author's store: 605 ms to 304 ms.

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
…hree old pickers

Every reader of past conversations now goes through the conversation
catalog, so the Claude session lister, the prompt index, the sessions IPC
and preload surface, the palette's resume mode, the prompt search modal
and the per-provider listing members of the registry are gone. The
provider feature matrix loses savedSessionListing because listing is no
longer something a provider can lack: the catalog reads each native index
itself, OpenCode included. The new Conversations feature carries its own
control reference so external operators learn where the commands went.

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
Resolves the three conflicts from Root Agent Code Management (#907): the
control host is now built before IPC registration, so the conversation
service is constructed right after the ledger and passed there; the
registry keeps main's transcript-path resolver import without the deleted
listers; both the TLDR and the Conversations feature references stay.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
…search budget and resumability

A natively moved Claude session (main's #883 appends a `relocated` record
and renames the transcript into the worktree's project dir) now lists and
resumes under the worktree it moved to, and a redirect stub never outranks
the transcript: the largest file per session id wins. Codex and OpenCode
bind lowercased roots so `lower(cwd) = ?` holds on case-sensitive file
systems too. Prompt search spends its row budget only on rows the request
can show, so hidden children and excluded providers no longer push a
visible row out of it. A row whose transcript file is gone is listed for
the record and refused as a resume target in both pickers, a head read
that hit its byte bound no longer classifies as empty, the picker asks for
a pane instead of claiming an empty repository, project directories are
matched with the family's case folding, and the footer counts matches when
a query is set.

Refs #874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Review resolution

One Claude orchestration agent reviewed the branch read-only against a written brief (verdict: FIX FIRST). Everything it rated should-fix is resolved on this branch; the review itself is kept out of the repository.

Adopted

  • Merged origin/main (merge commit, no rebase). The control host is now built before the workspace store opens, so nativeHistory.* receives a getter for the conversation service; both submodule pointers follow main.
  • Relocated Claude sessions (bug(claude): recover transcripts relocated into worktrees #883's relocated record, head or tail) list and resume under the worktree they moved to; a redirect stub never outranks the transcript. System test added.
  • Codex and OpenCode bind lowercased roots, so lower(cwd) = ? holds on case-sensitive file systems. System tests added for both adapters.
  • Prompt search spends its row budget only on rows the request can show.
  • Unavailable rows are refused as resume targets in both pickers, with a reason. Renderer tests added.
  • The picker asks for a pane when none is commanded and lists everywhere without one. Renderer test added.
  • A head read that hits its byte bound no longer classifies as empty (rule test); Claude project directories are matched with the family's case folding; the footer counts matches when a query is set; the Codex adapter reuses rollout paths learnt at discovery instead of opening the index per row.
  • Stale comments and the one orphaned type block that still described the deleted listers are gone; View Prompts has a modal-level test that it stays newest first.

Not adopted

  • The performance span keeps its sessionIndex.extractPrompts name so existing dashboards keep their series; the folder's module comment says where it lives now.
  • OpenCode still opens its database once per prompts() call: the query is indexed and the open is cheap; caching a handle across a search would be the only saving.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ukb9oTvJaojxUxoHfYEUby

@Juliusolsson05
Juliusolsson05 merged commit c33a10f into main Sep 12, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the feat/session-picker branch September 12, 2026 03:36
Juliusolsson05 added a commit that referenced this pull request Sep 12, 2026
…atalog

#899 landed the conversation-picker rebuild while this branch was in review,
and it supersedes this branch's feature D. Session listing is no longer a
provider-registry concern at all: `src/main/conversations` reads each
provider's native index directly and serves the pickers AND the external
nativeHistory capabilities from one place — including its own OpenCode source,
which closes #773 the same way this branch intended and cites it by number.

So the registry's `listSessions`/`listAllSessions`, the shared row mapper, and
the registry listing system test are all removed here rather than fought for.
Two implementations of "list OpenCode sessions" would be worse than either
alone, and the catalog's is the one the app now routes through.

What this branch keeps, because it is genuinely its own and main has no
version of it: the provider-owned history source (`loadHistoryChunk`), the
non-file transcript identity (`transcriptLocator` / `parseTranscriptLocator` /
`transcriptLastModifiedAt`), and everything downstream of them. Taking main's
`providerConfig.ts` wholesale dropped those on the first pass; they are
restored on top of it, and `listSessions` deliberately is not.

Also accepts main's deletion of `sessionIndex.ts` and the old resume picker
hook, both retired by the same PR.

The duplication that remains — the package's `OpencodeStore` and the catalog's
own SQLite reader both reading `opencode.db` — is real and is noted as a
follow-up on #910 rather than papered over.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Juliusolsson05 added a commit that referenced this pull request Sep 12, 2026
Written hours before #899 landed a conversation catalog that reads the
database directly. The line claimed the Resume picker uses this listing; it
does not, because that picker no longer exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Juliusolsson05 added a commit that referenced this pull request Sep 12, 2026
Textually clean, and the typecheck on the merged tree is clean. That is not
taken as proof of anything: the previous merge (#899) resolved cleanly in
places and still broke this branch's contract. The overlaps worth naming:

- #911 stops a stale session list from severing a mounted transcript view in
  the remote feed. This branch pins, in `forwarder.test.ts`, that the remote
  feed's plain-terminal filter does not drop an OpenCode Terminal pane (whose
  kind is the provider and only its runtime is terminal). Eviction is a second
  way a pane can leave the phone's view, so the two are checked together.
- #912 stops agent clones inheriting root control and resuming provisional
  ids, and re-applies MCP preferences on reload. This branch added
  `providerRuntime` to orchestrated agent creation and spawns the OpenCode TUI
  with built-in MCP launch config, so both paths are in scope for review.

Both are handed to an independent pre-merge review rather than asserted here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXcU2JMay6xCk8NKJ6cTgj
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment