Skip to content

history: closed chat rows carry their agent session and harness (C6 step 1, daemon) - #31871

Merged
cmux-bot[bot] merged 5 commits into
feat-cmux-nextfrom
rust-migration/history-merge
Nov 2, 2026
Merged

cmux-bot[bot] merged 5 commits into
feat-cmux-nextfrom
rust-migration/history-merge

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Rust migration C6, step 1 (daemon only, no FFI input). The merged history (history.entries.list) now carries a closed chat's agent identity, so the daemon's search finds a closed chat by its session id or harness. Until now, the app read closed rows in a separate 5000-row request and searched them again in Swift after it added that identity from its own closed records (HistoryService.filteredClosedDaemonEntries, enrichClosedEntries).

What changes

  • history_ops/sources.rs closed_entry: a closed tab whose tab has an agent_session sets session_id and provider (the harness) on its row. A screen, workspace or window carries no agent identity, even when the first tab it would reopen is a chat. This is the rule of Swift HistoryService.closedAgentSession.
  • cmux-history search_text: a closed row also matches its session_id and provider, as Swift HistoryEntry.searchText does for .closed.
  • The wire shape does not change: session_id and provider are already optional fields of HistoryEntry in resource-operations-v2.json. The React History page shows Copy Session ID only for agent rows, so closed chat rows get no new UI from this PR. The field docs of session_id and provider (cmux-history entry.rs and the catalog) still say "Agent only"; history: search untitled closed rows by the localized titles the app shows; delete the app's closed enrichment (C6 step 2) #31941 fixes entry.rs, and the catalog description goes with the next catalog PR in this lane.

The app half, which deletes the Swift enrichment copy, is the stacked PR on rust-migration/history-merge-app.

Tests (ported from the Swift suite ClosedAgentIdentityTests)

  • closed_chat_search_matches_session_and_harness_before_limit: port of daemonClosedSearchFiltersAfterEnrichmentAndBeforeLimit. 59 newer unrelated closes and limit: 1; a search for the session id or the harness finds the chat, an unknown text finds nothing.
  • closed_containers_carry_no_agent_identity: port of closedContainersRetainTheirOwnKind.
  • closed_tab_without_session_carries_no_session_id: a plain browser close, and a chat with no session yet (harness only).

Command (fleet): scripts/ci/cmux-tui-rust-check.sh test closed_, submitted with proof-step.py.

Review receipt: #31871 (comment) (SHIP at 69cc27d).

The app half is #31941 (stacked on this PR).

Follow-up (not in this PR): the daemon trail keeps the forward entries after a Back

Found while mapping the Swift trail rules against history_ops/trail.rs. It is a behavior gap, so it gets its own fix, not a part of this move.

  • Swift LocationTrail.record removes the entries after the cursor before it adds a new step (Chrome does the same: a new navigation drops the forward entries). The daemon has no cursor: TrailDocument::record always compares the step with the newest entry and adds it after that entry, so the forward branch stays in the daemon trail.
  • What the person sees: after Back from workspace W2 to W1 and a new step, the app shows no Forward. After a relaunch, the app adopts the daemon trail (HistoryTrailSync.adopt), and the dropped entries come back.
  • Red test plan (Rust, trail.rs): record A, B, C; record a Back to A (the step the app reports for the cursor move, which needs a field on history.trail.record, for example back_to: <entry id> or a cursor index); then record D. Expect the entries A, D. The test fails on the base, which keeps A, B, C, D. A second case: with steps: workspaces, a step in A's sidebar item after the Back refreshes A (the Swift rule compares with the cursor entry, not the newest one).
  • The fix needs a wire field on history.trail.record (a catalog change), so the app can tell the daemon where its cursor is.

Changelog

none

Observed / Not run

Observed: the red failed on the three new assertions and the greens above passed (job ids and hq runs above). The pre-push gate passed.
Not run: no Swift build or test (this PR changes no Swift). No live daemon check of history.entries.list; the tests call the row mapping and HistoryQuery::apply directly.

…for search

Ports ClosedAgentIdentityTests (daemonClosedSearchFiltersAfterEnrichmentAndBeforeLimit,
closedContainersRetainTheirOwnKind) to history_ops/sources.rs. Red on the base:
the merged history drops the closed chat's agent identity, so the daemon search
cannot find a chat by its session id or harness; the app enriches it afterwards.
The merged history (history.entries.list) now sets session_id and provider on
a closed tab row whose tab is an agent chat, and the search text of a closed
row includes them, as the Swift HistoryEntry.searchText does. A screen,
workspace or window keeps its container kind and carries no agent identity
(Swift HistoryService.closedAgentSession). This is the rule the app applied
after the read (HistoryService.enrichClosedEntries); the app half that deletes
that copy follows in its own PR.
@lawrencecchen lawrencecchen added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 146dde3: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr31871 -f ref=rust-migration/history-merge -f platform=macos -f backend_mode=local

web and size checks: green

