cmux NEXT incognito: Feed and iPhone push skip incognito windows (#31210)

Merged
TE
5d ago
+275-68

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

  • FeedHandoffPolicy allow-lists. An item that names a workspace or terminal (context.workspace, which is a ws_… id or a durable key, or context.terminal, a term_… 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 --workspace with 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 which WindowManager.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; on cancelled: true the daemon takes the item back (feed-local-handoff-abort, wired here as FeedLocalHandoffAbortRequest) and it stays local. On cancelled: false an earlier adopt already committed, so the driver records feed-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 from CmuxNextApp to CmuxNextAgentPane, next to the FeedSecretScrubber it uses. This keeps the arch ratchet's app_lines row 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): IncognitoFeedHandoffTests checks 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 no feed.adopt sent); an item with no context stays local.
  • Fleet runs (cmux-ci run ... package-test-lane.sh suite): IncognitoFeedHandoffTests passes at the head. In IncognitoSurfacesTests, incognitoRunsAreNeverSaved fails 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