Repository navigation
cmux NEXT: delete an incognito workspace's agent chats when it closes - #30828
Conversation
Adds _acpmux/workspace_purge (registered, documented, not implemented yet) and the tests that prove it: acpmux purges every chat it recorded for the closed incognito workspace and keeps every other one, only the local socket may ask, and a transcript older than its session (an adopted chat) is never deleted. The app side's IncognitoChats.key names an incognito workspace's key only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-afa4d1219370df72d7ba
acpmux's _acpmux/workspace_purge stops and purges every session it recorded for the workspace (sub-sessions, forks, side sessions and agents started from its terminals included) and deletes the Claude Code / Codex transcripts those sessions wrote, so the chat leaves Earlier, the palette and New Tab. A transcript born before its session (an adopted chat) is kept. The app sends it from WorkspaceClose.willClose for incognito workspaces (window close, Delete Space, crash-ledger discard at launch, quit) and the quit waits for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-afa4d1219370df72d7ba
|
Owned by an agent session · session |
Deleting an incognito space at quit closes its workspaces without WorkspaceClose.willClose, so the quit now sends each incognito window's workspace purge first. The purge also covers a Codex chat's retired native ids. (router-review BLOCK at f2f162c) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-afa4d1219370df72d7ba
There was a problem hiding this comment.
6 issues found across 13 files
You’re at about 92% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="cmux-tui/crates/acpmux/src/server/chats/workspace_purge.rs">
<violation number="1" location="cmux-tui/crates/acpmux/src/server/chats/workspace_purge.rs:51">
P2: Concurrent workspace-purge requests can both pass this identity check and call `Hub::kill` for the same session. Add a per-workspace single-flight or atomic purge claim before killing to prevent duplicate lifecycle events and deletion work.
(Based on your team's feedback about coalescing concurrent duplicate operations.) [0c888f46-01ae-4ecd-a446-f57b9f48a612]</violation>
<violation number="2" location="cmux-tui/crates/acpmux/src/server/chats/workspace_purge.rs:56">
P2: This branch silently abandons a failed purge, leaving the session or transcript behind while the RPC still succeeds. Return a retryable error or retain failed purges for retry instead of continuing after `hub.kill` fails.</violation>
<violation number="3" location="cmux-tui/crates/acpmux/src/server/chats/workspace_purge.rs:62">
P1: A transcript deletion failure is silently reported as a successful workspace purge, leaving the incognito chat in the device history. Propagate a retryable deletion error or retain failed paths for a later purge instead of counting only successful removals.</violation>
<violation number="4" location="cmux-tui/crates/acpmux/src/server/chats/workspace_purge.rs:87">
P1: Custom Claude Code profiles use their explicit family name, so this match skips their transcripts even though they live in Claude’s store. Use the profile/store family instead of the literal session family, or incognito chats using profiles such as `claude-sr2` remain in normal chat history.</violation>
</file>
<file name="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Quit/QuitCoordinator.swift">
<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Quit/QuitCoordinator.swift:192">
P2: This schedules two concurrent `_acpmux/workspace_purge` requests for every incognito workspace during quit. Deduplicate by workspace key or suppress the normal close hook while termination is performing this explicit purge, avoiding duplicate session-stop work and quit latency.</violation>
</file>
<file name="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Windows/IncognitoChats.swift">
<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Windows/IncognitoChats.swift:39">
P2: Quit only purges registered incognito windows, so a workspace still in `pendingClaims` can be omitted during the creation/quit race. Include pending incognito workspace claims in the quit purge and close set.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
| fn own_transcripts(hub: &Hub, meta: &SessionMeta) -> Vec<PathBuf> { | ||
| let (Some(index), None) = (hub.chat_index(), &meta.watching) else { return Vec::new() }; | ||
| let harness = match meta.family.as_deref() { | ||
| Some("claude") => AdapterKind::ClaudeCode, |
There was a problem hiding this comment.
P1: Custom Claude Code profiles use their explicit family name, so this match skips their transcripts even though they live in Claude’s store. Use the profile/store family instead of the literal session family, or incognito chats using profiles such as claude-sr2 remain in normal chat history.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/server/chats/workspace_purge.rs, line 87:
<comment>Custom Claude Code profiles use their explicit family name, so this match skips their transcripts even though they live in Claude’s store. Use the profile/store family instead of the literal session family, or incognito chats using profiles such as `claude-sr2` remain in normal chat history.</comment>
<file context>
@@ -0,0 +1,133 @@
+fn own_transcripts(hub: &Hub, meta: &SessionMeta) -> Vec<PathBuf> {
+ let (Some(index), None) = (hub.chat_index(), &meta.watching) else { return Vec::new() };
+ let harness = match meta.family.as_deref() {
+ Some("claude") => AdapterKind::ClaudeCode,
+ Some("codex") => AdapterKind::Codex,
+ _ => return Vec::new(),
</file context>
| } | ||
| purged.push(session.id.clone()); | ||
| // After the stop, so the harness writes no more of it. | ||
| transcripts += files.iter().filter(|f| remove(f)).count(); |
There was a problem hiding this comment.
P1: A transcript deletion failure is silently reported as a successful workspace purge, leaving the incognito chat in the device history. Propagate a retryable deletion error or retain failed paths for a later purge instead of counting only successful removals.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/server/chats/workspace_purge.rs, line 62:
<comment>A transcript deletion failure is silently reported as a successful workspace purge, leaving the incognito chat in the device history. Propagate a retryable deletion error or retain failed paths for a later purge instead of counting only successful removals.</comment>
<file context>
@@ -0,0 +1,133 @@
+ }
+ purged.push(session.id.clone());
+ // After the stop, so the harness writes no more of it.
+ transcripts += files.iter().filter(|f| remove(f)).count();
+ if let Some(index) = hub.chat_index().filter(|_| !files.is_empty()) {
+ index.paths_changed(&files, false);
</file context>
| } | ||
| // Read before the stop: the purge forgets the session. | ||
| let files = own_transcripts(hub, &session.meta()); | ||
| if let Err(e) = hub.kill(&session, true).await { |
There was a problem hiding this comment.
P2: This branch silently abandons a failed purge, leaving the session or transcript behind while the RPC still succeeds. Return a retryable error or retain failed purges for retry instead of continuing after hub.kill fails.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/server/chats/workspace_purge.rs, line 56:
<comment>This branch silently abandons a failed purge, leaving the session or transcript behind while the RPC still succeeds. Return a retryable error or retain failed purges for retry instead of continuing after `hub.kill` fails.</comment>
<file context>
@@ -0,0 +1,133 @@
+ }
+ // Read before the stop: the purge forgets the session.
+ let files = own_transcripts(hub, &session.meta());
+ if let Err(e) = hub.kill(&session, true).await {
+ tracing::warn!(session = %session.id, "workspace purge failed: {}", e.message);
+ continue;
</file context>
| await services.cache.browserTabs.flushRecords() | ||
| // Incognito windows' chats are deleted before their spaces go (prepareForTermination) and | ||
| // before acpmux's agents end (IncognitoChats). | ||
| IncognitoChats.closingForQuit(services) |
There was a problem hiding this comment.
P2: This schedules two concurrent _acpmux/workspace_purge requests for every incognito workspace during quit. Deduplicate by workspace key or suppress the normal close hook while termination is performing this explicit purge, avoiding duplicate session-stop work and quit latency.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextApp/Quit/QuitCoordinator.swift, line 192:
<comment>This schedules two concurrent `_acpmux/workspace_purge` requests for every incognito workspace during quit. Deduplicate by workspace key or suppress the normal close hook while termination is performing this explicit purge, avoiding duplicate session-stop work and quit latency.</comment>
<file context>
@@ -187,7 +187,11 @@ final class QuitCoordinator {
await services.cache.browserTabs.flushRecords()
+ // Incognito windows' chats are deleted before their spaces go (prepareForTermination) and
+ // before acpmux's agents end (IncognitoChats).
+ IncognitoChats.closingForQuit(services)
await services.windows.prepareForTermination()
+ await IncognitoChats.drain()
</file context>
| let mut transcripts = 0usize; | ||
| for session in of_workspace(hub, &workspace) { | ||
| // A parent's purge may already have taken this one. | ||
| if !hub.resolve(&session.id).is_ok_and(|s| Arc::ptr_eq(&s, &session)) { |
There was a problem hiding this comment.
P2: Concurrent workspace-purge requests can both pass this identity check and call Hub::kill for the same session. Add a per-workspace single-flight or atomic purge claim before killing to prevent duplicate lifecycle events and deletion work.
(Based on your team's feedback about coalescing concurrent duplicate operations.) [0c888f46-01ae-4ecd-a446-f57b9f48a612]
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/server/chats/workspace_purge.rs, line 51:
<comment>Concurrent workspace-purge requests can both pass this identity check and call `Hub::kill` for the same session. Add a per-workspace single-flight or atomic purge claim before killing to prevent duplicate lifecycle events and deletion work.
(Based on your team's feedback about coalescing concurrent duplicate operations.) [0c888f46-01ae-4ecd-a446-f57b9f48a612]</comment>
<file context>
@@ -0,0 +1,133 @@
+ let mut transcripts = 0usize;
+ for session in of_workspace(hub, &workspace) {
+ // A parent's purge may already have taken this one.
+ if !hub.resolve(&session.id).is_ok_and(|s| Arc::ptr_eq(&s, &session)) {
+ continue;
+ }
</file context>
| static func closingForQuit(_ services: AppServices) { | ||
| let windows = services.windows | ||
| let registry = windows.registry.value | ||
| for window in registry.windows where registry.isIncognito(window.id) { |
There was a problem hiding this comment.
P2: Quit only purges registered incognito windows, so a workspace still in pendingClaims can be omitted during the creation/quit race. Include pending incognito workspace claims in the quit purge and close set.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextApp/Windows/IncognitoChats.swift, line 39:
<comment>Quit only purges registered incognito windows, so a workspace still in `pendingClaims` can be omitted during the creation/quit race. Include pending incognito workspace claims in the quit purge and close set.</comment>
<file context>
@@ -0,0 +1,58 @@
+ static func closingForQuit(_ services: AppServices) {
+ let windows = services.windows
+ let registry = windows.registry.value
+ for window in registry.windows where registry.isIncognito(window.id) {
+ for id in window.workspaceIDs {
+ guard let (workspace, _) = services.machines.workspace(id: id) else { continue }
</file context>
|
Review (codex gpt-6.1-sol xhigh, router-review): SHIP at 572f459 An earlier BLOCK at f2f162c (quit deleted incognito spaces before any purge was sent) is fixed in 572f459. Follow-ups:
Checks at this head: |
…phemeral-chats # Conflicts: # Packages/macOS/CmuxNext/Sources/CmuxNextApp/Quit/QuitCoordinator.swift
Bugbot needs on-demand usage enabledBugbot uses usage-based billing for this team and requires on-demand usage to be enabled. A team admin can enable on-demand usage in the Cursor dashboard. |
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Gallery before/after for
States
Pick other states with a web and size checks: green Shipped in:
|
|
Found 6 test failures on Blacksmith runners: Failures
|
…orkspace A Keep Sessions quit skips endLocalAgents, so the windows step now waits for the incognito chats' purges (each within 3 s, inside a Keep quit's 4 s deadline). The acpmux purge scans the workspace again after each pass, so a fork, sub-session or terminal agent started during the close goes too. (router-review BLOCK at 05739a8) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-afa4d1219370df72d7ba
A DEV build attached to another app's daemon (CMUX_NEXT_ACPMUX_ATTACH) does not own its chats, as the quit already treats it. (router-review BLOCK at eaeea6d; closes #30917) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-afa4d1219370df72d7ba
There was a problem hiding this comment.
4 issues found across 3 files (changes from recent commits).
You’re at about 95% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Quit/QuitCoordinator.swift">
<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Quit/QuitCoordinator.swift:213">
P1: This drain happens after incognito workspaces and their terminals have already been closed, so a terminal agent can disappear before acpmux scans the workspace and its transcript can survive. Await the purge immediately after `closingForQuit` and before `prepareForTermination` (and separately drain any close-hook purges if needed).</violation>
</file>
<file name="cmux-tui/crates/acpmux/src/server/chats/workspace_purge.rs">
<violation number="1" location="cmux-tui/crates/acpmux/src/server/chats/workspace_purge.rs:34">
P2: `MAX_PASSES` can leave chats behind when new workspace descendants appear across more than four passes. Rescan until no untried workspace sessions remain, using an explicit deadline if a bound is required, because the purge promises to remove every workspace chat.</violation>
<violation number="2" location="cmux-tui/crates/acpmux/src/server/chats/workspace_purge.rs:63">
P2: `tried` suppresses a replacement chat that reuses an ID after the original is purged. Track session instances, such as `Arc::ptr_eq`, rather than only IDs so a fixed-ID replacement is processed.</violation>
</file>
<file name="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Windows/IncognitoChats.swift">
<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Windows/IncognitoChats.swift:28">
P2: The 3-second deadline also applies to ordinary incognito workspace closes, and a timeout only gets logged; a busy or large purge can therefore leave the incognito chats and transcripts behind. Apply the short budget only during quit, while allowing normal closes to use the longer purge deadline or retry until the purge is acknowledged.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
| // the step waits for them: every quit, Keep included, waits for a step past its limit. | ||
| IncognitoChats.closingForQuit(services) | ||
| await services.windows.prepareForTermination() | ||
| await IncognitoChats.drain() |
There was a problem hiding this comment.
P1: This drain happens after incognito workspaces and their terminals have already been closed, so a terminal agent can disappear before acpmux scans the workspace and its transcript can survive. Await the purge immediately after closingForQuit and before prepareForTermination (and separately drain any close-hook purges if needed).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextApp/Quit/QuitCoordinator.swift, line 213:
<comment>This drain happens after incognito workspaces and their terminals have already been closed, so a terminal agent can disappear before acpmux scans the workspace and its transcript can survive. Await the purge immediately after `closingForQuit` and before `prepareForTermination` (and separately drain any close-hook purges if needed).</comment>
<file context>
@@ -206,10 +206,11 @@ final class QuitCoordinator {
+ // the step waits for them: every quit, Keep included, waits for a step past its limit.
IncognitoChats.closingForQuit(services)
await services.windows.prepareForTermination()
+ await IncognitoChats.drain()
},
stopSavingWindows: { services.windows.stopSavingForTermination() },
</file context>
| break; | ||
| } | ||
| for session in left { | ||
| tried.insert(session.id.clone()); |
There was a problem hiding this comment.
P2: tried suppresses a replacement chat that reuses an ID after the original is purged. Track session instances, such as Arc::ptr_eq, rather than only IDs so a fixed-ID replacement is processed.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/server/chats/workspace_purge.rs, line 63:
<comment>`tried` suppresses a replacement chat that reuses an ID after the original is purged. Track session instances, such as `Arc::ptr_eq`, rather than only IDs so a fixed-ID replacement is processed.</comment>
<file context>
@@ -46,22 +49,34 @@ pub(super) async fn handle(
- if let Some(index) = hub.chat_index().filter(|_| !files.is_empty()) {
- index.paths_changed(&files, false);
+ for session in left {
+ tried.insert(session.id.clone());
+ // A parent's purge may already have taken this one.
+ if !hub.resolve(&session.id).is_ok_and(|s| Arc::ptr_eq(&s, &session)) {
</file context>
|
|
||
| pub(crate) const METHOD: &str = crate::rpc::method::MUX_WORKSPACE_PURGE; | ||
| /// Scans of the workspace's sessions one purge makes at most. | ||
| const MAX_PASSES: usize = 4; |
There was a problem hiding this comment.
P2: MAX_PASSES can leave chats behind when new workspace descendants appear across more than four passes. Rescan until no untried workspace sessions remain, using an explicit deadline if a bound is required, because the purge promises to remove every workspace chat.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/server/chats/workspace_purge.rs, line 34:
<comment>`MAX_PASSES` can leave chats behind when new workspace descendants appear across more than four passes. Rescan until no untried workspace sessions remain, using an explicit deadline if a bound is required, because the purge promises to remove every workspace chat.</comment>
<file context>
@@ -29,6 +30,8 @@ use crate::rpc::RpcError;
pub(crate) const METHOD: &str = crate::rpc::method::MUX_WORKSPACE_PURGE;
+/// Scans of the workspace's sessions one purge makes at most.
+const MAX_PASSES: usize = 4;
pub(super) async fn handle(
</file context>
| defer { pending[id] = nil } | ||
| do { | ||
| // Within a Keep quit's 4 s deadline (QuitSteps). | ||
| try await acpmux.purgeWorkspace(key: key, deadline: .seconds(3)) |
There was a problem hiding this comment.
P2: The 3-second deadline also applies to ordinary incognito workspace closes, and a timeout only gets logged; a busy or large purge can therefore leave the incognito chats and transcripts behind. Apply the short budget only during quit, while allowing normal closes to use the longer purge deadline or retry until the purge is acknowledged.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextApp/Windows/IncognitoChats.swift, line 28:
<comment>The 3-second deadline also applies to ordinary incognito workspace closes, and a timeout only gets logged; a busy or large purge can therefore leave the incognito chats and transcripts behind. Apply the short budget only during quit, while allowing normal closes to use the longer purge deadline or retry until the purge is acknowledged.</comment>
<file context>
@@ -17,13 +17,15 @@ enum IncognitoChats {
do {
- try await acpmux.purgeWorkspace(key: key)
+ // Within a Keep quit's 4 s deadline (QuitSteps).
+ try await acpmux.purgeWorkspace(key: key, deadline: .seconds(3))
} catch {
Logger(subsystem: "com.cmuxterm.app.next", category: "incognito")
</file context>
|
Reviews at the heads after the base merge:
|
Bugbot needs on-demand usage enabledBugbot uses usage-based billing for this team and requires on-demand usage to be enabled. A team admin can enable on-demand usage in the Cursor dashboard. |
|
Review (codex gpt-6-astra high, router-review): BLOCK at 0ad482a BLOCK at 0ad482a Visual verdict: no images; Leo tries the build
Follow-ups:
|
|
Spaces lead (owner's coordinator), on the gpt-6-astra BLOCK at 0ad482a, finding 1 (" Proof at this exact head: fleet run https://github.com/manaflow-ai/cmux/actions/runs/41316833674 (source 0ad482a) compiled Finding 2 (a session made after the final scan) is the reviewer's own [follow-up] tag and is tracked in #30919. The red |
… are the app's instance (#30828) #30828 added 'private static var pending' to the IncognitoChats enum, which raises the arch ratchet's global_mutable_statics row (+1, CmuxNextApp). IncognitoChats is now a final class that AppServices owns (services.incognitoChats); WorkspaceClose.willClose and the quit's windows and endLocalAgents steps call that one instance. The pure key(of:key:incognito:) stays static. No behavior change: one app, one set of pending purges, drained at quit as before.
…l, RoomMembership, IncognitoSpaceMarks out of CmuxNextApp (#29184 #30921 #30666 #30881 #30828) (#31036) * base red: arch app_lines, move five App files into their packages The feat-cmux-next tip is red on the arch ratchet's app_lines row: +295 CmuxNextApp lines over the last green arch tip 73dc0d8. This moves five self-contained App files into the package that owns their domain, with no behavior change (public API, the App calls it): - CloudMoveAlwaysGrants (#29184) and CloudMoveCLI -> CmuxNextCloud/Move, beside CloudMoveFolder and CloudMoveRetries. CmuxNextCloud now depends on CmuxNextDaemon for ProcessRunner (both service layer). - VideoControl (#30921) -> CmuxNextAgentPane, beside AgentVideoFrames. A struct with an instance methods() (namespace convention); the App registers VideoControl().methods(). CmuxNextAgentPane now depends on CmuxNextControl (ui -> service). - RoomMembership (#30666 added isHidden) -> CmuxNextDaemon/Store, beside PersonalStore whose pins it reads. A pure value type. - IncognitoSpaceMarks (#30881) -> CmuxNextBridge, beside SidebarMapping+Spaces that takes its hidden set. Still MainActor (Bridge has the UI default isolation). Tests that name the moved types import their new module. * base red: arch global_mutable_statics, IncognitoChats' pending purges are the app's instance (#30828) #30828 added 'private static var pending' to the IncognitoChats enum, which raises the arch ratchet's global_mutable_statics row (+1, CmuxNextApp). IncognitoChats is now a final class that AppServices owns (services.incognitoChats); WorkspaceClose.willClose and the quit's windows and endLocalAgents steps call that one instance. The pure key(of:key:incognito:) stays static. No behavior change: one app, one set of pending purges, drained at quit as before.
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Leo asked for incognito spaces (2026-10-31): "like incognito spaces, each new window is a space". The Spaces spec (
design/spaces.md, "Incognito") says an incognito space keeps no history, is never restored, and is deleted when its window closes.Problem
Agent chats started in an incognito workspace were ordinary saved acpmux chats. Closing the window ended the terminals (
WorkspaceClose.close,endTerminals: true) but not the chats. Their acpmux logs and titles stayed, the harness's own transcripts (~/.claude/projects,~/.codex) stayed, and the chats showed under Earlier in every normal window, in the palette and on New Tab. This is leak 1 of the incognito persistence audit.Change
_acpmux/workspace_purge {workspaceId}(server/chats/workspace_purge.rs), served on the local unix socket only. It stops and purges (Hub::killwith purge) every session acpmux recorded for that workspace when the session started (_meta.acpmux.workspaceId, which the app already stamps on every localsession/new, orCMUX_WORKSPACE_ID). That covers the app's chats plus their sub-sessions, forks and side sessions (each keeps its parent's workspace), and agents started from the workspace's terminals. For Claude Code and Codex sessions it also deletes the transcript the session wrote: the chat-index file named for each agent session id it recorded, plus Claude Code's sibling folder of that name. Once those are gone the chat leaves the device chat index. A transcript born before its session is kept, because that means an adopted chat that existed before. Watched chats are kept, and so is any file not named*<id>*.jsonl.IncognitoChats.closinghangs off the existingWorkspaceClose.willClosehook, which every close path already calls: incognito window close (discard), Delete Space, the crash-ledger discard at launch, andcloseIncognitoWindowsForTerminationat quit. For an incognito workspace it sends the purge throughAcpmuxEnvironment.purgeWorkspace(key:). The quit runsIncognitoChats.drain()afterwindows.prepareForTermination(), so the purges reach acpmux before its agents end.rpc.rs,protocol/mod.rs,spec/acpmux-protocol.json, the generated webmethods.tsanddocs/acpmux-schema.json.Why not
_meta.acpmux.ephemeral(what the audit suggested)I checked it:
server/ephemeral.rsties an ephemeral session to the connection that created it and purges it when that connection closes. The agent pane closes and reopens its acpmux WebSocket during normal use: replay backpressure (AgentPaneView.deliver), theme push failure, page teardown and remount. So an ephemeral incognito chat would be purged while its window is still open, which is user data loss._acpmux/kill {purge: true}works per session (requests.rs), but it would miss sub-sessions, forks and terminal agents. A purge scoped to the workspace covers all of them, and the logic stays in Rust.Earlier, the palette and New Tab: while the window is open, Earlier already skips chats an open workspace shows (
SidebarChatHistoryFeed.liveChats). After the close, the sessions and their transcripts are gone, so nothing lists them.Tests (two commits: red, then green)
workspace_purge_tests.rs:purges_only_the_workspaces_chats(two incognito chats purged, a chat in another workspace and one with no workspace kept, the uppercase key is accepted, a second purge finds nothing),only_the_local_socket_may_purge(LocalApp, Web and Peer get "Method not found"; a malformed id is refused), anda_transcript_older_than_its_session_is_kept(the birth-time rule and the file-name guard).IncognitoChatsTests.swift: only an incognito workspace's key is purged.cargo test -p acpmux --lib -- workspace_purge protocol_plus clippy-D warnings): 9 passed, clippy clean (run).Follow-ups (not in this PR)
session_index.jsonlthread name may also persist.Localization: no user-facing strings added.
Agent session: agent session
🤖 Generated with Claude Code