Shipped in:

@blacksmith-sh

blacksmith-sh Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Found 12 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmpe9ke8yby/summary-fail.json (live):/summary-fail.json (live): View Logs
/tmp/tmpfp8jhkkf/summary-missing.json (live):/summary-missing.json (live): View Logs
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows/
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows
View Logs
[new_workspaces] the window draws no frames/[new_workspaces] the window draws no frames View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames View Logs
main.DeveloperIdAppTests.test_download_prints_the_active_profile_only/
test_download_prints_the_active_profile_only
View Logs
stubs: /System/Library/Frameworks/Security.framework/Versions/A/Security has no stub/
Security has no stub
View Logs
stubs: CoreFoundation.tbd: install-name /System/Library/Frameworks/
CoreFoundation.framework/Versions/A/CoreFoundation, missing 1: ['_CFRetain']/
CoreFoundation, missing 1: ['_CFRetain']
View Logs
stubs: CoreFoundation.tbd: install-name /System/Library/Frameworks/
CoreFoundation.framework/Versions/A/CoreFoundation, missing 1: ['_log2']/
CoreFoundation, missing 1: ['_log2']
View Logs

...and 2 more test failures. View all on Blacksmith

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

Clippy job a49bac0aea6ac71436590cc5 (hq run 41395440128) refused two
qualified paths that the module imports already (sources.rs:152, :174).
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (rm-history-merge-review): SHIP at 69cc27d

I reviewed only this PR's own change (history_ops/sources.rs and cmux-history/src/query.rs). These two files have not changed since 146dde3, where all four fleet jobs passed. I did not compile or run anything. The two post-merge jobs, 90c87e9136d3c5a123acdb26 (test closed_) and 45784b550246ca7bf50cb552 (clippy), were still running when I wrote this.

Correctness

  • Same rule as Swift. sources.rs:95-100 is the same rule as HistoryService.closedAgentSession (HistoryService.swift:194-197):
    • It applies only when the item kind is tab.
    • It reads the agent identity from the same shown tab, which is the first tab that is not empty, else the first tab (sources.rs:56, which is shownTab with isBlank == tab.empty).
    • A chat with session: null gets session_id None and keeps its provider.
    • A null or missing agent_session gives no identity. The JSON Null.get returns None.
  • Matches the wire shape. public_tab (closed_history_query.rs:59-79) emits agent_session {session, harness} and already turns empty strings into null.
  • Small difference, no effect on search. Swift keeps a non-nil AgentSession when both fields are null. Rust leaves both fields None. Search does not change, because Swift compactMap drops nil values. For naming, closed_kind == conversation_tab is still the signal, because public_tab sets the kind to conversation whenever agent_session is present.
  • Search matches Swift. query.rs search_text for Closed now uses url, cwd, session_id, provider, the same fields in the same order as Swift HistoryEntry.searchText .closed (HistoryEntry.swift:59).
  • Search is bounded. The change adds two short strings per closed row. apply still filters before limit, and merged.rs list already maps every closed item.
  • Wire is compatible. session_id and provider are optional fields with no kind restriction in the HistoryEntry catalog type. Older connections that lack closed-conversation-tabs-v1 already receive agent_session in closed.list, so they get no new data.

Readers

  • React History (webviews/src/pages/history/model.ts:150-156): the closed-row menu is still Reopen plus Copy URL. "Copy Session ID" appears only for agent rows, and KindIcon keys on kind. A closed row with provider set is not treated as an agent row, so no menu shows a wrong value.
  • Swift DaemonHistoryEntry.historyEntry (DaemonHistoryEntry.swift:85-96): ignores session_id and provider on closed rows, so nothing breaks. The agent path still requires kind == agent.
  • Browser-host history gate (cmux-browser-host/src/gate/history.rs:67-71): asks for kinds: ["page"] only, so closed rows do not reach page scripts.
  • CLI cmux history: passes the rows through.

Tests

Each new test has an assertion that fails on the base, and the red job 4c0e5906edb44b1489265384 confirms this.

  • closed_chat_search_matches_session_and_harness_before_limit needs both halves of the fix: the row mapping and search_text.
  • closed_containers_carry_no_agent_identity fails a fix that ignores the item kind.
  • In closed_tab_without_session_carries_no_session_id, the browser half and the container half pass on the base, but the pending-chat half does not.

Layer ownership

This change moves a search rule from Swift into the Rust owner, as L1 and L7 of layer-ownership.md require. No violation.

