Chats: acpmux owns the chat list; the sidebar history holds only its window (P0 stall)

LA
6d ago
+1728-108

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, because SidebarChatHistoryFeed.chats held the same copy-on-write storage (ChatsFeed+Start.swift:29).
  • The SidebarChatHistoryFeed.chats setter (SidebarChatHistoryFeed.swift:22) ran the derived AcpmuxChatsStore.== 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_changed sends 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_projects groups 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: AcpmuxChatWindows keeps 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/lagged asks for the window again, once. The sidebar chat history (Earlier and Pinned) reads two windows. Its whole-index setter is deleted. ProjectsImport relays acpmux's grouped projects and takes no mirror snapshot.
  • plans/cmux-next/layer-ownership.md rules 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).

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).