cmux NEXT incognito: Feed and iPhone push skip incognito windows (#31210)
Part of #31210 (part b; the daemon-lane list-workspaces part is a separate PR).
Problem
A notification from an incognito window's tab still left the Mac: the feed handoff (FeedHandoffDriver) moved its local feed item to the person's cloud Feed, and from there it went out as an iPhone push, with its workspace, tab and agent text. Banners already redact incognito notifications (#30881) and phones already hide incognito workspaces (#30880, #31055), but Feed items and pushes weren't filtered (design/spaces.md "Incognito": nothing of the window is kept or shown outside it).
Behavior after
FeedHandoffPolicyallow-lists. An item that names a workspace or terminal (context.workspace, which is aws_ā¦id or a durable key, orcontext.terminal, aterm_ā¦id) hands off only when that workspace is in a normal window now. An incognito window's item creates no Feed item and sends no push. Normal workspaces hand off as before. An item with no context (cmux notify --workspacewith no tab) cannot be classified, so it stays local. This loses nothing: the daemon already posts such a notice read, and only unread items hand off.NotificationCenterService.feedHandoffPolicy()builds the normal set from the daemon tree, leaving out every workspace for whichWindowManager.isIncognito(workspace:)is true. That is the same predicate the banners use, and it covers listed, claimed, discarding and daemon-flagged ephemeral workspaces.- An item whose workspace is not classified yet waits. This covers a workspace that isn't in the tree yet, one that is gone (an incognito window closed while its item was handing off), and an item a crashed run left handing off. If such an item is already frozen for handoff, it is withdrawn and never adopted. The driver sends
feed.adopt.cancel; oncancelled: truethe daemon takes the item back (feed-local-handoff-abort, wired here asFeedLocalHandoffAbortRequest) and it stays local. Oncancelled: falsean earlier adopt already committed, so the driver recordsfeed-local-handoff-done. (Reviews of e0b8cb7, e1f487e and 91ba543.) - No pass runs until incognito workspaces are classified (
classified): the windows have restored or given up, which discards crash-ledger leftovers and waits for ephemeral flags, and the daemon's ephemeral flags are known. Before that, a crashed run's incognito workspace could read as normal, and a frozen item could leave with empty text before its normal workspace loaded. The end of the restore starts a pass, and so does the daemon's ephemeral flags becoming known. (Review of 3d672cb.) FeedHandoffPolicy(pure) moves fromCmuxNextApptoCmuxNextAgentPane, next to theFeedSecretScrubberit uses. This keeps the arch ratchet'sapp_linesrow from rising; the push lowers it.
Trade-off: an item from a normal workspace that closed before the next handoff pass now stays local instead of reaching the phone. Follow-up #31267: run a pass when the workspace tree loads, so a pass that ran too early doesn't hold normal items until the next arrival.
Tests
- Commit 1 (red, fails on the fleet as expected):
IncognitoFeedHandoffTestschecks that an incognito window's item, matched by workspace or by terminal, gets no handoff content and empty frozen text, and that a normal workspace's item still hands off. - The later commits add three cases: the incognito item stays local after its window closes; an unclassified workspace's item waits and a frozen one gets no adopt text; a frozen incognito item is withdrawn (
feed.adopt.cancel, then the daemon abort, with nofeed.adoptsent); an item with no context stays local. - Fleet runs (
cmux-ci run ... package-test-lane.sh suite):IncognitoFeedHandoffTestspasses at the head. InIncognitoSurfacesTests,incognitoRunsAreNeverSavedfails the same way at base 68e3df8 (SandboxRunFollower), so it's a base red, not this PR's.
Localization
No new or changed user-facing strings.
Not covered
The agent-permission feed bridge (AcpmuxPermissionFeedBridge, off by default behind feed.agentPermissionPrompts) posts by acpmux session, not by workspace. It is left for a follow-up.
Agent session: agent session
š¤ Generated with Claude Code