Non-blocking notes

  1. Stale field docs. cmux-tui/crates/cmux-history/src/entry.rs:94 and :97 still say "Agent only". The catalog descriptions of HistoryEntry.session_id ("Agent: the provider's own session id.") and provider in cmux-tui/spec/resource-operations-v2.json also still say agent only. Closed tab rows now carry both fields. Update these docs in a follow-up. The catalog edit regenerates the SDK copies.
  2. PR description overstates the React page. The description says "The React History page already reads session_id (Copy Session ID)". That is true only for agent rows (model.ts:155). A closed chat row gets no new React UI from this PR.
  3. For the app half, history: search untitled closed rows by the localized titles the app shows; delete the app's closed enrichment (C6 step 2) #31941. The Swift test daemonClosedSearchFiltersAfterEnrichmentAndBeforeLimit also expects a match for AgentPaneModel.tabTitle ("Agent") on an unnamed chat. The daemon gives an unnamed closed chat an empty title (sources.rs:78-84). The app then shows its "Terminal" fallback (DaemonHistoryEntry.swift:76). When history: search untitled closed rows by the localized titles the app shows; delete the app's closed enrichment (C6 step 2) #31941 deletes the enrichment, a search for "Agent" will stop finding unnamed closed chats unless history: search untitled closed rows by the localized titles the app shows; delete the app's closed enrichment (C6 step 2) #31941 handles that case. history: search untitled closed rows by the localized titles the app shows; delete the app's closed enrichment (C6 step 2) #31941 must also map session_id and provider into ClosedItem.agentSession (DaemonHistoryEntry.swift:95), or the brand and name are lost.
  4. Untested branch. No test covers the shown-tab choice for a tab item with more than one tab, for example a blank tab before a chat. The code reuses the same tab binding as the title, so the risk is low.

@lawrencecchen lawrencecchen added the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Nov 2, 2026
lawrencecchen added a commit that referenced this pull request Nov 2, 2026
…itle

Ports the AgentPaneModel.tabTitle case of the deleted Swift test
daemonClosedSearchFiltersAfterEnrichmentAndBeforeLimit (review of #31871,
finding 3). Red on 8318ac2: the row title is empty, so a search for
"agent" finds nothing; before this PR the app's enrichment gave the row its
localized title and searched it.
lawrencecchen added a commit that referenced this pull request Nov 2, 2026
The row leaves an untitled closed chat's title empty and the client shows its
localized default (Swift AgentPaneModel.tabTitle). cmux-history search_text
now adds that default's English text for a closed conversation_tab row with
no title, so a search for "agent" finds it, as the app's enrichment did
before this PR. Also corrects the docs of HistoryEntry.session_id and
provider: closed agent chat tabs carry them too (review of #31871, finding 1).
lawrencecchen added a commit that referenced this pull request Nov 2, 2026
…sed rows

history.entries.list takes untitled_titles {chat, terminal}: the titles the
client shows, in the person's language, for a closed row the daemon leaves
untitled. A closed agent chat (session_id or provider set) shows the chat
title, every other untitled closed row the terminal title (Swift
DaemonHistoryEntry.historyEntry and closedAgentSession). The search matches
that title; without the field no default title matches, as before, so the CLI
and older apps see no change. No word is built into Rust.

Capability history-untitled-titles-v1 (identify) says the daemon takes the
field and lists a closed chat's session id and harness: an app sends the field
only to such a daemon (cmux-tui-contract.md: wire changes behind a capability).

Catalog: the untitled_titles field and the HistoryUntitledTitles type; the
descriptions of HistoryEntry.session_id and provider now name closed chat tabs
(review of #31871, finding 1). Generated: the seven .cmux-resource-api.json
descriptors (sync-resource-api.py) and cmux-app.d.ts (gen-cmux-global.ts);
both --check clean, check-resource-api-boundary.py and check-spec-inventory.py
pass.
@cmux-bot
cmux-bot Bot merged commit 552364d into feat-cmux-next Nov 2, 2026
238 of 242 checks passed
@cmux-bot
cmux-bot Bot deleted the rust-migration/history-merge branch November 2, 2026 07:25
lawrencecchen added a commit that referenced this pull request Nov 5, 2026
…te the Swift closed-row enrichment (#36900)

* test(history): a daemon closed chat row carries its agent identity (red: the app reads it only from its own closed records)

* history: closed chat rows take their agent identity from the daemon; delete the Swift enrichment and the 5,000-row closed re-read

The daemon's history.entries.list closed rows carry session_id and provider since #31871, and
the daemon search matches them. The app now maps them in DaemonHistoryEntry.historyEntry and
lists closed rows in the same daemon read as the other kinds. HistoryService.filteredClosedDaemonEntries
and enrichClosedEntries (Swift copies of the daemon's closed-row rule) are gone.

Changed for a person: a search for the localized untitled chat name ("New Chat") no longer
finds untitled closed chats; their session id and harness still find them.

This branch was previously deployed

3 inactive deployments
ci-routing-agent — 69cc27de Deployed Nov 2, 2026 by github-actions[bot] via cmux-next macOS placement #61866
artifacts — 69cc27de Deployed Nov 2, 2026 by lawrencecchen via publish tree-addressed cmux-tui #99898
sentry-dsyms — 69cc27de Deployed Nov 2, 2026 by lawrencecchen via upload cmux-tui dSYMs to Sentry #99898
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant