Skip to content

fix(codex): hold the fabricated permission BEL for the Codex quiet window - #14715

Closed
brennanb2025 wants to merge 1 commit into
mainfrom
brennanb2025/codex-permission-bell-quiet-window-13600
Closed

brennanb2025 wants to merge 1 commit into
mainfrom
brennanb2025/codex-permission-bell-quiet-window-13600

Conversation

@brennanb2025

Copy link
Copy Markdown
Contributor

ELI5

When Codex pauses to ask permission, Orca fakes a terminal "ding" so the pane lights up. Under "Approve for me" Codex answers its own question a moment later — but the ding had already fired, so you got a false Attention requested notification. This PR makes the ding wait a moment and cancel itself if Codex resolves the pause. The visible "Codex - action required" label still appears instantly.

What Changed

  • src/shared/codex-attention-quiet-window.ts (new) — owns CODEX_ATTENTION_QUIET_MS (moved out of the renderer coordinator, value unchanged at 1500ms), plus shouldDeferSyntheticPermissionBell and buildSyntheticTerminalTitleFrame.
  • src/main/synthetic-permission-bell-deferral.ts (new) — per-pane timer map that arms, re-arms, cancels, and bulk-cancels the held-back BEL.
  • src/main/index.ts — driveSyntheticTitleFromHook now emits the OSC title without the trailing BEL for a Codex permission pause and defers the BEL instead. Any later non-permission hook state, a pane status clear, or window teardown cancels it.
  • src/renderer/src/components/terminal-pane/agent-completion-coordinator.ts — imports the shared constant instead of defining a second copy.

No wire fields, no new env vars, no installer/relay/WSL/SSH changes.

Why

#8519 added a 1.5s quiet window for Codex attention — but only on the renderer's dispatchAttention path (the rich "… - Codex needs input" notification).

Orca raises attention for the same hook event a second way. src/main/index.ts fabricates a synthetic title frame per hook state, and for waiting/blocked it appends a standalone BEL:

sendSyntheticTitle(ptyId, `\x1b]0;${label}\x07${needsUserInput ? '\x07' : ''}`, { force: true })

That frame goes to runtime.ingestSyntheticTitleFrame → applySyntheticTitleFrame → onBell in pty-connection.ts, which marks the worktree/tab unread and, after only AGENT_TASK_COMPLETE_NOTIFICATION_GRACE_MS (250ms), dispatches { source: 'terminal-bell' }. That notification renders as Bell in <worktree> / <repo> · Attention requested — exactly the notification in the issue screenshot.

So the reporter's observation is explained without the auto-review needing to exceed 1.5 seconds: the 1.5s window never covered this path at all. The reporter said as much ("This reproduction does not prove that the auto-review itself took more than exactly 1.5 seconds"); this is the missing half.

The fix makes Orca's own fabricated attention signal honor the decision #8519 already made, and puts both paths on one shared constant so they cannot drift apart again.

Scope discipline. Only the BEL Orca fabricates is deferred. A real BEL emitted by Codex's own output is untouched — onBell documents that suppressing real PTY bells is a transport-layer guess, and that stays true.

Not over-suppressing. Codex request_user_input also normalizes to waiting, but no auto-reviewer ever answers a question put to the user, so it is exempt and rings inline. Non-Codex runtimes are untouched. And a Codex pause that is not resolved still rings — 1.5s later, not never.

Superseded / related PRs

Supersedes #11046 (fix(codex): suppress auto-review-owned permission attention, @GoodFarming). Its diagnosis of the symptom is right, but the remedy does not fix the reported repro:

  1. Its authority function ignores the wire stamp outright — resolveAuthoritativeCodexApprovalReviewer contains a literal void args.wireReviewer and returns resolveCodexApprovalReviewer(args.agentArgs). Reviewer ownership therefore comes only from Orca launch args matching approvals_reviewer=auto_review.
  2. In [Bug]: Codex “Approve for me” can still trigger false attention notifications after #8519 #13600 the user selects "Approve for me" inside the Codex TUI, not via an Orca launch arg. Reviewer resolves to unknown, suppression never engages, and the bell still fires.
  3. Because the wire value is voided for the decision, the ~1000 lines plumbing ORCA_CODEX_APPROVAL_REVIEWER through installer-utils, the managed hook script, pty-subprocess, WSL interop (WSLENV), the SSH relay, the relay hook server, hydrate persistence, and a new codexApprovalReviewer IPC/wire field carry no effect on the fix — and the hook version bump 1 → 2 is wire surface taken on for it.
  4. It never touches the BEL emission; it only re-gates driveSyntheticTitleFromHook behind the same launch-arg proof.

