Skip to content

Resolve agent notification targets from live identity at delivery time - #7946

Merged
austinywang merged 67 commits into
mainfrom
issue-7939-notification-wrong-pane
Jul 14, 2026
Merged

austinywang merged 67 commits into
mainfrom
issue-7939-notification-wrong-pane

Conversation

@austinywang

@austinywang austinywang commented Jul 12, 2026 •

Copy link
Copy Markdown
Contributor

Closes #7939

Symptom

With multiple Claude Code agents in different workspaces, a turn-complete notification (and its unread ring / status pill) intermittently lands on the wrong Claude pane in a different workspace.

Root cause

Hook → pane attribution trusted, in priority order: the persisted session record, a tty-name binding, and spawn-time CMUX_WORKSPACE_ID/CMUX_SURFACE_ID env — none verified against where the agent process actually lives at delivery time:

Fix — one authoritative live resolution at delivery time

App (Sources/AgentDeliveryTargetResolution.swift, new agent.resolve_delivery_target control method):

  • {pid} probe: the pane that owns the agent process right now, from two independent live signals — the process's controlling tty device matched against every surface's pty device (unique match only), cross-checked against the process's own CMUX_SURFACE_ID environment (a panel UUID is stable pane identity for the process lifetime; only the workspace binding can go stale) re-homed through workspaceContainingPanel. Disagreement or ambiguity refuses to answer rather than guessing.
  • {surface_id} probe: the workspace that currently hosts a known surface (re-homes moved panes).
  • The same resolver now backs every in-app delivery path: queued notifications follow their surface to its current workspace instead of being dropped on a stale claim, notify_target (sync) records under the surface's current workspace instead of misfiling/erroring, and notification click-through re-homes at click time (Notification click navigates to wrong tab when multiple tabs have Claude Code #2792 class).

