Notify on codex PermissionRequest: new nativeApprovalPrompt feed semantic raises the agentPermissionPrompt alert - #9804
Conversation
…mission-prompt notification FeedEventClassifier.classify now returns a FeedEventClassification struct carrying a notifiesNativeApprovalPrompt flag alongside the wire event name and actionability. The flag is false for every current semantic, so runtime behavior is unchanged in this commit; the new test asserting that codex PermissionRequest events set it is expected to FAIL, demonstrating #9592 (the event is normalized to non-actionable PreToolUse telemetry at ingest and no agentPermissionPrompt notification is ever raised). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…emantic
Codex blocks in its own approval reviewer when its PermissionRequest hook
fires, and that hook is wired only to the feed bridge — which deliberately
normalizes it to non-actionable PreToolUse telemetry so cmux Feed never
competes with Codex's native prompt ("Approve for me" depends on this). That
normalization also silently dropped the only signal Codex emits while
blocked, so notifications.agentPermissionPrompt never fired for codex seats.
Separate the two concerns in the classifier registry: a new
.nativeApprovalPrompt semantic keeps the exact telemetry wire behavior
(PreToolUse, non-actionable, no blocking wait) but marks the classification
notifiesNativeApprovalPrompt. Codex's PermissionRequest/permission_request
register with it; any future native-approval agent opts in with one registry
line. On that flag, the feed hook sends a fire-and-forget notify_target_async
built through the shared AgentHookNotificationClassifier, so the alert
carries the same "Permission"/"Approval needed" strings and the
c=needs-permission meta the generic notification hook and Claude's
permission_prompt path use — gated app-side by the existing
"Agent Needs Permission" setting. No new user-facing strings.
Verified against a mock socket: `cmux hooks feed --source codex --event
PermissionRequest` previously emitted only the feed.push frame (release
0.64.22); it now also emits
`notify_target_async <ws> <sf> Codex|Permission|<command>|c=needs-permission;p=0`,
while codex PreToolUse still emits no notification.
Fixes #9592
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughFeed event classification now returns structured approval state. Codex permission requests trigger native approval notifications without creating actionable Feed events. Codex tool lifecycle events clear those notifications through an acknowledged feed transport. ChangesNative approval prompt handling
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant Codex
participant FeedHook
participant FeedEventClassifier
participant FeedAttentionTransport
participant FeedTelemetryTransport
Codex->>FeedHook: PermissionRequest
FeedHook->>FeedEventClassifier: classify event
FeedEventClassifier-->>FeedHook: non-actionable approval notification
FeedHook->>FeedAttentionTransport: send notification and await acknowledgment
FeedHook->>FeedTelemetryTransport: send telemetry
Codex->>FeedHook: PostToolUse
FeedHook->>FeedEventClassifier: classify lifecycle event
FeedEventClassifier-->>FeedHook: clear prompt
FeedHook->>FeedAttentionTransport: clear notification and await acknowledgment
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (5 errors)
✅ Passed checks (20 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
…erts Review findings on the previous commit: - The notification body carried the full tool summary (complete shell command). Commands can embed credentials, and notification banners reach lock screens, paired phones, and the recorded notification history. The body now names only the tool — the same "<tool> needs approval" string the in-app Feed approval banner uses — never the tool input. - Codex fires PermissionRequest before its own "Approve for me" reviewer (#5507), so an auto-approved request would leave a stale or false "Permission" alert with nothing pending. Codex tool lifecycle progress (PreToolUse/PostToolUse feed events) now clears the pane's notifications, mirroring Claude's pre-tool-use clear_notifications contract. The clear is registry-scoped to sources that raise native approval prompts, so other agents' tool telemetry never touches the notification queue. The immediate notify on PermissionRequest is retained deliberately: codex has no post-reviewer hook, and the wrapper-injected schema already posts this same immediate needs-permission notification via `hooks codex notification`; suppressing until authoritative proof would recreate the silence reported in #9592. Verified against a mock socket: PermissionRequest now emits `notify_target_async <ws> <sf> Codex|Permission|shell needs approval|c=needs-permission;p=0` and PreToolUse/PostToolUse emit `clear_notifications --tab=<ws> --panel=<sf>`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review asked for per-request keyed notification clears. Rejected: notifications carry no request identity anywhere in cmux, and pane-wide uncorrelated clears on progress signals are the shipped contract for every agent integration (Claude session-start/prompt-submit/pre-tool-use, the generic approvalResponse action for Hermes' resolved native approvals, and codex's own prompt-submit hook — which also self-heals denied-approval residue at the next turn). Record that invariant on the flag so future reviewers see the ownership decision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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 34939-34973: Update the socketPath branch around
sendBestEffortFeedTelemetry so the feed line and optional promptLine are sent
through one reused SocketClient connection, avoiding separate connection and
authentication attempts. Preserve the existing best-effort behavior and send
both lines when promptLine is non-nil.
In `@CLI/FeedEventClassifier.swift`:
- Around line 169-238: Make wireMapping the sole owner of
clearsNativeApprovalPrompt: derive the appropriate clear value there for the
lifecycle branches using its existing source parameter, and remove the separate
clearsPrompt derivation and overwrite in classify. Ensure the
FeedEventClassification returned by wireMapping is passed through without
replacing its clear flag.
- Around line 65-80: Update classify’s clearsPrompt logic in FeedEventClassifier
so Codex .toolStart does not clear a pending native approval prompt. Restrict
Codex clearing to the post-approval lifecycle event that confirms the approval
decision resolved, while preserving existing clearing behavior for other
eligible sources and events.
In `@cmuxTests/FeedEventClassificationTests.swift`:
- Around line 172-181: Extend codexToolLifecycleClearsNativeApprovalPrompt to
assert that the beforeShellExecution lifecycle alias with the shell tool also
sets clearsNativeApprovalPrompt to true. Keep the existing assertions unchanged
and cover this alias alongside the other Codex PreToolUse entrypoints.
🪄 Autofix
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 Plus
Run ID: 07ca1e8c-e36d-4358-968b-e3831151d07e
📒 Files selected for processing (3)
CLI/FeedEventClassifier.swiftCLI/cmux.swiftcmuxTests/FeedEventClassificationTests.swift
…ends Address review findings on the clear semantics and delivery: - Codex gives no ordering guarantee between its PermissionRequest and pre-tool hooks, so a start-time clear could race and erase the just-raised prompt while the agent is still blocked — reintroducing the silence behind #9592. Clears now fire only on tool COMPLETION (PostToolUse/post_tool_use), which strictly follows any approval. beforeShellExecution and PreToolUse are covered as non-clearing in tests. - wireMapping is now the single owner of clearsNativeApprovalPrompt; classify no longer rewraps the classification. - The socketPath telemetry lane sends the feed frame and the notify/clear line over ONE connection (batched sendBestEffortFeedTelemetry(lines:)) instead of paying a second connect + auth per tool event. Verified against a mock socket: PermissionRequest emits feed.push + notify_target_async (redacted body), PreToolUse emits only feed.push, and PostToolUse emits feed.push + a pane-scoped clear_notifications. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… too Two review findings on the delivery lanes: - The notify/clear line now precedes the feed frame in both send branches: the feed frame can be large and its best-effort 50ms write can fail under backpressure, and a failed telemetry write must never swallow the permission notification (that would recreate #9592's silence). - The wrapper-injected codex hooks route tool telemetry through `hooks codex post-tool-use` → sendFeedTelemetry, which bypassed the feed-hook clear: wrapper-launched seats posted the permission notification via `hooks codex notification` but never cleared it on tool completion. sendFeedTelemetry now derives the same FeedEventClassifier decision and prepends the pane-scoped clear, giving both ingress paths one shared classification/side-effect path. The target helper falls back to the pane env (CMUX_WORKSPACE_ID / CMUX_SURFACE_ID) when the event lacks identities. Verified against a mock socket on both paths: `hooks feed --source codex --event PermissionRequest` emits notify_target_async then feed.push; `--event PostToolUse` and `codex-hook post-tool-use` emit clear_notifications then feed.push. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The previous commit routed the native-approval-prompt clear through sendFeedTelemetry so wrapper-launched codex seats would clear on tool completion. Review correctly flagged that the wrapper-injected hooks run as fire-and-forget nohup workers with no ordering guarantee: a delayed PostToolUse worker's pane clear could erase a NEWER request's live permission notification — silencing a blocked agent, the exact failure this PR fixes. Remove the wrapper-lane clear and document why; wrapper staleness is pre-existing shipped behavior that self-heals at the next prompt-submit pane clear. The synchronous feed-hook path keeps the clear: its events arrive in codex's own order. Verified against a mock socket: `codex-hook post-tool-use` emits no clear; `hooks feed --source codex --event PostToolUse` emits exactly one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review correctly noted that one-way writes return before the app's detached per-connection worker enqueues the mutation, so a completed hook process was no proof its clear had been applied — a delayed clear could still erase a newer request's live notification. The notify/clear line is now sent request/response and awaited (bounded at 2s) before the synchronous feed hook returns, the same contract Claude's and Hermes' hooks use for clear_notifications/notify_target_async. Codex runs these hooks synchronously, so the next hook's process starts only after this mutation is in the app's ordered lane. The feed frame stays one-way: nonessential telemetry whose failure must never swallow the notification. Verified against an acknowledging mock socket: PermissionRequest emits awaited notify then feed.push, PostToolUse emits awaited clear then feed.push, PreToolUse emits only feed.push; warm hook latency ~0.15s. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two review findings: - On relay-backed sockets, the acknowledged attention send closes its connection, so the follow-up one-way feed write reconnected implicitly with the default (unbounded-by-write-timeout) relay challenge — able to outlive the agent's hook budget. The feed frame now travels on its own explicitly bounded best-effort connection whenever an attention command was sent; no implicit reconnect remains. - The attention command construction (UUID gating, payload shape, tool name sanitization, needs-permission meta) moves into the shared-compiled FeedEventClassifier as a pure builder, and new unit tests assert the exact notify_target_async / clear_notifications wire lines plus the nil cases. Transport ordering (awaited acknowledge before the hook returns) remains verified by the mock-socket harness documented in the PR — the app-hosted unit target cannot spawn the CLI against a live socket. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/FeedEventClassifier.swift`:
- Around line 44-52: Update the .promptSubmit handling in FeedEventClassifier,
including the telemetry("UserPromptSubmit") path, so clearsNativeApprovalPrompt
is true when the source raises native approval prompts. Preserve the existing
behavior for sources that do not use native approval prompts.
In `@cmuxTests/FeedEventClassificationTests.swift`:
- Around line 314-354: Extend
attentionCommandRequiresUUIDTargetsAndAttentionSemantics to assert nil when
surfaceId is missing. Add coverage near attentionCommandSanitizesPipeInToolName
for newline characters in tool names, verifying they are replaced or neutralized
so the generated socket command remains a single line.
🪄 Autofix
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 Plus
Run ID: 7d1d47b7-00d6-4d08-8e6d-1bf4c2a27749
📒 Files selected for processing (3)
CLI/FeedEventClassifier.swiftCLI/cmux.swiftcmuxTests/FeedEventClassificationTests.swift
Codex's fire-and-forget prompt-submit worker clears the pane at turn start from a detached process; in a narrow window (worker slower than the model's first approval-needing tool call) its late clear can remove the new permission notification. This is the same pre-existing exposure the shipped wrapper-path notification has always had — this change does not widen the class — and eliminating it requires origin-time-fenced clears, a cross-layer notification-store protocol change out of scope here. Record the invariant at the send site. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two review-suggested test additions: a nil surface ID must yield no command (both UUID targets required), and a newline in a payload-controlled tool name must not split the single socket command line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review noted the added coverage stopped at classification and pure command
construction — a misrouted promptLine dispatch would restore the silent
agent while every unit test stayed green. Add a focused behavior suite
that spawns the real CLI against the existing FakeCmuxSocket harness and
asserts, on the actual socket transport:
- codex PermissionRequest emits the exact gated notify_target_async line
and it precedes the feed.push telemetry frame;
- codex PostToolUse emits the exact pane-scoped clear_notifications line
before its telemetry frame;
- codex PreToolUse emits neither (no premature clear, no over-notify);
- the hook AWAITS the app's acknowledgement: with the fake delaying its
OK by 0.5s, a fire-and-forget regression would return instantly.
Verified red/green: the suite fails against the pre-fix release CLI
0.64.22 ("missing gated permission notification") and passes against this
branch's build. Wired into ci.yml beside the other CLI hook suites.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review correctly flagged that the essential notify/clear connection reused the telemetry lane's 50ms fast-fail bounds: a relay-backed socket's multi-round-trip HMAC handshake (or a busy local socket) could never finish inside them, so remote terminals silently lost the permission notification. The attention transport now runs under one absolute deadline (feedAttentionAcknowledgeTimeoutSeconds) spanning connect, authentication, and the acknowledged send; the telemetry lane keeps its deliberate fast-fail bounds. New behavior test: with the fake socket delaying every reply (including auth) by 0.5s under a socket password, the notification still delivers — a fast-fail transport drops it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
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)
CLI/cmux.swift (1)
34951-35000: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick winReuse the open
clientfor the telemetry line instead of opening a second connection.When
promptLineis non-nil andclientis non-nil, the code sends the attention line over the existingclient, then callssendBestEffortFeedTelemetry(socketPath: telemetrySocketPath, ...)for the feed telemetry line. This constructs a newSocketClient, connects, and re-authenticates, even thoughclientis already open and authenticated.Compare this to the
else if let clientbranch two lines below, which correctly sends the telemetry line withclient.sendOneWay(command: line, writeTimeout: 0.05)on the existing connection. Theclient-present branch with apromptLineshould do the same instead of falling back to a freshsocketPath-based connection.This method is reachable in production: the
feed-hookbackward-compatibility command passes the main authenticated sessionclientintorunFeedHook, so this doubling happens on every classified event with an attention command in that call path.⚙️ Proposed fix: reuse the open client for telemetry when available
if let promptLine { if let client { _ = try? client.send( command: promptLine, responseTimeout: Self.feedAttentionAcknowledgeTimeoutSeconds ) + _ = try? client.sendOneWay(command: line, writeTimeout: 0.05) } else if let socketPath { sendAcknowledgedFeedAttention( socketPath: socketPath, attentionLine: promptLine, socketPassword: socketPassword ) - } - let telemetrySocketPath = socketPath ?? client?.socketPath - if let telemetrySocketPath { sendBestEffortFeedTelemetry( - socketPath: telemetrySocketPath, + socketPath: socketPath, line: line, socketPassword: socketPassword ) } } else if let client {🤖 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 `@CLI/cmux.swift` around lines 34951 - 35000, Update the promptLine handling in runFeedHook so that when the existing client is available, it sends the telemetry line with client.sendOneWay using the same bounded write timeout as the nearby client branch. Only use sendBestEffortFeedTelemetry with the socket path when no client is available, avoiding a second connection and re-authentication.
🤖 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 `@tests/test_codex_permission_prompt_notification.py`:
- Around line 205-216: Replace the elapsed-based assertion in the
PermissionRequest test with deterministic acknowledgement gating: update
FakeCmuxSocket to signal notification receipt and block its acknowledgement on a
test-controlled gate, launch the hook via Popen, assert it remains running while
the gate is closed, then release the gate and await completion while preserving
the notification and empty-stdout checks.
---
Outside diff comments:
In `@CLI/cmux.swift`:
- Around line 34951-35000: Update the promptLine handling in runFeedHook so that
when the existing client is available, it sends the telemetry line with
client.sendOneWay using the same bounded write timeout as the nearby client
branch. Only use sendBestEffortFeedTelemetry with the socket path when no client
is available, avoiding a second connection and re-authentication.
🪄 Autofix
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 Plus
Run ID: 9ff6c104-6624-4b81-9010-ddd76de539ed
📒 Files selected for processing (4)
.github/workflows/ci.ymlCLI/cmux.swiftcmuxTests/FeedEventClassificationTests.swifttests/test_codex_permission_prompt_notification.py
| stdout, frames, elapsed = run_feed_hook_capture( | ||
| cli_path, root / "cmux-ack.sock", "PermissionRequest", raw_response_delay=delay | ||
| ) | ||
| if stdout != {}: | ||
| raise AssertionError(f"PermissionRequest must stay non-blocking: {stdout!r}") | ||
| if EXPECTED_NOTIFY_COMMAND not in raw_commands(frames): | ||
| raise AssertionError(f"missing gated permission notification: {frames!r}") | ||
| if elapsed < delay - 0.1: | ||
| raise AssertionError( | ||
| f"hook returned in {elapsed:.2f}s without awaiting the delayed " | ||
| f"({delay}s) notification acknowledgement" | ||
| ) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Replace the elapsed-time assertion with an acknowledgement gate.
elapsed < delay - 0.1 makes test correctness depend on scheduler timing. It can fail under load without a transport regression.
Have FakeCmuxSocket signal when it receives the notification and block its acknowledgement on a test-controlled gate. Start the hook with Popen, verify it remains running while the gate is closed, then release the gate and await completion.
As per coding guidelines, “Test code must avoid real wall-clock dependencies” and “Tests must not read wall-clock APIs … in assertions.”
🧰 Tools
🪛 Ruff (0.16.1)
[warning] 209-209: Avoid specifying long messages outside the exception class
(TRY003)
[warning] 211-211: Avoid specifying long messages outside the exception class
(TRY003)
[warning] 213-216: Avoid specifying long messages outside the exception class
(TRY003)
🤖 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 `@tests/test_codex_permission_prompt_notification.py` around lines 205 - 216,
Replace the elapsed-based assertion in the PermissionRequest test with
deterministic acknowledgement gating: update FakeCmuxSocket to signal
notification receipt and block its acknowledgement on a test-controlled gate,
launch the hook via Popen, assert it remains running while the gate is closed,
then release the gate and await completion while preserving the notification and
empty-stdout checks.
Source: Coding guidelines
Review flagged that the attention notify/clear was a plain V1 command
built from ambient env identities: on a restored remote pane those are
snapshot aliases, and the relay remaps IDs only inside JSON requests, so
the command would target a stale pane and the blocked agent stayed
silent on restored remote terminals.
The attention delivery now resolves the live identity first through the
alias-safe `agent.resolve_delivery_target` {surface_id} re-home probe —
the same contract Claude's hooks use; the probe's JSON request IS
relay-remapped, so the app answers with live identities — and addresses
the V1 command to the answer, falling back to the ambient identities
when the probe is unsupported or fails (correct for local panes). The
probe, connect, auth, and acknowledged send all share the one absolute
2s deadline.
New behavior test: with the fake resolving the ambient surface to a
re-homed (workspace, surface) pair, the notification must target the
resolved pair and never the ambient identities.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 34661-34753: Update deliverNativeApprovalPromptAttention and the
runFeedHook telemetry flow to reuse the active connection for
sendBestEffortFeedTelemetry when activeClient.isRelayBacked is false, including
the common nil-client local socket path. Keep relay-backed connections on the
existing separate-connection telemetry path because the acknowledged send closes
them, and preserve proper closure and timeout behavior for both paths.
🪄 Autofix
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 Plus
Run ID: 09a408da-13ee-4c65-9ca9-1a0693632972
📒 Files selected for processing (2)
CLI/cmux.swifttests/test_codex_permission_prompt_notification.py
| /// Delivers the pane-attention command (permission notify / resolved | ||
| /// clear) for a classified feed event: resolves the LIVE pane identity, | ||
| /// builds the command via the shared, unit-tested builder | ||
| /// (``FeedEventClassifier/nativeApprovalPromptAttentionCommand``), and | ||
| /// sends it request/response, AWAITING the app's `OK`. | ||
| /// | ||
| /// Live-identity resolution uses the same alias-safe | ||
| /// `agent.resolve_delivery_target` `{surface_id}` re-home probe Claude's | ||
| /// hooks use: on a restored remote pane the ambient env IDs are snapshot | ||
| /// aliases, and the relay remaps IDs only inside JSON requests — a plain | ||
| /// V1 command built from ambient IDs would target a stale pane. The | ||
| /// probe's request IS remapped, so the app answers with live identities; | ||
| /// when it is unsupported or fails, the ambient identities are the | ||
| /// fallback (correct for local panes). | ||
| /// | ||
| /// Awaiting the attention line is what makes cross-process hook ordering | ||
| /// real: one-way writes return before the app's detached per-connection | ||
| /// worker has enqueued the mutation, so a completed hook process is no | ||
| /// proof its clear was applied — a delayed clear could then erase a | ||
| /// NEWER request's live notification (#9592's silence, reintroduced). | ||
| /// Codex runs these feed hooks synchronously, so blocking this process | ||
| /// until the app acknowledges the mutation (same request/response | ||
| /// contract Claude's and Hermes' hooks use) guarantees the next hook's | ||
| /// process starts only after this mutation is in the app's ordered lane. | ||
| /// | ||
| /// Everything runs under ONE absolute deadline spanning connect, | ||
| /// authentication, resolution, and the acknowledged send: this line is | ||
| /// the essential payload, and the budget must survive a relay-backed | ||
| /// connection's multi-round-trip handshake — unlike the telemetry | ||
| /// lane's deliberate 50 ms fast-fail bounds. Failures never propagate: | ||
| /// the hook always returns `{}` after the bounded wait. | ||
| private func deliverNativeApprovalPromptAttention( | ||
| classification: FeedEventClassification, | ||
| source: String, | ||
| toolName: String, | ||
| eventDict: [String: Any], | ||
| env: [String: String], | ||
| client: SocketClient?, | ||
| socketPath: String?, | ||
| socketPassword: String? | ||
| ) { | ||
| let ambientWorkspaceId = (eventDict["workspace_id"] as? String) ?? env["CMUX_WORKSPACE_ID"] | ||
| let ambientSurfaceId = (eventDict["surface_id"] as? String) ?? env["CMUX_SURFACE_ID"] | ||
| let deadline = Date().addingTimeInterval(Self.feedAttentionAcknowledgeTimeoutSeconds) | ||
| func remainingBudget() -> TimeInterval { | ||
| max(deadline.timeIntervalSinceNow, 0.05) | ||
| } | ||
|
|
||
| var ownedClient: SocketClient? | ||
| defer { ownedClient?.close() } | ||
| let activeClient: SocketClient | ||
| if let client { | ||
| activeClient = client | ||
| } else if let socketPath { | ||
| let attentionClient = SocketClient(path: socketPath) | ||
| do { | ||
| try attentionClient.connectWithoutRetry(responseTimeout: remainingBudget()) | ||
| try authenticateClientIfNeeded( | ||
| attentionClient, | ||
| explicitPassword: socketPassword, | ||
| socketPath: socketPath, | ||
| responseTimeout: remainingBudget(), | ||
| deadline: deadline | ||
| ) | ||
| } catch { | ||
| attentionClient.close() | ||
| return | ||
| } | ||
| ownedClient = attentionClient | ||
| activeClient = attentionClient | ||
| } else { | ||
| return | ||
| } | ||
|
|
||
| let liveTarget = resolvedAttentionDeliveryTarget( | ||
| workspaceId: ambientWorkspaceId, | ||
| surfaceId: ambientSurfaceId, | ||
| client: activeClient, | ||
| deadline: deadline | ||
| ) | ||
| guard let attentionLine = FeedEventClassifier.nativeApprovalPromptAttentionCommand( | ||
| classification: classification, | ||
| displayName: Self.agentDef(named: source)?.displayName ?? source, | ||
| toolName: toolName, | ||
| workspaceId: liveTarget?.workspaceId ?? ambientWorkspaceId, | ||
| surfaceId: liveTarget?.surfaceId ?? ambientSurfaceId | ||
| ) else { return } | ||
| _ = try? activeClient.send( | ||
| command: attentionLine, | ||
| responseTimeout: remainingBudget(), | ||
| deadline: deadline | ||
| ) | ||
| } |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win
Reduce the extra socket round trip added per native-approval event.
deliverNativeApprovalPromptAttention sends the acknowledged attention command over client when it is provided, or otherwise opens, uses, and closes its own connection. Immediately after it returns, runFeedHook calls sendBestEffortFeedTelemetry, which always opens a brand-new SocketClient connection and re-authenticates for the feed.push telemetry line, regardless of whether client was already open.
In the common feed-hook/hooks feed dispatch path, client is nil, so each native-approval event now performs two full connect+authenticate round trips instead of one. classification.clearsNativeApprovalPrompt fires on Codex PostToolUse for every tool call on agents that raise native approval prompts, so this cost repeats on every tool call, not once per turn.
The code comments explain that combining both sends onto one connection is unsafe for relay-backed sockets, because send() closes a relay connection after use and a later sendOneWay reconnect is not timeout-bounded. That reasoning does not apply to local Unix-domain sockets: SocketClient.send() only sets shouldCloseAfterSend when relayEndpoint != nil, so a non-relay connection stays open after the acknowledged attention send and could carry the telemetry line as a follow-up sendOneWay call on the same connection before closing.
Consider branching on activeClient.isRelayBacked inside deliverNativeApprovalPromptAttention: reuse the same connection for the telemetry line when it is not relay-backed, and fall back to a separate connection only for relay-backed sockets.
Also applies to: 35004-35043
🤖 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 `@CLI/cmux.swift` around lines 34661 - 34753, Update
deliverNativeApprovalPromptAttention and the runFeedHook telemetry flow to reuse
the active connection for sendBestEffortFeedTelemetry when
activeClient.isRelayBacked is false, including the common nil-client local
socket path. Keep relay-backed connections on the existing separate-connection
telemetry path because the acknowledged send closes them, and preserve proper
closure and timeout behavior for both paths.
Review flagged that the optional live-target probe received the entire remaining attention deadline: a stalled probe could consume the whole budget and starve the notify/clear send it exists to serve. The probe is now capped (1s) and always leaves a send reserve (0.75s) of the shared deadline; when the remaining budget cannot fund both, the probe is skipped and the command falls back to ambient addressing. New behavior test: with the fake stalling agent.resolve_delivery_target for 3s (past the whole deadline), the notification is still written, addressed to the ambient identities. FakeCmuxSocket now keeps draining buffered request lines when its replies hit a closed peer — matching the real app's per-connection worker, which reads written lines after the hook process exits (verified pi suite unaffected). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#9804 landed `FeedEventClassifier.classify(...).0` while classify() already returned the named FeedEventClassification struct, so CLI/cmux.swift no longer compiles on main (every app-host and tests-build-and-lag CI job fails with "value of type 'FeedEventClassification' has no member '0'"). Use .hookEventName, matching the other call site. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Add standalone iOS keyboard pinning lab * Test rapid iOS keyboard dock reversals * Unify iOS keyboard dock presentation * Fix CLI compile break from classify() tuple access #9804 landed `FeedEventClassifier.classify(...).0` while classify() already returned the named FeedEventClassification struct, so CLI/cmux.swift no longer compiles on main (every app-host and tests-build-and-lag CI job fails with "value of type 'FeedEventClassification' has no member '0'"). Use .hookEventName, matching the other call site. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Strengthen rapid keyboard dock coverage * test(panes): drop stale MobileInjectedAttachStartupTests referencing removed API The main merge replaced MobileStartupConnectionCoordinator's connectInjectedAttach with the claim/finish lifecycle, and DogfoodAttachPreparationTests already covers that lifecycle end to end. The stale file kept the whole CmuxMobileShellUITests target from compiling, so no package UI suite could run in CI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Scope pairing scanner guidance copy onto MobilePairingScannerSheet The caseless MobilePairingScannerGuidanceCopy enum (from #9493) trips the namespace-enum rule in scripts/lint-ios-package-conventions.sh, turning the package-conventions-lint job red for every branch that touches Packages/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Scope keyboard dock seam measurement to transitions * Scope dock seam metric to keyboard transitions * Test whole dock during keyboard reversal * Isolate keyboard dock from terminal layout * Animate hosted keyboard dock reflows * Localize keyboard pinning lab name --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Fixes #9592.
Problem
With
notifications.agentPermissionPrompt: true(the default), a Codex seat blocked on its approval dialog never produces a user notification. Codex'sPermissionRequesthook is wired to the Feed bridge (cmux hooks feed --source codex --event PermissionRequest), andFeedEventClassifierdeliberately maps it to non-actionablePreToolUsetelemetry so cmux's Feed does not double-prompt against Codex's own approval reviewer (that mapping is what keeps "Approve for me" working). But that normalization also silently dropped the one signal Codex fires while blocked on the user, so noagentPermissionPromptnotification was ever raised — exactly what the issue's bus capture shows (agent.hook.PreToolUse+feed.item.*, nonotification.*).Fix
The classifier's actionability and its user-attention notification were conflated in one flag. This PR separates them with a new registry semantic instead of special-casing codex at a call site:
FeedEventClassifiergains a.nativeApprovalPromptsemantic: "the agent is blocked waiting for the user in its own approval UI." Wire behavior is unchanged (non-actionablePreToolUsetelemetry, no cmux Feed approval card, no blocking wait), but the classification now carriesnotifiesNativeApprovalPrompt: true. Codex'sPermissionRequest/permission_requestregister with it. Any future agent with a native approval UI gets the same behavior with one registry line.runFeedHookdelivers a fire-and-forgetnotify_target_asyncon that flag, built through the sharedAgentHookNotificationClassifier(same "Permission" / "Approval needed" strings, samec=needs-permission;p=0meta the generic agentnotificationhook and Claude'spermission_promptpath emit), so the app gates it under the existing "Agent Needs Permission" setting viaAgentNotificationDelivery. The tool summary (command/path) becomes the notification body when present.No new user-facing strings: the notification reuses the existing localized
agent.generic.notification.subtitle.permission/agent.generic.notification.body.approvalNeededkeys and the agent display name ("Codex"). Localization audit: no UI/settings/menu/help text changed; no web message catalogs affected.Commits
classifyto return aFeedEventClassificationstruct carryingnotifiesNativeApprovalPrompt(false everywhere, behavior-neutral) and addscodexPermissionRequestRaisesPermissionPromptNotification, which fails against the old mapping. The struct widening is included so the test compiles; the behavioral assertion is the failing part..nativeApprovalPromptsemantic, the codex registry entries, and the CLI notification delivery.Testing
FeedEventClassificationTests: new tests assert codexPermissionRequest/permission_requestclassify as telemetry-with-notification, the completion-only clear scoping, and the exact attention wire commands (UUID gating, payload shape, pipe/newline sanitization,needs-permissionmeta).tests/test_codex_permission_prompt_notification.pyspawns the real CLI against the existingFakeCmuxSocketharness and asserts the actual socket transport: the exact gatednotify_target_asyncline precedesfeed.pushonPermissionRequest; the exact pane-scopedclear_notificationsprecedes telemetry onPostToolUse;PreToolUseemits neither; and the hook awaits the app's acknowledgement (0.5s-delayed OK is waited out — a fire-and-forget regression returns instantly). Red/green proven: the suite fails against release CLI 0.64.22 ("missing gated permission notification") and passes on this branch.cmux hooks feed --source codex --event PermissionRequestwith a codex payload from a bound pane env):feed.pushtelemetry frame withhook_event_name: "PreToolUse"— no notification, matching the issue's bus capture.PermissionRequestemits the same unchangedfeed.pushframe plusnotify_target_async <workspace-uuid> <surface-uuid> Codex|Permission|shell needs approval|c=needs-permission;p=0;PreToolUseemits only thefeed.pushframe (no over-notification, no premature clear);PostToolUseemitsfeed.pushplus a pane-scopedclear_notifications --tab=<ws> --panel=<sf>.AgentNotificationDelivery/agentNotificationShouldDeliver) is pre-existing and already unit-tested.Review round (codex autoreview findings addressed)
Notification body no longer contains tool input. The first cut put the tool summary (full shell command) in the notification body; commands can embed credentials and banners reach lock screens, paired phones, and the recorded notification history. The body now names only the tool —
"<tool> needs approval"via the samefeed.notification.permission.bodystring the in-app Feed approval banner uses (falling back to the existing "Approval needed").Stale/false alerts self-heal — on tool completion only. Codex's
PermissionRequestfires before its own "Approve for me" reviewer (per Keep Codex permission hooks non-blocking #5507's contract), so an auto-approved request would leave a stale "Permission" alert. A completed tool strictly follows any approval, so codexPostToolUsefeed events now clear the pane's notifications (mirroring the pane-wide clears Claude's lifecycle hooks and Hermes' approval-response hook already perform; codex's ownprompt-submithook clears denied-approval residue at the next turn). Pre-tool events deliberately do NOT clear: codex gives no ordering guarantee betweenPermissionRequestand its pre-tool hooks, so a start-time clear could race and erase the just-raised prompt while the agent is still blocked. The clear is registry-scoped: only sources that raise native approval prompts get it. The clear stays pane-wide and uncorrelated by design — notifications carry no request identity anywhere in cmux, and this matches every existing agent integration.Clears ride only the synchronous feed-hook path. A review round asked for the clear on the wrapper telemetry lane too (
hooks codex post-tool-use); the next round correctly observed that wrapper-injected hooks run as fire-and-forgetnohupworkers with no ordering guarantee, so a delayed completion worker's clear could erase a newer request's live notification. The wrapper-lane clear was removed again (documented insendFeedTelemetry); wrapper-path staleness is pre-existing shipped behavior that self-heals at the nextprompt-submitpane clear. The feed-hook path's events arrive in codex's own order (synchronous hook commands), so its clear is ordering-safe.Immediate notify retained deliberately. Codex offers no post-reviewer "now prompting the user" hook, and the wrapper-injected hook schema (
CodexHookInjectionSchema→cmux hooks codex notification) already posts this same immediate needs-permission notification today; suppressing until "authoritative" proof would recreate exactly the silence Codex PermissionRequest hook events never produce a user notification (agentPermissionPrompt) — event is normalized to PreToolUse at ingest #9592 reports. The alert is gated by the user's "Agent Needs Permission" setting and now self-heals as above.Transport ordering is acknowledged, not assumed. The notify/clear line is sent request/response and awaited (bounded 2s) before the synchronous hook returns — the same contract Claude's and Hermes' hooks use — so the next hook's process starts only after the mutation is in the app's ordered lane. The feed frame stays one-way best-effort on its own bounded connection (no implicit relay reconnect can outlive the agent's hook budget). Warm hook latency ~0.15s measured.
Consciously accepted residual (documented in code): codex's fire-and-forget
prompt-submitworker clears the pane at turn start from a detached process; if that worker is slower than the model's very first approval-needing tool call, its late clear can remove the new notification. This is the same pre-existing exposure the shipped wrapper-path notification (hooks codex notification) has always had — this PR does not widen the class. Eliminating it requires origin-time-fenced clears (a cross-layer notification-store protocol change), deliberately out of scope.Open review findings (deferred — not addressed in this PR)
Thirteen structured-review rounds ran on this branch; every earlier finding was either fixed as prescribed (redaction, completion-only clears, acknowledged transport, relay deadline budget, alias-safe live-target resolution, probe budget reserve, CLI-level behavior tests) or consciously rejected with codebase evidence (per-request notification correlation, prompt-submit clear race). The final round raised three further transport-tail P1s that are deferred as follow-ups rather than iterated further here:
sendrequires a fresh HMAC handshake, the 2s attention deadline may not fit probe + notify on slow links; needs relay-path measurement to size properly.{}before classification, so an oversized PermissionRequest doesn't notify; delivery could fall back to the trusted--eventflag.All three are narrower than the shipped baseline this PR improves on (previously no codex approval notification existed on any path), and each is confined to the new attention lane's degraded-transport tails.
Notes / scope
PostToolUse; deny → turn continues), so setting needs-input state here would risk a stuck badge. The notification is the contractagentPermissionPromptdocuments.CMUX_WORKSPACE_ID/CMUX_SURFACE_ID(same identities the feed frame itself rides); if either is missing or not a UUID the notification is skipped best-effort, never failing the hook.🤖 Generated with Claude Code