Also related to #8387 / #8519 (the timer this restores parity with) and #13991 (closed as a duplicate of #11046).

#10947 / PR #11174 is independent — not bundled. #10947 is about where Codex status hooks get installed (real ~/.codex/hooks.json vs managed CODEX_HOME) and shares no code with the attention path; this PR touches none of the five files #11174 touches. Verified rather than assumed, per the precedent set in #14611.

Not a naming coincidence (per #14626). This gate keys on the normalized agentType === 'codex' plus normalized state, never on a hook event name, so it does not depend on Codex and Claude happening to share event vocabulary.

Linked Issue

Fixes #13600

Visual Proof

N/A — and stated plainly rather than implied: this was not verified on Windows. #13600 is os:Windows + WSL, and the Windows host available to this session is unreachable (remote_runtime_unavailable). The change is derived from the code path and covered by automated tests that drive the real createTerminalTitleTracker bell detector; no live Windows/WSL repro was performed and none is claimed.

Nothing here is platform-dependent — no process.platform branch is added or changed, and the deferral runs identically on macOS, Linux, and Windows. The affected notification is OS-level, so there is no rendered UI surface to screenshot.

Testing

npx vitest run --config config/vitest.config.ts \
  src/shared/codex-attention-quiet-window.test.ts \
  src/main/synthetic-permission-bell-deferral.test.ts \
  src/renderer/src/components/terminal-pane/agent-completion-coordinator.test.ts \
  src/renderer/src/components/terminal-pane/terminal-title-tracker-parity.test.ts \
  src/renderer/src/components/terminal-pane/terminal-side-effect-facts-handler.test.ts \
  src/renderer/src/components/terminal-pane/parked-terminal-byte-watcher.test.ts \
  src/renderer/src/components/terminal-pane/use-notification-dispatch.test.ts \
  src/renderer/src/components/terminal-pane/codex-auto-approval-notification-suppression.test.ts \
  src/renderer/src/hooks/agent-hook-completion-notifications.test.ts \
  src/shared/terminal-output-side-effects.test.ts
# 10 files, 261 tests passed

Coverage on the two assertions that matter:

Assertion Test
An auto-reviewable Codex PermissionRequest raises no bell holds the attention BEL out of a Codex permission frame while keeping the title — frame fed to the real tracker yields 0 bells
A genuine pause still raises attention rings once when the deferred BEL is released for a real pause — 1 bell
A Codex question to the user is never delayed rings a Codex request_user_input pause inline, without the quiet window — 1 bell, deferBell === false
Other runtimes unchanged still rings a non-Codex permission frame inline — 1 bell
Resolved pause drops the bell drops the BEL when the pause resolves inside the window (#13600)
Regression anchor pins the pre-fix frame as the one that rang inline — the shipped 1.4.179 frame still rings, so the new frame's silence is the fix and not a tracker that stopped seeing fabricated bells

Non-vacuity proven by mutation against pre-fix behavior:

  • Reverting the builder to the shipped inline BEL (needsUserInput ? BELL : '') → 2 tests fail.
  • Neutralizing SyntheticPermissionBellDeferral.cancel to a no-op → 3 tests fail.
  • Injecting an any into the new shared module → oxlint errors, confirming the lint gate is live on these paths rather than silently clean.

Scoped checks (no full typecheck — OOM risk):

  • npx tsc --noEmit over the two new modules and a probe asserting the ParsedAgentStatusPayload → driveSyntheticTitleFromHook / shouldDeferSyntheticPermissionBell / buildSyntheticTerminalTitleFrame call sites: exit 0.
  • npx oxlint on all new/changed files: clean; pre-commit oxlint + react-doctor + oxfmt passed.

Platforms actually exercised: macOS (unit tests). Windows / WSL: not exercised — see Visual Proof. SSH: no transport code touched.

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

AI Disclosure

Claude Opus 5 via Claude Code.

Review

  • Security — no new wire field, IPC field, env var, or hook-script surface; nothing crosses a trust boundary. Compare fix(codex): suppress auto-review-owned permission attention #11046, which adds a codexApprovalReviewer wire field and bumps the hook version.
  • Cross-platform — no process.platform branch added or changed; deferral behaves identically on macOS, Linux, and Windows.
  • Remote SSH — no relay, transport, or hook-installer code touched. Synthetic frames are fabricated in main and never entered the PTY byte stream.
  • Remote wire compatibility — no RPC param, stream opcode, or published-content change; a mixed-version client/host pair is unaffected.
  • Mobile — the terminal-bell notification path is desktop-only; no mobile surface touched.
  • Backwards compatibility — CODEX_ATTENTION_QUIET_MS moved modules with its value unchanged; every non-Codex path and every non-permission state emits byte-identical frames.
  • Performance — one setTimeout per Codex permission pause, unref'd so it cannot hold the app open at quit, replaced on re-arm and cleared on resume/status-clear/teardown.

Known residual (stated, not hidden): if a Codex auto-review round trip exceeds the quiet window, both attention paths still fire. That is now a single shared constant instead of two divergent behaviors, but tuning it needs a live Windows/WSL repro this session could not obtain, so it is deliberately left at the value #8519 chose.

Checklist

  • This PR is small and focused
  • I explained what changed and why (including ELI5)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred) — scoped lint/tests/typecheck run locally as listed above; full typecheck deliberately skipped (OOM risk), CI covers it

Author

  • X / Twitter: @BrennanKB5

…ndow

Codex "Approve for me" could still raise "Bell in <worktree> · Attention
requested" after #8519 because that PR debounced only the renderer's
hook-derived attention notification. Main fabricates its own attention
signal for the same hook: driveSyntheticTitleFromHook injected a standalone
BEL alongside the "Codex - action required" OSC title on every Codex
PermissionRequest, and that BEL reached onBell through the per-PTY tracker
with only the 250ms terminal-bell grace — never the 1.5s Codex window.

Route the fabricated BEL through the same window: the OSC title still lands
immediately so the visible status is never delayed, while the BEL is held
and dropped if the pause resolves inside the window. CODEX_ATTENTION_QUIET_MS
now lives in shared so the two attention paths cannot drift apart again.

Codex request_user_input also normalizes to `waiting`, but no auto-reviewer
answers a question put to the user, so it keeps ringing inline.

Fixes #13600
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@brennanb2025, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 5 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1b9219e9-1444-46d0-bafe-cfdcd588ad2a

📥 Commits

Reviewing files that changed from the base of the PR and between ab9d1a2 and 0637605.

📒 Files selected for processing (6)
  • src/main/index.ts
  • src/main/synthetic-permission-bell-deferral.test.ts
  • src/main/synthetic-permission-bell-deferral.ts
  • src/renderer/src/components/terminal-pane/agent-completion-coordinator.ts
  • src/shared/codex-attention-quiet-window.test.ts
  • src/shared/codex-attention-quiet-window.ts

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.

@marcelkooi

Copy link
Copy Markdown

Thank you for this PR @brennanb2025. Anything left for this to be merged?

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Superseded by #21389, which is merged.

This held the fabricated permission BEL for the 1.5s quiet window so an auto-resolved "Approve for me" pause could cancel it. The merged change removes the need: the row never enters a waiting state for a reviewer-owned approval, so the synthetic "action required" title and its BEL are never emitted in the first place.

Two things found while reproducing this that argue against the timing approach in general:

  • The quiet window cannot separate the cases. Auto-reviews were measured at 3s and 20s, and a person can answer in under a second, so no fixed constant divides them.
  • The existing window has a latent hole worth a separate look: its deferred callback re-checks isLive()/hasAgentRunEvidence and returns without notifying, so a real prompt whose pane goes non-live inside the window is dropped rather than delayed.

Closing as superseded. The suppressors and CODEX_ATTENTION_QUIET_MS are still wired on main and are now redundant; removing them is tracked separately.

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.

[Bug]: Codex “Approve for me” can still trigger false attention notifications after #8519

2 participants