CLI (CLI/CMUXCLI+ClaudeHookDeliveryTarget.swift): all Claude hook subcommands (session-start, prompt-submit, stop, notification, push-notification; PreToolUse without probes for per-tool cheapness) route through resolveClaudeHookDeliveryTarget:

  1. live pid target — beats a polluted session record and heals it via the existing upserts + active-pointer self-heal (fixes the Claude auto-resume can write a session's resume binding to the wrong workspace after restored TTY cache drift #7391 class, including the record being born wrong at SessionStart from a stale tty row);
  2. the Fix agent hooks misrouting notifications/status/summary to the focused tab #7228 legacy chain (record → caller tty → spawn env), unchanged;
  3. moved-pane re-home when the identity surface is no longer in the resolved workspace (fixes the Notifications route to the stale workspace after a pane is moved to another workspace (CMUX_WORKSPACE_ID captured at spawn, never re-resolved) #5781 class) — instead of borrowing the old workspace's focused surface.

Explicit --workspace/--surface flags bypass the probes, and an app without the new method degrades to the legacy chain exactly as before (covered by a regression test).

Related issues addressed by the shared fix

Tests (two-commit red/green)

Commit 1 adds failing tests, commit 2 the fix:

  • cmuxTests/ClaudeHookLiveDeliveryTargetTests.swift (+ harness): stop prefers the live pid target over a polluted record and heals it; stop follows a moved pane to its current workspace; SessionStart is not poisoned by a stale debug.terminals tty row; legacy routing survives an app without the resolver method.
  • cmuxTests/AgentNotificationLiveRetargetTests.swift: queued and sync deliveries retarget to the surface's current workspace (ring lands on the owning pane only); pure unique-tty-match decision table.

Also manually verified the three CLI scenarios end-to-end against the built binary with a mock control server (polluted record → live pane + healed store; moved pane → new workspace; method-less app → legacy routing).

Not cleanly testable here: the pid→tty kernel probe (proc_pidinfo e_tdev vs. live surface pty devices) needs a real agent process inside a real pane; it's covered indirectly by the pure-function decision-table test and the mocked v2 protocol tests, not by an end-to-end unit test.

Notes

  • Localization audit: no user-facing strings were added or changed (new code emits machine protocol payloads and DEBUG logs only); existing localized strings untouched.
  • Swift budgets: all new files are well under 500 lines; CLI/cmux.swift shrinks by ~30 lines; Sources/TerminalController.swift and Sources/TerminalNotificationQueue.swift are at or below their base line counts. No TSV changes.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

High Risk
Touches agent hook routing, notification delivery/clear semantics, and cross-workspace navigation—high user-visible impact if resolution or fail-closed paths regress; relay boundary changes need careful review.

Overview
Fixes wrong-workspace Claude notifications (#7939) by resolving where an agent lives now at delivery time instead of trusting stale session records, tty rows, or spawn env.

App: Adds agent.resolve_delivery_target (live PID → controlling tty vs pane pty, plus surface → current workspace). Queued/sync notification delivery, clears, and pending-queue discard follow a surface to its current workspace; relay create_for_target stays workspace-bound (retargetsToLiveSurfaceOwner: false). OS notification open/nav carries that flag so clicks can re-home when allowed.

CLI: New resolveClaudeHookDeliveryTarget unifies Claude hooks: prefer live PID probe (skipped on relay / PreToolUse), legacy chain, then surface re-home; SessionEnd cleanup and pane-scoped clear_notifications use the live pane.

Tests: Broad coverage for live routing, lifecycle cleanup, PID auth, and move/clear races.

Reviewed by Cursor Bugbot for commit 7c5e5c1. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Resolve agent notification targets from live identity at delivery time so rings, status, opens, and clears land on the right pane/workspace across macOS, iOS, and web. Adds a provenance flag so trusted notifications can re‑home while confined ones stay workspace‑scoped; fixes #7939.

  • Bug Fixes
    • App: Adds agent.resolve_delivery_target; queued/sync deliveries retarget to the surface’s current workspace. Clears use live ownership with generation barriers so pending work can’t resurrect; tab/workspace clears resolve each queued entry’s live target. macOS open routing propagates retargetsToLiveSurfaceOwner; pid_t overflow guarded; adds localized delivery‑target error messages.
    • CLI: Routes all Claude hooks through a live target resolver; skips PID probes on relay. PreToolUse keeps cheap routing but re‑homes by surface; SessionEnd cleanup and clears act on the live pane only. Explicit flags bypass; PID auth tightened; legacy path preserved when the app lacks the new method. Success ack aligned to structured {}.
    • Phone/APNs/iOS/web: Push payloads build from stored notifications and include retargetsToLiveSurfaceOwner. Confined/relay payloads omit surfaceId; iOS taps honor the flag and do not cross workspaces; web route policy parses it; macOS open fallback uses it.
    • CI/Tests: Adds a focused CI job for notification routing regressions. Broad coverage for move/clear races, provenance boundaries, PID/auth and relay gating, in‑flight cancellation, and mutation causality; timeouts raised for CI stability.

Written for commit 47b45a1. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added fail-closed, live identity–based delivery-target resolution for agent and Claude hooks, with unified CLI routing.
    • Introduced retargetsToLiveSurfaceOwner to control whether terminal notifications can retarget to the live surface owner across opening and mobile push/tap.
  • Bug Fixes
    • Improved notification/status delivery, clearing, and session cleanup accuracy after pane/workspace moves and stale-routed identities.
  • Localization
    • Added user-facing delivery-target error messages (invalid PID, not found, unavailable).
  • Tests
    • Expanded end-to-end and regression coverage for live routing, lifecycle cleanup, PID behavior, move/clear races, and retargeting.

austinywang and others added 2 commits July 12, 2026 13:00
…7939)

Two Claude agents in different workspaces must never see a turn-complete
notification, unread ring, or status pill land on the other agent's pane:

- CLI: stop must prefer the live agent-pid target over a polluted session
  record (#7391 drift) and heal the record; a moved pane's notification
  must follow the surface to its current workspace (#5781); SessionStart
  must not be poisoned by a stale debug.terminals tty row; legacy routing
  must survive an app without the resolver method.
- App: a queued or synchronously delivered notification addressed with a
  stale workspace id but a live surface id must be retargeted to the
  surface's current workspace at delivery time instead of being dropped
  (async) or misfiled (sync).

These tests fail on main; the fix lands in the follow-up commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#7939)

One authoritative hook-event -> live surface -> current pane/workspace
resolution, shared by the CLI and the app:

App: new `agent.resolve_delivery_target` control method backed by
AgentDeliveryTargetResolution.swift. A `{pid}` probe resolves the pane
that owns the agent process RIGHT NOW from two independent live signals
(the process's controlling tty matched against surface pty devices, and
the process's own stable CMUX_SURFACE_ID environment re-homed through
workspaceContainingPanel); disagreement or ambiguity refuses to guess.
A `{surface_id}` probe returns the workspace that currently hosts a
known surface. The same resolver now retargets every in-app delivery
path: queued notifications follow their surface to its current
workspace instead of being dropped on a stale workspace claim, the sync
notify path records under the surface's current workspace instead of
misfiling, and notification click-through re-homes at click time.

CLI: Claude hook routing goes through resolveClaudeHookDeliveryTarget,
which puts live process identity above every persisted or spawn-time
claim: live pid target first (beats a polluted session record - the
issue #7391 resume/tty drift class - and heals it via the existing
upserts and active-pointer self-heal), then the #7228 legacy chain
unchanged, then moved-pane re-home (issue #5781 class) when the
identity surface is no longer listed in the resolved workspace.
Explicit --workspace/--surface flags bypass the probes, per-tool
PreToolUse skips them for cheapness, and an app without the method
degrades to the legacy chain exactly as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 12, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jul 14, 2026 3:21am
cmux-staging Building Building Preview, Comment Jul 14, 2026 3:21am

@coderabbitai

coderabbitai Bot commented Jul 12, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Claude hooks now resolve live workspace and surface targets through PID and surface probes. Notification delivery, clearing, policy handling, navigation, sidebar mutations, push payloads, and persisted routing metadata preserve live-owner or source-confined behavior.

Changes

Live delivery-target routing

Layer / File(s) Summary
Agent delivery-target API
Sources/AgentDeliveryTargetResolution.swift, Sources/TerminalController.swift
Adds fail-closed PID, tty, surface, and workspace resolution through agent.resolve_delivery_target.
Unified Claude hook routing
CLI/CMUXCLI+ClaudeHookDeliveryTarget.swift, CLI/CMUXCLI+ClaudePushNotificationHook.swift, CLI/cmux.swift
Introduces ClaudeHookRoutingContext, consolidates hook resolution, and applies authoritative or legacy fallback routing across hook subcommands.
Notification retargeting and ownership
Sources/TerminalNotification*.swift, Sources/AppDelegate+Notification*.swift, Packages/macOS/CmuxNotifications/...
Preserves retargeting provenance through delivery, policy evaluation, clearing, persistence, native notifications, click routing, and push payload construction.
Workspace-owned mutations
Sources/TerminalController+ControlSidebarContext*.swift, Sources/Workspace.swift, Packages/macOS/CmuxWorkspaces/...
Resolves deferred sidebar mutations and registry data against current panel ownership and exposes the required workspace state.
Regression coverage and wiring
cmuxTests/*, tests/test_claude_hook_clear_running_status.py, .github/workflows/ci.yml, cmux.xcodeproj/project.pbxproj
Adds mock JSON-RPC and CLI harnesses, live-routing and race tests, focused CI execution, and Xcode project registrations.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

  • manaflow-ai/cmux-dev-artifacts#3830 — Directly concerns Claude session-end routing and cleanup.
  • manaflow-ai/cmux#7939 — Directly requests authoritative live Claude hook delivery-target resolution.
  • manaflow-ai/cmux-dev-artifacts#3793 — Relates to fail-closed missing-PID resolver behavior.
  • manaflow-ai/cmux-dev-artifacts#3828 — Relates to unresolved Claude delivery-target handling.

Possibly related PRs

Suggested reviewers: lawrencecchen


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (4 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Algorithmic Complexity ❌ Error FAIL: TerminalNotificationPolicyInFlightStore.discard and live-owner clear helpers rescan all tabs/panels per request/surface via workspaceContainingPanel, with no benchmark for ~1k-record paths. Use a precomputed surface→workspace index (or reuse a single live-owner snapshot) so clears/discards stay one-pass instead of resolving each target with a fresh global scan.
Cmux Full Internationalization ❌ Error Resources/Localizable.xcstrings adds 3 user-facing keys, but each only has en/ja entries while the catalog already supports 20 locales. Add translations for every existing Localizable.xcstrings locale (ar, bs, da, de, es, fr, it, km, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, zh-Hant) or shrink the catalog locale set.
Cmux Swiftui State Layout ❌ Error Workspace.swift adds new @Published state (remoteLastHeartbeatAt, listeningPorts, activeRemoteTerminalSessionCount) on an ObservableObject, which the rule flags over @Observable snapshots. Move this UI-facing state into an @Observable model or value snapshot/closure boundary; keep the legacy Workspace bridge unchanged if possible.
Cmux No Ambient Global State ❌ Error Sources/AgentDeliveryTargetResolution.swift adds module-wide free funcs at lines 35, 47, 62; they’re ambient API, not private helpers, violating no-ambient-global-state. Move those helpers onto an instance-owned resolver or make them private/fileprivate methods inside the owning AppDelegate/TerminalController extension, then inject the resolver at the seam.
Docstring Coverage ⚠️ Warning Docstring coverage is 17.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (20 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed PASS: New UI-bound APIs stay on @MainActor types; new value models/protocols are nonisolated/Sendable, and the new store accesses occur from MainActor call sites.
Cmux Swift Blocking Runtime ✅ Passed No new blocking waits, sleeps, main-sync, or manual locks were added in the touched production Swift hunks; new code uses actors/callbacks instead.
Cmux Browser Automation Off-Main ✅ Passed HEAD commit only adds notification regression tests; no browser.* routing, mainActor dispatch, or ControlCommandExecutionPolicy changes are present.
Cmux Expensive Synchronous Load ✅ Passed No new RestorableAgentSessionIndex/load-based history load was added; the new socket resolver uses cached in-memory surfaceTTYDevices plus one proc_pidinfo check, and existing CLI session-store par...
Cmux Cache Substitution Correctness ✅ Passed No persistence/snapshot path swaps a fresh read for a stale cache; surfaceTTYDevices is event-driven, and cachedCMUXScope has explicit positive/negative freshness checks.
Cmux No Hacky Sleeps ✅ Passed The PR diff here only touches Swift/resources; no TypeScript, JS, shell, or build/runtime script changes introduced sleeps/polling.
Cmux Swift Concurrency ✅ Passed The PR patch adds no new prohibited legacy async patterns in production code; remaining Dispatch/Task/Combine uses are existing or allowed callback/test boundaries.
Cmux Swift @Concurrent ✅ Passed PASS: The changed Swift code is synchronous or explicitly UI-bound; no changed nonisolated async helper lacks @concurrent, and no invalid @concurrent annotations were introduced.
Cmux Swift File And Package Boundaries ✅ Passed New production Swift files are small and focused; only existing oversized files were touched incidentally, with no >250-line growth or clear responsibility/package-boundary violations.
Cmux Swiftpm Lockfiles ✅ Passed No Package.swift/.gitignore/Package.resolved changes; pbxproj only adds source-file refs, not SwiftPM package refs, so the lockfile rule isn’t violated.
Cmux Swift Logging ✅ Passed PASS: the only added prints are CLI hook/user output, which the rule allows; the new logger is nonisolated private let, and no new app/runtime NSLog/debugPrint violations are evident.
Cmux User-Facing Error Privacy ✅ Passed New user-facing errors are generic (PID, no target, unavailable); no banned vendor/auth/payload details or raw upstream messages were added.
Cmux Architecture Rethink ✅ Passed The PR centralizes hook/notification routing behind explicit live-identity invariants and doesn’t add new timing hacks, split owners, or duplicate entrypoints.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No standalone auxiliary windows were added or changed; the only NSWindow hits are main-workspace/test fixtures, and no cmuxAuxiliaryWindowIdentifiers or cmux.* close-shortcut registration changed.
Cmux Source Artifacts ✅ Passed All 54 changed paths are source/config/tests/localization; none match artifact-only paths like logs, caches, DerivedData, or temp folders.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Production diffs only adjust routing/clear logic; no new test/debug seam members were added in changed Sources files, and the only DEBUG blocks are logging/test scaffolds.
Title check ✅ Passed The title clearly summarizes the main change: resolving agent notification targets from live identity at delivery time.
Description check ✅ Passed The description is mostly complete and includes summary, root cause, fix, testing, and related issues, though demo video, review trigger, and checklist sections are missing.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-7939-notification-wrong-pane

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread CLI/CMUXCLI+ClaudeHookDeliveryTarget.swift
@greptile-apps

greptile-apps Bot commented Jul 12, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR resolves agent notification targets from live pane identity at delivery time. The main changes are:

  • Live PID and surface-based target resolution for Claude hooks.
  • Pane-move-aware notification delivery, clearing, and navigation.
  • Workspace-confinement metadata across macOS, iOS, and web push paths.
  • Focused tests for routing, lifecycle cleanup, and move/clear races.

Confidence Score: 5/5

This looks safe to merge.

  • The latest changes address stale queue keys, workspace clears, in-flight cancellation, and moved-pane hook routing.
  • Confined notifications remain scoped throughout delivery and navigation.
  • No blocking issue was found in the changed code.

Important Files Changed

Filename Overview
CLI/CMUXCLI+ClaudeHookDeliveryTarget.swift Centralizes Claude hook routing around live PID and surface identity with legacy fallback support.
Sources/AgentDeliveryTargetResolution.swift Adds fail-closed delivery-target resolution from current process and pane ownership.
Sources/TerminalNotificationQueue.swift Adds generation-aware clear barriers and live-owner matching for queued notifications.
Sources/TerminalNotificationPolicyInFlightStore.swift Cancels in-flight work according to current pane ownership while preserving confined requests.
Sources/TerminalNotificationStore.swift Preserves retargeting provenance during notification clearing, storage, and rebinding.
Sources/AppDelegate+NotificationOpen.swift Re-homes trusted notification opens while keeping confined opens within their source workspace.
Sources/Cloud/PhonePushPayload.swift Carries live-retargeting provenance into mobile push payloads.
web/services/apns/routePolicy.ts Applies notification retargeting provenance at the web push routing boundary.

Reviews (51): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment thread Sources/TerminalNotificationQueue.swift Outdated
Comment thread CLI/cmux.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLI/CMUXCLI`+ClaudePushNotificationHook.swift:
- Around line 35-46: Update the unresolved-target acknowledgment in the Claude
push notification hook to print the exact literal “OK” instead of using
localized output. Keep the existing telemetry handling and early return
unchanged.

In `@Sources/AgentDeliveryTargetResolution.swift`:
- Around line 159-161: Update the AppDelegate.shared guard in the delivery
target resolution flow to replace the implementation-specific “AppDelegate not
available” message with product-facing recovery text indicating that delivery
target resolution is unavailable and should be retried after cmux finishes
starting, while preserving the existing error code and data.
- Around line 162-163: Update the PID handling in the delivery-target resolution
flow around v2Int and liveAgentDeliveryTarget: require an exact, safely
representable pid_t value before routing, avoiding the trapping pid_t(pid)
conversion. When a PID parameter is supplied but cannot be represented exactly
or is invalid, return invalid_params instead of allowing fallback routing;
preserve normal routing for valid positive PIDs.

In `@Sources/TerminalNotificationQueue.swift`:
- Around line 417-420: Update the target resolution in the queued notification
delivery flow around agentNotificationDeliveryTarget to fail closed when it
returns nil. Remove the fallback to the claimed tabId and surfaceId, and skip
the notification instead; preserve delivery when a live target is returned and
match the existing queued-delivery behavior.
- Line 421: Update the notification discard logic around
TerminalMutationBus.shared.discardPendingNotifications to also discard the
claimed pending key from the original pane location, in addition to target.tabId
and target.surfaceId. Ensure both the old key and the rehomed target key are
removed before delivering the synchronous notification.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d0b375e6-3794-43e8-a112-31c24f2ee423

📥 Commits

Reviewing files that changed from the base of the PR and between e634825 and 113c504.

📒 Files selected for processing (11)
  • CLI/CMUXCLI+ClaudeHookDeliveryTarget.swift
  • CLI/CMUXCLI+ClaudePushNotificationHook.swift
  • CLI/cmux.swift
  • Sources/AgentDeliveryTargetResolution.swift
  • Sources/AppDelegate+NotificationOpen.swift
  • Sources/TerminalController.swift
  • Sources/TerminalNotificationQueue.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/AgentNotificationLiveRetargetTests.swift
  • cmuxTests/ClaudeHookLiveDeliveryTargetTestSupport.swift
  • cmuxTests/ClaudeHookLiveDeliveryTargetTests.swift

Comment thread CLI/CMUXCLI+ClaudePushNotificationHook.swift
Comment thread Sources/AgentDeliveryTargetResolution.swift
Comment thread Sources/AgentDeliveryTargetResolution.swift Outdated
Comment thread Sources/TerminalNotificationQueue.swift Outdated
Comment thread Sources/TerminalNotificationQueue.swift Outdated
…pace rehome

- agent.resolve_delivery_target: convert the caller-supplied pid with
  pid_t(exactly:) so an out-of-range 64-bit value degrades to the
  surface/workspace probes instead of trapping (socket-reachable crash).
- Sync notification delivery: discard superseded pending notifications by
  their canonical identity (the surface) so an entry queued under a stale
  claimed workspace key cannot survive retargeting and duplicate/replace
  the newer notification.
- Claude hook rehome: apply the app's identity-surface ownership answer
  even when the owning workspace is unchanged — a confirmed identity
  surface outranks the focused-surface fallback in the same workspace.
- Regression tests for all three.

Review findings from codex autoreview, Cursor Bugbot, Greptile, CodeRabbit
on #7946.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…onEnd cleanup

- Skip the live agent-pid probe on relay-backed socket connections: a
  hook running on an SSH/cloud host carries a remote pid that must not be
  resolved against the Mac's local process table. UUID-based probes and
  the legacy chain still apply.
- Split allowsLiveProbe into allowsPidProbe: PreToolUse still skips the
  per-tool pid/tty scan, but the cheap {surface_id} re-home probe stays
  enabled so a mid-turn pane move cannot make PreToolUse mutate (and
  re-record via upsert) the old workspace's focused pane.
- Route SessionEnd cleanup through the live target resolver: clear
  status/pid/notifications on the workspace that owns the pane NOW, not
  the consumed record's stale workspace (which also wiped unrelated
  panes' notifications there). Fork-parent cleanup uses the shared
  resolver too.
- Harness regression tests for the SessionEnd and PreToolUse paths.

Round-2 codex autoreview findings on #7946.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread CLI/cmux.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/AgentDeliveryTargetResolution.swift (1)

27-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Mark AgentDeliveryTargetCandidate as nonisolated.

This pure value-model struct with UUID fields is returned by the nonisolated function agentDeliveryTargetMatchingTTYDevice. Per the project's Swift 6 coding guidelines, pure value-model structs should be explicitly marked nonisolated to avoid implicit @MainActor isolation.

♻️ Proposed fix
-struct AgentDeliveryTargetCandidate: Equatable {
+nonisolated struct AgentDeliveryTargetCandidate: Equatable {
     let workspaceId: UUID
     let surfaceId: UUID
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/AgentDeliveryTargetResolution.swift` around lines 27 - 30, Mark the
AgentDeliveryTargetCandidate struct as nonisolated while preserving its
Equatable conformance and UUID fields, so the value model can be returned by the
nonisolated agentDeliveryTargetMatchingTTYDevice function without implicit actor
isolation.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 24619-24643: Update the clearAgentSurfaceResumeBinding call to
pass cleanupSurfaceId instead of consumedSession.surfaceId. Preserve the
existing workspaceId selection and the matched live-authoritative fallback
behavior used by sendClaudeFeedTelemetry and the surrounding cleanup flow.

---

Outside diff comments:
In `@Sources/AgentDeliveryTargetResolution.swift`:
- Around line 27-30: Mark the AgentDeliveryTargetCandidate struct as nonisolated
while preserving its Equatable conformance and UUID fields, so the value model
can be returned by the nonisolated agentDeliveryTargetMatchingTTYDevice function
without implicit actor isolation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ec689d36-d872-4cda-b6db-0d9cf09d2f70

📥 Commits

Reviewing files that changed from the base of the PR and between fe6a106 and 965e780.

📒 Files selected for processing (4)
  • CLI/CMUXCLI+ClaudeHookDeliveryTarget.swift
  • CLI/cmux.swift
  • Sources/AgentDeliveryTargetResolution.swift
  • cmuxTests/ClaudeHookLiveDeliveryTargetTests.swift

Comment thread CLI/cmux.swift Outdated
…oints

- notification.create_for_caller: a preferred surface that moved out of
  the (stale, spawn-time) preferred workspace follows the surface to its
  current owner instead of falling back to the old workspace's focused
  pane — plain `cmux notify` from a moved pane hit the wrong pane.
- notification.create_for_target / create_for_surface: resolve the
  surface's current owner before rejecting a stale workspace claim, so
  moved-pane deliveries retarget instead of erroring (matches the v1
  notify_target guard fixed earlier; shared-behavior policy).
- Pending-notification discard for a surface now always uses the
  canonical surface identity: a surface-scoped clear that raced the queue
  drain could leave a stale-keyed entry that re-delivered (resurrected)
  the notification right after the user dismissed it.
- Regression tests for all three.

Round-3 codex autoreview findings on #7946. Note: extends the fix to two
sibling entrypoint files (TerminalNotificationCallerResolver.swift,
TerminalController+ControlNotificationContext.swift) per the repo's
shared-behavior policy — same bug class, same resolver path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread Sources/TerminalNotificationQueue.swift
…rehome

- Revert live re-homing in notification.create_for_target: it is relay-
  reachable (RemoteDaemonProxyTunnel pins workspace_id to the relay's
  owner workspace), and the app-side membership guard is what confines a
  VM to its authorized workspace — a global surface lookup would allow
  cross-workspace injection from a leaked pane UUID. Moved-pane re-homing
  for relayed notifications is deferred until a trusted surface binding
  exists. Regression test pins the boundary.
- notification.create_for_surface (local-only, not relay-reachable):
  re-home before BOTH rejects, including when the claimed routing
  workspace was closed, not just when it no longer lists the surface.
- Restore exact enqueue-key semantics for the (tabId, surfaceId) discard
  used by rebindSurfaceNotifications: a surface-wide discard could drop a
  newer notification legitimately queued under the destination key during
  a pane move. Surface-scoped CLEARS now use the canonical by-surface
  discard through a dedicated helper (net-zero growth in the hard-capped
  store file). Regression tests for both semantics.

Round-4 codex autoreview findings on #7946.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/TerminalController+ControlNotificationContext.swift (1)

41-86: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Extract shared "rehome and deliver" helper to remove duplication.

controlNotificationCreateForSurface (Lines 54-71) and controlNotificationCreateForTarget's rehomedDelivery() (Lines 103-120) each independently call AppDelegate.shared?.workspaceContainingPanel(...), then deliverNotificationSynchronously(...), then build an identical .delivered(workspaceID:surfaceID:windowID:) result. This is the same behavior wired through two separate code paths within one file.

♻️ Proposed consolidation
+    private func rehomedNotificationDelivery(
+        surfaceID: UUID,
+        excludingWorkspaceID: UUID? = nil,
+        title: String,
+        subtitle: String,
+        body: String
+    ) -> ControlNotificationTargetedDeliveryResolution? {
+        guard let owner = AppDelegate.shared?.workspaceContainingPanel(
+            panelId: surfaceID,
+            preferredWorkspaceId: excludingWorkspaceID
+        ), owner.workspace.id != excludingWorkspaceID else { return nil }
+        deliverNotificationSynchronously(
+            tabId: owner.workspace.id,
+            surfaceId: surfaceID,
+            title: title,
+            subtitle: subtitle,
+            body: body
+        )
+        return .delivered(
+            workspaceID: owner.workspace.id,
+            surfaceID: surfaceID,
+            windowID: AppDelegate.shared?.windowId(for: owner.tabManager)
+        )
+    }

Then both controlNotificationCreateForSurface's guard ws.panels[surfaceID] != nil else { ... } and controlNotificationCreateForTarget's rehomedDelivery() nested closure call this one helper instead of duplicating the lookup/deliver/build-result sequence.

As per coding guidelines, "Do not wire the same behavior separately through multiple surfaces; use one shared action path." This is a fresh duplication introduced by this diff (two near-identical new branches), not pre-existing debt.

Also applies to: 88-141

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/TerminalController`+ControlNotificationContext.swift around lines 41
- 86, Extract the shared rehome-and-deliver sequence into a helper used by
controlNotificationCreateForSurface and controlNotificationCreateForTarget’s
rehomedDelivery(). Have the helper resolve the current owner via
workspaceContainingPanel, synchronously deliver the notification, and construct
the delivered result including workspace, surface, and window identifiers.
Replace both duplicated branches with this helper while preserving their
existing fallback behavior when no owner is found.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Sources/TerminalController`+ControlNotificationContext.swift:
- Around line 41-86: Extract the shared rehome-and-deliver sequence into a
helper used by controlNotificationCreateForSurface and
controlNotificationCreateForTarget’s rehomedDelivery(). Have the helper resolve
the current owner via workspaceContainingPanel, synchronously deliver the
notification, and construct the delivered result including workspace, surface,
and window identifiers. Replace both duplicated branches with this helper while
preserving their existing fallback behavior when no owner is found.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 69babdd2-5cf3-41e2-a6c5-07ac9d28695e

📥 Commits

Reviewing files that changed from the base of the PR and between 965e780 and 428f662.

📒 Files selected for processing (4)
  • Sources/TerminalController+ControlNotificationContext.swift
  • Sources/TerminalNotificationCallerResolver.swift
  • Sources/TerminalNotificationQueue.swift
  • cmuxTests/AgentNotificationLiveRetargetTests.swift

Comment thread CLI/CMUXCLI+ClaudeHookDeliveryTarget.swift
…End scoping

- PreToolUse AskUserQuestion/ExitPlanMode: when the resolver returned an
  authoritative live target, use its surface for the upsert, lifecycle,
  and needs-input notification instead of re-preferring the persisted
  session surface — a stale/closed record surface would re-pollute the
  record and pin the blocking prompt on the wrong pane.
- SessionEnd: clear the resume binding on the live pane (and also on the
  record's surface when it differs, so a misfiled binding cannot
  survive), and scope the re-homed notification clear to the moved pane
  instead of wiping sibling panes in the destination workspace.
- Regression tests: needs-input uses the resolved surface; re-homed
  SessionEnd clear is panel-scoped.

Round-5 codex autoreview findings on #7946.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread Sources/TerminalNotificationQueue.swift Outdated
austinywang and others added 2 commits July 12, 2026 14:30
…tive

The inherited CMUX_SURFACE_ID is spawn-time evidence that can be leaked
from the operator's focused pane (documented by
testCodexHookOverridesLeakedEnvSurfaceWithProcessTTYBinding). When the
controlling-tty lookup fails, promoting an env-only answer to an
authoritative pid resolution could override a valid session record and
recreate the wrong-pane bug. The env signal now only corroborates the
unique kernel-tty match (extracted into the pure
agentDeliveryTargetCombining with unit coverage); env-only refuses and
the caller falls back to the legacy chain and re-home probes.

Round-6 codex autoreview finding on #7946.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ome priority

- The async clear_notifications --tab --panel path (enqueueClearNotifications)
  still discarded pending deliveries by their enqueue-time key, leaving a
  stale-keyed entry to retarget and resurrect the cleared notification;
  it now discards by canonical surface identity like the store clear.
  Regression test added.
- When the session record's workspace has died and the legacy chain falls
  back to the caller-tty workspace, the record surface's current owner now
  outranks that fallback: a stale tty row could otherwise mark an
  unrelated pane authoritative and skip the identity-surface re-home.

Greptile PR review findings on #7946.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread CLI/cmux.swift Outdated
…g cleaned

A record polluted to another agent's pane (#7391) whose own session is
active there made SessionEnd's shouldApplyClaudeHookVisibleMutation gate
look stale, skipping cleanup of the REAL pane — stranding its ring and
status after exit. The gate now checks the live-resolved cleanup target
(workspace + surface) instead of the consumed record's address; the two
only differ in exactly the pollution case, where live is correct.

Regression test sessionEndPollutedRecordStillClearsLivePane (foreign
active session on the polluted record surface; cleanup must land on the
live pid target). The SessionEnd/PreToolUse lifecycle tests move to a
new ClaudeHookLifecycleCleanupTests.swift (wired into cmuxTests in
project.pbxproj) to keep both suites under the 500-line file budget.

Cursor Bugbot finding 3567171933 on #7946.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread Sources/TerminalNotificationQueue.swift Outdated
…ive owner

A pending async notification queued under a stale claimed tabId DRAINS
into the live workspace that owns its surface (#7939 retargeting), so a
workspace-wide clear that matched only the enqueue key missed it and the
notification reappeared right after the clear.

Workspace-wide clears now discard by live delivery target: phase 1
snapshots the pending addresses (sequence + claimed key) under the bus
lock, phase 2 resolves each entry's delivery target on the main actor
(the same agentNotificationDeliveryTarget used at drain) and discards
exactly the snapshotted sequences — entries enqueued between the phases
are newer than the clear and deliberately survive. The v1 async
clear_notifications --tab path stays enqueue-ordered: FIFO drains the
stale entry into the live workspace BEFORE the clear barrier wipes it,
so its end state was already clean (regression-pinned).

The delivery extensions move from TerminalNotificationQueue.swift into
TerminalNotificationLiveRetargetDelivery.swift (wired into the app
target) to stay inside the file-length budget.

Cursor Bugbot finding "Tab-wide clear misses stale queue" on #7946.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 13, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

Comment thread Sources/TerminalNotificationPolicyInFlightStore.swift
…ests

All four app-host unit shards failed compiling the test file:
WorkspaceRemoteConfiguration lives in the CmuxCore package and the file
imported only CmuxControlSocket/Darwin/Foundation/Testing plus the app
module (sibling tests that use the type import CmuxCore).

Also pin the confined in-flight clear semantics Greptile asked about:
an authorized-workspace tab-wide clear cancels a confined in-flight
relay delivery even though the request carries a surfaceId
(authorizedWorkspaceClearCancelsConfinedInFlightRelayDelivery).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 13, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

app-host shard 4/4 flaked on "Clearing policy work terminates its hook
subprocess": the 2s marker wait lost to subprocess spawn + SIGTERM
propagation + marker write on a loaded CI runner (passed locally, 76/77
green). Raise the marker/file/notification wait deadlines to 15s — the
behavior under test is unchanged and a longer deadline only slows the
failure path — and drop the one explicit 2s override.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread CLI/CMUXCLI+ClaudeHookDeliveryTarget.swift Outdated
…ntics

- The "terminates its hook subprocess" regression asserted subprocess
  signal delivery, which is not reliable under the CI xctest harness
  (deterministic 15s timeout there, green locally). The test now asserts
  the guaranteed contract instead: a cleared in-flight request's result
  is discarded — the hook is marker-gated to complete strictly AFTER the
  clear, and its late result must record nothing. Termination stays
  best-effort and unasserted.
- PreToolUse surface re-home no longer skips relay-backed connections:
  pids are host-local (that restriction stays in
  liveAgentPidDeliveryTarget), but surface UUIDs are valid across the
  relay. Over the restricted cloud CLI bridge the resolver method is
  denied, which classifies as .failed and stays fail-closed exactly as
  before; relay transports that authorize the resolver now re-home.

Greptile findings "Allow relay surface re-home" and CI shard 4/4 on #7946.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +80 to +81
guard let requestSurfaceId = request.surfaceId else {
if request.tabId == tabId { idsToDiscard.append(id) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Cancel moved in-flight requests

A workspace-wide clear compares a retargetable request only with its original request.tabId. If a request starts under W1, its surface moves to W2, and W2 is cleared while policy evaluation is running, the request survives because its stored tab is still W1. Final delivery then resolves the surface to W2 and records the notification after the clear. Workspace clears must compare retargetable requests with their current surface owner while keeping confined requests scoped to their authorized workspace.

…on-wrong-pane

# Conflicts:
#	CLI/CMUXCLI+ClaudePushNotificationHook.swift
#	CLI/cmux.swift
#	cmux.xcodeproj/project.pbxproj
@austinywang
austinywang force-pushed the issue-7939-notification-wrong-pane branch from 9e5c1f4 to 4c6ceb4 Compare July 13, 2026 22:51
austinywang and others added 2 commits July 13, 2026 17:44
Main replaced the claude-hook success output "OK" with the structured
ack "{}" (printClaudeHookAck). The three hook test helpers added on
this branch still asserted stdout == "OK\n", which failed CI shard 4/4
after merging main. Expect "{}\n" to match the merged convention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on-wrong-pane

# Conflicts:
#	.github/workflows/ci.yml
@cursor

cursor Bot commented Jul 14, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@austinywang
austinywang merged commit d64f605 into main Jul 14, 2026
31 of 35 checks passed
hhsw2015 pushed a commit to hhsw2015/cmux that referenced this pull request Jul 16, 2026
manaflow-ai#7946)

* Add failing regression tests for wrong-pane notification attribution (manaflow-ai#7939)

Two Claude agents in different workspaces must never see a turn-complete
notification, unread ring, or status pill land on the other agent's pane:

- CLI: stop must prefer the live agent-pid target over a polluted session
  record (manaflow-ai#7391 drift) and heal the record; a moved pane's notification
  must follow the surface to its current workspace (manaflow-ai#5781); SessionStart
  must not be poisoned by a stale debug.terminals tty row; legacy routing
  must survive an app without the resolver method.
- App: a queued or synchronously delivered notification addressed with a
  stale workspace id but a live surface id must be retargeted to the
  surface's current workspace at delivery time instead of being dropped
  (async) or misfiled (sync).

These tests fail on main; the fix lands in the follow-up commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Resolve agent notification targets from live identity at delivery time (manaflow-ai#7939)

One authoritative hook-event -> live surface -> current pane/workspace
resolution, shared by the CLI and the app:

App: new `agent.resolve_delivery_target` control method backed by
AgentDeliveryTargetResolution.swift. A `{pid}` probe resolves the pane
that owns the agent process RIGHT NOW from two independent live signals
(the process's controlling tty matched against surface pty devices, and
the process's own stable CMUX_SURFACE_ID environment re-homed through
workspaceContainingPanel); disagreement or ambiguity refuses to guess.
A `{surface_id}` probe returns the workspace that currently hosts a
known surface. The same resolver now retargets every in-app delivery
path: queued notifications follow their surface to its current
workspace instead of being dropped on a stale workspace claim, the sync
notify path records under the surface's current workspace instead of
misfiling, and notification click-through re-homes at click time.

CLI: Claude hook routing goes through resolveClaudeHookDeliveryTarget,
which puts live process identity above every persisted or spawn-time
claim: live pid target first (beats a polluted session record - the
issue manaflow-ai#7391 resume/tty drift class - and heals it via the existing
upserts and active-pointer self-heal), then the manaflow-ai#7228 legacy chain
unchanged, then moved-pane re-home (issue manaflow-ai#5781 class) when the
identity surface is no longer listed in the resolved workspace.
Explicit --workspace/--surface flags bypass the probes, per-tool
PreToolUse skips them for cheapness, and an app without the method
degrades to the legacy chain exactly as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address review findings: pid_t overflow, stale queue keys, same-workspace rehome

- agent.resolve_delivery_target: convert the caller-supplied pid with
  pid_t(exactly:) so an out-of-range 64-bit value degrades to the
  surface/workspace probes instead of trapping (socket-reachable crash).
- Sync notification delivery: discard superseded pending notifications by
  their canonical identity (the surface) so an entry queued under a stale
  claimed workspace key cannot survive retargeting and duplicate/replace
  the newer notification.
- Claude hook rehome: apply the app's identity-surface ownership answer
  even when the owning workspace is unchanged — a confirmed identity
  surface outranks the focused-surface fallback in the same workspace.
- Regression tests for all three.

Review findings from codex autoreview, Cursor Bugbot, Greptile, CodeRabbit
on manaflow-ai#7946.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address round-2 review: relay pid namespace, PreToolUse rehome, SessionEnd cleanup

- Skip the live agent-pid probe on relay-backed socket connections: a
  hook running on an SSH/cloud host carries a remote pid that must not be
  resolved against the Mac's local process table. UUID-based probes and
  the legacy chain still apply.
- Split allowsLiveProbe into allowsPidProbe: PreToolUse still skips the
  per-tool pid/tty scan, but the cheap {surface_id} re-home probe stays
  enabled so a mid-turn pane move cannot make PreToolUse mutate (and
  re-record via upsert) the old workspace's focused pane.
- Route SessionEnd cleanup through the live target resolver: clear
  status/pid/notifications on the workspace that owns the pane NOW, not
  the consumed record's stale workspace (which also wiped unrelated
  panes' notifications there). Fork-parent cleanup uses the shared
  resolver too.
- Harness regression tests for the SessionEnd and PreToolUse paths.

Round-2 codex autoreview findings on manaflow-ai#7946.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address round-3 review: shared live retargeting for all notify entrypoints

- notification.create_for_caller: a preferred surface that moved out of
  the (stale, spawn-time) preferred workspace follows the surface to its
  current owner instead of falling back to the old workspace's focused
  pane — plain `cmux notify` from a moved pane hit the wrong pane.
- notification.create_for_target / create_for_surface: resolve the
  surface's current owner before rejecting a stale workspace claim, so
  moved-pane deliveries retarget instead of erroring (matches the v1
  notify_target guard fixed earlier; shared-behavior policy).
- Pending-notification discard for a surface now always uses the
  canonical surface identity: a surface-scoped clear that raced the queue
  drain could leave a stale-keyed entry that re-delivered (resurrected)
  the notification right after the user dismissed it.
- Regression tests for all three.

Round-3 codex autoreview findings on manaflow-ai#7946. Note: extends the fix to two
sibling entrypoint files (TerminalNotificationCallerResolver.swift,
TerminalController+ControlNotificationContext.swift) per the repo's
shared-behavior policy — same bug class, same resolver path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address round-4 review: relay auth boundary, rebind discard, surface rehome

- Revert live re-homing in notification.create_for_target: it is relay-
  reachable (RemoteDaemonProxyTunnel pins workspace_id to the relay's
  owner workspace), and the app-side membership guard is what confines a
  VM to its authorized workspace — a global surface lookup would allow
  cross-workspace injection from a leaked pane UUID. Moved-pane re-homing
  for relayed notifications is deferred until a trusted surface binding
  exists. Regression test pins the boundary.
- notification.create_for_surface (local-only, not relay-reachable):
  re-home before BOTH rejects, including when the claimed routing
  workspace was closed, not just when it no longer lists the surface.
- Restore exact enqueue-key semantics for the (tabId, surfaceId) discard
  used by rebindSurfaceNotifications: a surface-wide discard could drop a
  newer notification legitimately queued under the destination key during
  a pane move. Surface-scoped CLEARS now use the canonical by-surface
  discard through a dedicated helper (net-zero growth in the hard-capped
  store file). Regression tests for both semantics.

Round-4 codex autoreview findings on manaflow-ai#7946.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address round-5 review: authoritative surface in needs-input, SessionEnd scoping

- PreToolUse AskUserQuestion/ExitPlanMode: when the resolver returned an
  authoritative live target, use its surface for the upsert, lifecycle,
  and needs-input notification instead of re-preferring the persisted
  session surface — a stale/closed record surface would re-pollute the
  record and pin the blocking prompt on the wrong pane.
- SessionEnd: clear the resume binding on the live pane (and also on the
  record's surface when it differs, so a misfiled binding cannot
  survive), and scope the re-homed notification clear to the moved pane
  instead of wiping sibling panes in the destination workspace.
- Regression tests: needs-input uses the resolved surface; re-homed
  SessionEnd clear is panel-scoped.

Round-5 codex autoreview findings on manaflow-ai#7946.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address round-6 review: env-only pid resolution must not be authoritative

The inherited CMUX_SURFACE_ID is spawn-time evidence that can be leaked
from the operator's focused pane (documented by
testCodexHookOverridesLeakedEnvSurfaceWithProcessTTYBinding). When the
controlling-tty lookup fails, promoting an env-only answer to an
authoritative pid resolution could override a valid session record and
recreate the wrong-pane bug. The env signal now only corroborates the
unique kernel-tty match (extracted into the pure
agentDeliveryTargetCombining with unit coverage); env-only refuses and
the caller falls back to the legacy chain and re-home probes.

Round-6 codex autoreview finding on manaflow-ai#7946.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address PR review: async clear canonical identity, dead-workspace rehome priority

- The async clear_notifications --tab --panel path (enqueueClearNotifications)
  still discarded pending deliveries by their enqueue-time key, leaving a
  stale-keyed entry to retarget and resurrect the cleared notification;
  it now discards by canonical surface identity like the store clear.
  Regression test added.
- When the session record's workspace has died and the legacy chain falls
  back to the caller-tty workspace, the record surface's current owner now
  outranks that fallback: a stale tty row could otherwise mark an
  unrelated pane authoritative and skip the identity-surface re-home.

Greptile PR review findings on manaflow-ai#7946.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address Cursor review: SessionEnd staleness gate judges the pane being cleaned

A record polluted to another agent's pane (manaflow-ai#7391) whose own session is
active there made SessionEnd's shouldApplyClaudeHookVisibleMutation gate
look stale, skipping cleanup of the REAL pane — stranding its ring and
status after exit. The gate now checks the live-resolved cleanup target
(workspace + surface) instead of the consumed record's address; the two
only differ in exactly the pollution case, where live is correct.

Regression test sessionEndPollutedRecordStillClearsLivePane (foreign
active session on the polluted record surface; cleanup must land on the
live pid target). The SessionEnd/PreToolUse lifecycle tests move to a
new ClaudeHookLifecycleCleanupTests.swift (wired into cmuxTests in
project.pbxproj) to keep both suites under the 500-line file budget.

Cursor Bugbot finding 3567171933 on manaflow-ai#7946.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add regression coverage for live-owner workspace clears

* Address Cursor review: tab-wide clears resolve each pending entry's live owner

A pending async notification queued under a stale claimed tabId DRAINS
into the live workspace that owns its surface (manaflow-ai#7939 retargeting), so a
workspace-wide clear that matched only the enqueue key missed it and the
notification reappeared right after the clear.

Workspace-wide clears now discard by live delivery target: phase 1
snapshots the pending addresses (sequence + claimed key) under the bus
lock, phase 2 resolves each entry's delivery target on the main actor
(the same agentNotificationDeliveryTarget used at drain) and discards
exactly the snapshotted sequences — entries enqueued between the phases
are newer than the clear and deliberately survive. The v1 async
clear_notifications --tab path stays enqueue-ordered: FIFO drains the
stale entry into the live workspace BEFORE the clear barrier wipes it,
so its end state was already clean (regression-pinned).

The delivery extensions move from TerminalNotificationQueue.swift into
TerminalNotificationLiveRetargetDelivery.swift (wired into the app
target) to stay inside the file-length budget.

Cursor Bugbot finding "Tab-wide clear misses stale queue" on manaflow-ai#7946.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit d64f605)

This branch was successfully deployed

1 active deployment
Preview – cmux — 47b45a13 Deployed Jul 14, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Claude turn-complete notification + unread ring sometimes land on the wrong Claude pane in another workspace (multi-agent misattribution)

1 participant