Chats: acpmux owns the chat list; the sidebar history holds only its window (P0 stall)
Summary
Cause (base measurement, not a gain of this PR). Lawrence's v2.2.9-opt debug.hangs (2026-10-30) show main-thread hangs of 1,148, 730, 656 and 570 ms in the chat index mirror:
AcpmuxChatsStore.apply(Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AcpmuxChatsStore.swift:93,values[key] = chat) copied the whole[String: AcpmuxChat]dictionary for each chat change, becauseSidebarChatHistoryFeed.chatsheld the same copy-on-write storage (ChatsFeed+Start.swift:29).- The
SidebarChatHistoryFeed.chatssetter (SidebarChatHistoryFeed.swift:22) ran the derivedAcpmuxChatsStore.==over every chat and freed the old copy.
Fix (P-RUST, Lawrence 2026-10-30: "move as much as possible into Rust"): acpmux owns the chat list that a client shows.
- Rust:
_acpmux/chats_window {window, filter, limit, topLevel, exclude, keys}answers the window's rows, sorted, filtered and paged by acpmux. After each index or chat activity change,_acpmux/chats_window_changedsends the next revision, the window's keys in order and only the rows whose value changed. A window is calculated again from the index as it is now (level-triggered)._acpmux/chat_projectsgroups the index into each harness's project folders. The connection's window task is owned (R6): it ends with the last window, the connection or the runtime. - Swift:
AcpmuxChatWindowskeeps only the visible rows of each window (value snapshots) and applies a diff in O(window keys + changed rows). A revision gap, an unknown row or_acpmux/laggedasks for the window again, once. The sidebar chat history (Earlier and Pinned) reads two windows. Its whole-index setter is deleted.ProjectsImportrelays acpmux's grouped projects and takes no mirror snapshot. plans/cmux-next/layer-ownership.mdrules 1-3 and L7: Rust owns every state, list, sort, filter, search and rule. Swift is only the platform shell, and never a relay or a mirror for data that a webview shows.
What is measured. For one chat change, the main-thread work goes from a copy of all N chats plus a deep == over N to O(changed rows). Measured as counts (rows touched, not ms) by the red job fe50d4f83638994ca7517f0d and the green job 6479f2dce62c06698cfa9745 (ChatsWindowMainThreadTests, a 3,000-chat index). No ms before/after is measured yet. When the Austin's-day harness gives a debug.hangs result for the chats sites, it goes in a follow-up comment.
Step 2 (follow-up in this series, R7): the palette chats page, Open Chat, cookie import and the New Tab cards still read the private mirror of ChatsFeed. No other object holds it, so a change applies in place. The ChatsFeed mirror is deleted in #28090.
Changelog
Changed: The sidebar's chat history gets its rows from acpmux, which sorts and filters the list; the app keeps only the rows it shows.
Testing
At head 7cca9aa2618f712f033a6192309e7c6387e896bb (merge of feat-cmux-next 98f84fb96a8 into 2e0e3430fef). The red commits are the red test b6d2aef6a3f cherry-picked onto the base (proof/28055-chats-window-red-98f84fb, proof/28055-chats-window-red-fe8bef7).
- Rust window:
cmux-tui-rust-check.sh test three_thousand_chats.- Red on the merge base 98f84fb96a8 + test (814fb359097): FAIL on its own assertion, "Method not found: _acpmux/chats_window" (job 6a3c3b9d9fbbde5ad39bfc98). Also red on fe8bef7c44cf + test (a3f5bf1a0da, job bc7f21491e46c8967d6d6bc3). Each job ran the test 2 times (one rerun alone); every attempt failed the same way.
- Green on the head: PASS (job 81e2a6b26dafec20dcf16c99).
- Swift:
package-test-lane.sh suite Packages/macOS/CmuxNext ChatsWindowMainThreadTests.- Red on fe8bef7c44cf + test (a3f5bf1a0da): failed on its own expectation, "the main thread holds 3000 chats for a window of 20" (job fe510cbde315a27d70d00c08). fe8bef7c44cf is the first parent of the merge base 98f84fb96a8; 98f84fb96a8 changes only Rust daemon files (#27801), no Swift.
- Green on the head, with
SidebarChatHistoryTests: 5/5 and 14/14 passed (job 64d8360ed1b2572602373234).
- Compile (
--package CmuxNext --app --recipe-version 2) of the head: CmuxNext, app and cmux-tui passed, no errors in the log (job 616a151e91e340b5964be06f). - Earlier heads: Rust red 1bc0568c5166c48e22cd6dd9 / green a306b4817142e8b5832222f9, R6 red 5bc005704f543058bdccb0fd / green dc205f17ef07c5da5b4d7362, Swift red fe50d4f83638994ca7517f0d / green 6479f2dce62c06698cfa9745.
- Review: SHIP at the head, https://github.com/manaflow-ai/cmux/pull/28055#issuecomment-6537642492 (3 low findings, follow-ups).
Observed / Not run
Observed: I read the logs of every job above. The two reds failed on their own assertions; the greens passed; the compile passed. The only merge conflict was the acpmux Conn struct (both new fields kept).
Not run: a GUI load with many chats (ms before/after); the Austin's-day harness will give it. R6 (chats_window_owned) was not run again at this head. Web lint on push: the webviews checks bun run check:design-tokens, bun run check:layers, vp fmt --check and vp lint failed only in webviews/src/pages/settings (from #25592). They are red on base fe8bef7c44cf, and this PR does not touch those files. The push used CMUX_WEB_LINT_SKIP_REASON with that check list and base sha.
No capture: no visible UI change (the same rows, from a different source).