Chats: delete the app's copy of the chat index; every reader asks acpmux (step 2 of #28055)
Summary
Step 2 of #28055 (P-RUST, R7: no permanent second path). The app holds no copy of acpmux's chat index. AcpmuxChatsStore (the mirror whose whole-dictionary copy and deep == on the main thread were the v2.2.9 hangs, base measurement in #28055) is deleted with its last readers:
- New Tab cards: a window over the index (
newtab.cards: the newest 24 top-level chats,AgentPaneDeviceChat.maximumPushed). Only that window's messages push to the pages. - Command palette chats page: a window (
palette.chats) whose spec carries the query. acpmux ranks every chat with the palette's own ranker (cmux-palette-ranker, the crate the palette runs through its C ABI) over the same fields the page searched: title, folder and harness, every word anywhere, best match first. It answers the best 200 and the total that matched; when more match, the page's last row says so ("The best 200 of N matching chats. Type more to narrow."). The query's result ends after that answer (the spinner stops; an empty answer shows the empty state). The window closes with the page. - Search (Cmd-Shift-A) and the Chats & agents scope: they list the same acpmux answer. A contributed page that answers its own queries (
queryResults) has no providers, so the palette asks it for each query through one shared path (PaletteContributedAnswersin CmuxNextPalette), in a later turn than the key's (the chats' choice history is encoded off the main thread, and ChatsFeed encodes each request on the connection's serial queue).- Search: the chats show after the best ranked section (where the ranker put the best row), with every row the ranked rows leave under the 400-row page and at least their best 10 (taken from the weakest ranked rows when the page is full). A cut answer ends with a row that says how many more chats match and opens the Agent Chats page with the same query. Return runs the best ranked row of the current query; with no ranked row it waits for the chats answer. When rows above the selected one grow, the list scrolls to it.
- Chats & agents scope: the chats lead the page, and its rows are not final until acpmux answers, so a Return runs the best chat, as on the chats page itself. No timer: without a connection, when acpmux refuses the query, or when its index is not ready (
ready: false), the answer ends at once with no rows and Return runs the best agent session. When acpmux connects or its index fills the window, the shown query is asked again. - An older query's chats stay on screen only as stand-ins until the new answer replaces them: no key, click or Return selects them, and they are never final. An answer that ends empty (refused, not connected, no match) takes them off at once.
- Leaving the page closes acpmux's window. A window asked again (reconnect, lag) is not answered until its full answer comes.
- Open Chat:
_acpmux/chat_opennow answers the chat itself with its plan. A running chat is found by its key (harness:sessionId). - Cookie import requester: the caller's acpmux session lane by id, O(1). The lane now carries the summary's
preview. - ChatsFeed keeps the one connection, the windows and an index-change signal for the projects relay. The watch asks for a one-chat page, and its lines reach the main thread with no chat in them. This also fixes review F9 of #28055 (one copy of the mirror per page).
#28055 has landed; the base is feat-cmux-next. After the base merges, the page moved from CmuxNextApp into CmuxNextAgentLanes (5e88f7951b1) and AppServices holds the lane store (63702fa5c22).
Behavior change: a harness name ("codex") and words that are not next to each other match as before, and a chat the person chose before ranks first among equal matches as before (the page sends its choice history of chats; acpmux ranks with the same palette ranker and the same frecency). The page shows the best 200 matches and says when more match; the empty query shows the newest 200 and says how many more chats there are ("N more chats. Type to search."). Known difference (lead decision): the relative-date text ("yesterday") is no longer searched; it is follow-up 2 in followups.md.
Changelog
Changed: Search Agent Chats shows the best 200 matching chats (or the newest 200) and says how many more there are; it no longer finds a chat by its relative date. In Search (Cmd-Shift-A), Agent Chats is its own section after the best match, in the order the chats index ranks them, and comes in a moment after the other rows; when the page is full it shows the best 10 and a row that opens Agent Chats with the rest.
Testing
-
Rust:
tests/chats_window.rsa_query_window_ranks_like_the_palette_and_reports_how_many_matched("claude" finds every claude-code chat with total 30; "fix build" finds the 15 chats that hold both words) andwindow.rsunit tests ("codex" finds codex chats; non-adjacent words; the folder; an untitled chat). Red at 5c5380f4a32, green at the head. -
Swift:
AgentChatsPaletteAnswerTests(the stream ends after the first answer; the notice row past the page;untitledsent). Red at 833106b01e9 and 59af11da74a, green at the head. -
Swift:
ChatsFeedHoldsNoIndexTests(a 3,000-chat watch page and one index change reach the main thread with noAcpmuxChat). Red at 1790b10f4b0, green at the head (job ids in a comment). -
Rust:
tests/chats_open.rschat_open_then_session_new_adopts_from_the_profile_storechecks that the plan names its chat. Red at 1790b10f4b0, green at the head. -
Swift:
ContributedAnswersTests(review 2 of the fix: a Return before the new query's rank runs its best row; a late chats answer keeps the selected best row first; the chats stay until the next answer replaces them; the Chats scope's Return waits for the chats) andaWindowIsNotAnsweredAgainUntilTheNewConnectionAnswers. Red at 26fb2bc8648, green at 184f853c9ab (job ids in a comment). -
Swift:
AgentChatsJoinSearchTests(Search and the Chats & agents scope, with the realAgentChatsPalettePageas the App contributes it, ask acpmux and list its chat). Red at 6150416c460, green at fdab8c77356 withAgentChatsPaletteAnswerTests,CmdShiftASearchesEverythingTestsandCmdPListsOpenItemsTests(job ids in a comment). -
Compile:
cmux-ci compile-check --app --package CmuxNext --recipe-version 2at the head. -
Lockfile: acpmux now depends on
cmux-palette-ranker. Thecmux-tui/Cargo.lockchange (one line in the acpmux entry) is the output ofcargo update -p cmux-palette-ranker --offline(rewrites the lock only, builds nothing), run on the base lock; it is byte-identical to the committed lock.cargo metadata --locked --offline --format-version 1accepts the head's lock (exit 0) and refuses the base lock with the new dependency (exit 101).
Observed / Not run
Results of the jobs go in comments; the last ones are in issuecomment-6547205694 and the A/B comment.
Observed, STRESS A/B (bench build, 60-workspace STRESS session on a 1920x1200 virtual display, 3000 seeded chats, 220 typed keys of "idle" per side, same Mac cmux23, through the mini scheduler). Before = merge-base 25c15849ff9 plus the bench harness (68c8e0018493), job 84b28b44de05d9aa3cbcd8c4. After = 8dc940d31eb, job f7e2a0be41b5b9de9cc24c01:
| signpost / metric (ms) | before p50 / p95 / p99 / max | after p50 / p95 / p99 / max |
|---|---|---|
key round trip (debug.key) | 1.492 / 5.739 / 6.443 / 6.775 | 1.498 / 5.249 / 6.021 / 6.42 |
key.palette_query_rows (edit to the frame with the final ranked rows) | 9.115 / 12.379 / 13.422 / 15.354 | 8.959 / 13.442 / 18.437 / 22.767 |
| longest main-thread stall while typing | 0 | 0 |
Perf claim: none beyond #28055's counts. This is a correctness change. The table reports the A/B as it was measured; it is not a claim that the change makes Search faster or slower.
Not run:
key.palette_query_answer(the edit to the frame that shows the chats answer) was not measured. No chats answer landed on either side, so that interval has no samples. Why acpmux gave no rows for the 3000 seeded chats is under investigation separately (diagnosis probe 9692561c9f3f8d03130e1e45). At the base, chats were ranked rows from the deleted mirror, so the base can have no answer frame.- A GUI pass of the palette chats page, Search with live acpmux chats, and the New Tab cards.
No capture: the rows look the same; only their source changes.
![View with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Deletes the app's mirror of acpmux's chat index (AcpmuxChatsStore) — the whole-dictionary copy and deep equality on the main thread behind the v2.2.9 hangs in #28055. Every reader now asks acpmux for the chats it needs, so the feed holds no chat of the index.
What changed
- New Tab cards read the newest 8 top-level chats from a window over the index; only that window's changes push to the pages.
- The palette's chats page is a query window: the spec carries the query, the person's chat choice history (frecency) and the page's
untitledword. acpmux ranks with the palette's own ranker by title, folder and harness (every word anywhere; a chat chosen before ranks above an equal match) and answers the best 200 matches and how many matched; a last, disabled row says how many more matched than the page shows, in each locale's plural form. - Search and the Chats & agents scope ask that page for each typed query (
PaletteContributedAnswers). An answer shows only with the current query's ranked rows; an older query's answer is a stand-in that navigation never selects and never holds Return. A non-leading answer gets at least its best 10 rows in Search, and when the page cuts it, ends with a "more" row that says how many more match and opens the Agent Chats page with the query; in the Chats scope the chats lead, so Return waits for the best chat. A query's answer ends at once with no rows when acpmux is not connected, is not ready or refuses the window's request (an older spec's refusal is ignored), and there is no timer: every request ends with rows, with none, or when the query moves on. An answer that changes nothing delivers nothing — the skip compares the shown rows and the held state, so a re-answer with the same chats under new titles still reaches the screen — and shown and selectable rows cut the page at the same place. The page asks the shown query again when acpmux connects or its not-ready index fills the window. _acpmux/chat_openanswers the chat itself with its plan; Open Chat and reveal find a running chat by its key (harness:sessionId).- The cookie import requester reads the caller's acpmux session lane by id (O(1)); the lane now carries the summary's
preview. - The feed keeps one connection, the windows and an index-change signal for the projects relay; the watch asks for a one-chat page and its lines reach the main thread with no chat in them.
- Behavior change: the palette's chats page lists acpmux's best 200 rank matches per query instead of the app filtering every chat by title, key, harness, folder, accounts and roots.
Testing
ChatsFeedHoldsNoIndexTests: a 3,000-chat watch page and one index change reach the main thread with noAcpmuxChat.AgentChatsPaletteAnswerTestsandAgentChatsMoreChatsPluralsTests: a query stream ends after acpmux's first answer, and the more-notice row shows and pluralizes per locale.AgentChatsJoinSearchTestsandContributedAnswersTests: Search and the Chats & agents scope list the chats acpmux answers; old answers are never returned on, chats keep a minimum share of a full Search page (with a more row when cut), a missing, not-ready or refused answer never holds Return, a stale refusal is ignored, the page asks again when acpmux connects, navigation never stops on a stand-in, a retitled re-answer reaches the screen, and an empty answer with no stand-ins changes nothing.tests/chats_open.rschecks that_acpmux/chat_opennames its chat in the plan;tests/chats_window.rscovers a query window's rank order (choice history, harness names, non-adjacent words) and its match total.cmux-ci compile-checkpasses at the head;palette-open-stages.py --search-keystimes typed keys on Search to the frames that show the query's rows and its contributed answer (signpostskey.palette_query_rowsandkey.palette_query_answer), seeding chats where acpmux's index reads them (transcripts under a tempHOME), asking the app's own acpmux for the word before typing and counting the chat rows each key shows;--vdisplayon a 1920x1200 virtual display so the STRESS window fits, report only, no CI.