Skip to content

Gate agent notifications on background work + per-category settings - #7129

Merged
lawrencecchen merged 27 commits into
mainfrom
feat-agent-notify-gating
Jul 2, 2026
Merged

lawrencecchen merged 27 commits into
mainfrom
feat-agent-notify-gating

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 1, 2026 •

Copy link
Copy Markdown
Contributor

Problem

cmux fired a "Completed" notification on every Claude turn end, even when the turn was intermediate: a background build, a Monitor, or a scheduled wakeup was still running and would re-invoke the agent. Real repro: a chrome build + a 3600s Monitor were still running when cmux pinged "done" at the 16-minute mark.

Empirical basis (claude 2.1.197, injected hooks)

  • The Stop hook payload carries background_tasks ({id,type,status,description,command}) and session_crons. Both [] on a truly-idle turn; a completed task vanishes from the array. Present since claude v2.1.145; absent (nil) on older clients.
  • The Notification payload does not carry background_tasks. It has notification_type: permission_prompt (blocked on user, immediate) and idle_prompt (~60s after turn end, and fires even while a background task runs).

Approach

The CLI has the runtime signal; the app has the config. The CLI classifies each agent notification and forwards a c=<category>;p=<0|1> meta segment appended to the notify_target_async payload; the app gates delivery by user settings.

category setting (default) pending=0 pending=1
needs-permission agentPermissionPrompt (on) deliver deliver
turn-complete agentTurnComplete (whenIdle) deliver drop
turn-complete agentTurnComplete = always / never always / never always / never
idle-reminder agentIdleReminder (on) deliver drop
other / meta absent (codex, grok, antigravity, legacy) — deliver deliver

pending = any background_tasks[].status == "running" OR non-empty session_crons. A persistent Monitor keeps pending=1 for its lifetime, so whenIdle intentionally holds the done-ping until the watcher stops; permission_prompt bypasses the gate.

Changes

  • CLI (CLI/cmux.swift): hasActiveClaudeBackgroundWork(_:), notifyMeta, notificationPayload(meta:); caches hadPendingBackgroundWorkAtStop on the session record (idle_prompt reads it since its payload lacks background_tasks). nil/absent arrays → not pending, so claude < 2.1.145 behaves as before.
  • App (Sources/TerminalController.swift): parseNotificationPayload gains an optional 4th meta segment, treated as meta only when it begins with c= (else folded back into the body, so legacy callers whose body contains | parse byte-identically). AgentNotificationGate decides delivery; gated in notifyTargetQueued before enqueue.
  • Settings: notifications.agentPermissionPrompt / agentTurnComplete / agentIdleReminder — DefaultsKeys, Settings UI rows (2 toggles + enum picker), cmux.json bridge + valid-paths, cmux.schema.json, en/ja/ko/uk localization.
  • Tests: ClaudeBackgroundWorkNotifyTests (7 CLI behavioral: meta tags + cache across stop/permission/idle) and AgentNotificationGateTests (8 pure: full decision table + meta parser), wired into pbxproj.

Default-behavior note

For the common case (no background work) pending=0, so you still get the instant "done" ping. Only when background work is pending does whenIdle suppress it. permission_prompt still notifies by default.

🤖 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

Medium Risk
Touches notification delivery, agent lifecycle/hibernation, and large CLI hook paths; wrong gating could hide permission prompts or duplicate/suppress completion pings, though defaults preserve most legacy behavior and tests cover key paths.

Overview
Fixes premature “agent finished” pings when Claude (and other agents) still have running background tasks or scheduled crons after a turn ends.

CLI classifies hook notifications and appends an optional 4th pipe segment c=<category>;p=<0|1> on notify_target_async. It detects pending work from background_tasks / session_crons, caches hadPendingBackgroundWorkAtStop for later idle_prompt events (which lack those fields), keeps lifecycle/status Running instead of Idle while work is pending, and skips misleading Needs input for suppressed idle nags. Built-in agents (Claude, Grok, Antigravity) get consistent tagging and intermediate-event suppression.

App parses that meta in TerminalController, applies agentNotificationShouldDeliver from new AgentNotificationGate, and drops gated payloads before enqueue. Untagged payloads behave as before.

Settings: notifications.agentPermissionPrompt, agentTurnComplete (whenIdle | always | never), agentIdleReminder — UI, cmux.json/schema, localization. Tests cover the decision table, meta parser, and CLI stop/notification behavior.

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


Summary by cubic

Gates agent notifications by background work and per‑category settings so “Finished”/“Waiting” alerts only fire when truly idle across all built‑in agents. Keeps panes Running while work is pending, localizes the status pill, and adds per‑category notification controls.

  • New Features

    • Tag agent notifications with c=<category>;p=<0|1> and gate in‑app via AgentNotificationGate on notify, notify_surface, notify_target, and notify_target_async; cache hadPendingBackgroundWorkAtStop so idle_prompt reads pending.
    • Settings: notifications.agentPermissionPrompt, notifications.agentTurnComplete (whenIdle default | always | never), notifications.agentIdleReminder with UI, search entries, cmux.json bridge/schema, and localization.
    • Built‑ins (Claude, Grok, Antigravity) tag turn boundaries and permission/waiting prompts; typeless notifications fall back to cue classification; bypass‑permission question bells tag needs-permission.
  • Bug Fixes

    • Strict meta parser: only c=turn-complete|needs-permission|idle-reminder;p=0|1 is accepted; extras/duplicates/unknown categories are ignored and left in the body; legacy payloads parse unchanged.
    • Suppress “turn‑complete” and “idle‑reminder” while background tasks or crons are pending; pending idle_prompt no longer flips “Needs input”. Pane shows localized Running and publishes .running lifecycle; persist .running runtimeStatus for suppressed waiting cues.
    • Antigravity: skip intermediate turn‑complete before dedupe; idle‑reminder tags include the pending bit; pending waiting cues keep Running. Validate notifications.agentTurnComplete from cmux.json against whenIdle|always|never.

Written for commit 5d4971d. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added notification controls for agent permission prompts, “agent turn complete” timing (whenIdle/always/never), and idle reminders.
    • Agent notifications now carry optional categorization metadata so delivery can be tailored to settings.
  • Bug Fixes
    • Idle reminders are suppressed when a stop indicates unfinished background work remains pending (with legacy behavior unchanged when metadata is missing).
  • Documentation
    • Updated UI and web configuration text/schema (English and Japanese) for the new settings.
  • Tests
    • Added coverage for notification gating and pending-background-work stop behavior.

cmux fired a "Completed" notification on every Claude turn end, even when
the turn was intermediate (a background build, a Monitor, or a scheduled
wakeup was still running and would re-invoke the agent). Verified against
claude 2.1.197: the Stop hook payload carries background_tasks and
session_crons; the Notification hook carries notification_type
(permission_prompt vs idle_prompt) but not background_tasks, and idle_prompt
fires ~60s after an intermediate turn even while a task runs.

Mechanism: the CLI now classifies each agent notification and forwards the
signal to the app as an optional c=<category>;p=<0|1> meta segment appended
to the notify_target_async payload. The app gates delivery by user config
(NotificationsCatalogSection), so the runtime signal (CLI-side) and the
policy (app-side, where settings live) stay cleanly separated.

- CLI: hasActiveClaudeBackgroundWork(_:) reads background_tasks[].status ==
  "running" or non-empty session_crons (nil/absent => not pending, so
  claude < 2.1.145 behaves as before). Stop caches
  hadPendingBackgroundWorkAtStop on the session record and tags
  c=turn-complete;p=<pending>. Notification tags needs-permission /
  idle-reminder; idle-reminder reads the cache because its payload lacks
  background_tasks.
- App: parseNotificationPayload gains an optional 4th meta segment, treated
  as meta only when it begins with c= (else folded back into the body, so
  legacy callers whose body contains | parse byte-identically).
  AgentNotificationGate decides delivery; gated in notifyTargetQueued.
- Settings: notifications.agentPermissionPrompt (default on),
  agentTurnComplete (whenIdle default | always | never),
  agentIdleReminder (default on). Settings UI rows, cmux.json bridge +
  schema, en/ja/ko/uk localization.
- Tests: ClaudeBackgroundWorkNotifyTests (CLI meta + cache) and
  AgentNotificationGateTests (decision table + meta parser).

Principled fix: one shared background-work predicate, one CLI->app signal,
policy centralized app-side. The same predicate is what the hibernation
effort needs to avoid hibernating panes with live background work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 1, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Canceled Canceled Jul 2, 2026 12:18pm
cmux-staging Building Building Preview, Comment Jul 2, 2026 12:18pm

@coderabbitai

coderabbitai Bot commented Jul 1, 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

Adds cached background-work state to Claude stops, tags notification payloads with category and pending metadata, and uses new agent notification settings to gate delivery. The change also adds matching settings UI, schema/localization entries, and CLI and unit tests.

Changes

Agent Notification Gating

Layer / File(s) Summary
Session record pending-work caching
CLI/cmux.swift
ClaudeHookActiveSessionRecord gains hadPendingBackgroundWorkAtStop, threaded through initializers and upsert mapping, and set conditionally on Stop.
Notification classification and payload meta encoding
CLI/cmux.swift
Classifies notification_type into categories, computes pending via cached flag and background-work detection, and encodes meta (`c=;p=<0
Notification gating model and delivery
Sources/TerminalController.swift, Sources/AgentNotificationGate.swift
Adds category/mode/meta parsing and shouldDeliver; parseNotificationPayload returns a 4-tuple, and queued delivery is gated by the new settings.
Agent notification settings keys and UI
Packages/macOS/CmuxSettings/.../NotificationsCatalogSection.swift, Packages/macOS/CmuxSettingsUI/.../AppSection.swift
New catalog keys for permission prompt, turn-complete mode, and idle reminder; wired into AppSection state, init, observation, and new UI rows.
Settings JSON path mapping and schema/localization
Sources/CmuxSettingsJSONPathSupport.swift, web/data/cmux.schema.json, web/messages/en.json, web/messages/ja.json, Resources/Localizable.xcstrings
Extends file mapping and allowlist, JSON schema properties, and localized description strings for the three settings.
Gate and background-work notification tests
cmuxTests/AgentNotificationGateTests.swift, cmuxTests/ClaudeBackgroundWorkNotifyTests.swift, cmux.xcodeproj/project.pbxproj
New test suites validate gating decisions, meta parsing, and end-to-end CLI notification tagging/caching behavior; registers new test files and gate source in the Xcode project.

Estimated code review effort: 4 (Complex) | ~60 minutes


Important

Pre-merge checks failed

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

❌ Failed checks (3 errors, 2 warnings)

Check name Status Explanation Resolution
Cmux Swift File And Package Boundaries ❌ Error Sources/AgentNotificationGate.swift is a pure 71-line, unit-testable policy/parser in the app target; the existing CmuxNotifications package is the right boundary. Move AgentNotifyCategory/AgentTurnCompleteMode/AgentNotificationMeta/AgentNotificationGate into Packages/macOS/CmuxNotifications and import it from TerminalController.
Cmux Full Internationalization ❌ Error New xcstrings keys only cover en/ja/ko/uk while the catalog supports 20 locales, and the new web notification copy exists only in en/ja, leaving the other routing locales untranslated. Add translations for the new xcstrings keys in every existing catalog locale (ar, bs, da, de, es, fr, it, km, nb, pl, pt-BR, ru, th, tr, zh-Hans, zh-Hant, etc.) and mirror the new web keys across every web/messages locale file.
Cmux No Ambient Global State ❌ Error Sources/AgentNotificationGate.swift adds a caseless enum namespace with only a static API (AgentNotificationGate.shouldDeliver), which the rule forbids. Move the delivery decision into a constructable/injectable type or a private/fileprivate helper instead of a new static namespace enum.
Docstring Coverage ⚠️ Warning Docstring coverage is 8.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description is detailed, but it does not follow the required template and is missing Testing, Demo Video, Review Trigger, and Checklist sections. Add the template sections with testing details, a demo video/link for the UI changes, the review-trigger comment block, and the checklist items.
✅ Passed checks (20 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: gating agent notifications with background-work and per-category settings.
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 No new actor-isolation debt: UI/settings changes stay on @MainActor, pure helpers are value-only, and no shared mutable Sendable refs or background store access were introduced.
Cmux Swift Blocking Runtime ✅ Passed Touched production code only adds metadata parsing/gating and cached state; no new semaphores, waits, sleeps, syncs, or locks in the changed paths. Test waits are allowed.
Cmux Browser Automation Off-Main ✅ Passed PR changes notification plumbing/settings only; no browser.* routing or ControlCommandExecutionPolicy changes were introduced in the touched files.
Cmux Expensive Synchronous Load ✅ Passed PASS: The PR adds only in-memory meta/pending checks; no new synchronous agent-history loads, JSON/transcript scans, or RestorableAgentSessionIndex.load() calls appear in the changed paths.
Cmux Cache Substitution Correctness ✅ Passed PASS: the cache is written from each fresh Stop payload, lookup reads it fresh from disk, and nil/legacy states default to false; no stale opportunistic read in a persistence path.
Cmux No Hacky Sleeps ✅ Passed No changed TS/JS/shell/runtime-script files were touched; the diff is Swift, JSON/localization, and project metadata only, so no hacky-sleep violation exists.
Cmux Algorithmic Complexity ✅ Passed New notification logic only does single-pass checks over small hook payload/meta fields and O(1) session-store updates; no nested rescans or repeated sort/filter on scalable collections.
Cmux Swift Concurrency ✅ Passed New notification helpers are pure sync functions; added UI uses @State/SwiftUI only, with no new DispatchQueue/Combine/completion-handler APIs in the diff.
Cmux Swift @Concurrent ✅ Passed No new async/nonisolated/@Concurrent mismatch: added notification gating helpers are synchronous/pure, and UI-side delivery stays on @MainActor.
Cmux Swiftpm Lockfiles ✅ Passed No cmux-owned .gitignore ignores Package.resolved; the only ignore is vendored vendor/bonsplit. The PR’s pbxproj edits are source-file registrations, not package-reference changes.
Cmux Swift Logging ✅ Passed PASS: The PR diff adds no prohibited logging in runtime Swift; no new print/debugPrint/dump/NSLog/Logger calls appear in changed production files, and tests are allowed.
Cmux User-Facing Error Privacy ✅ Passed PASS: The new user-facing text is generic settings/help copy and notification labels; no added error/alert output exposes raw upstream messages or internal provider data.
Cmux Swiftui State Layout ✅ Passed AppSection only adds more DefaultsValueModel @State fields to an existing settings view; no new ObservableObject/@published, GeometryReader, lazy-row store refs, or render-time mutation.
Cmux Architecture Rethink ✅ Passed Adds a required payload bridge and caches pending-at-stop in the existing session store; no new timing, polling, observer, or split-owner workaround introduced.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR only changes notifications/settings/terminal parsing; no NSWindow/WindowGroup or cmuxAuxiliaryWindowIdentifiers changes appear in the touched Swift files.
Cmux Source Artifacts ✅ Passed Changed paths are intentional source/config/localization/test assets (Swift, JSON, xcstrings, pbxproj), with no temp/build/cache/artifact paths.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No changed file under **/Sources/** in this PR diff; only CLI/cmux.swift and tests were modified, so no production test/debug seam was added.
✨ 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 feat-agent-notify-gating

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/cmux.swift

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ae4203a24a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

#expect(handled.wait(timeout: .now() + 5) == .success)
harness.assertSuccessfulHook(result)
let snapshot = context.state.snapshot()
context.cleanup()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the hook store before cache assertions

The tests that call runStopHook read cachedPending(storeURL, ...) after the helper returns, but ClaudeHookContext.cleanup() removes root, and storeURL is inside that root. As a result those cache assertions always see nil even when the CLI wrote hadPendingBackgroundWorkAtStop, so the newly added tests fail; defer cleanup until after the caller reads the store or read the cached value before deleting the fixture directory.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in be4cf8c: the helper now reads the cached value from the store before cleanup() deletes the fixture dir, and returns it to the caller. Verified on the AWS M4 Pro runner (all suite tests pass).

— Claude Code

@greptile-apps

greptile-apps Bot commented Jul 1, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Gates "Agent Finished" and "Agent Waiting for Input" notifications on whether background tasks / scheduled crons are still running when a Claude turn ends, and adds per-category notification settings (agentPermissionPrompt, agentTurnComplete, agentIdleReminder). The CLI appends a c=<category>;p=<0|1> meta segment to notify_target_async payloads; the app parses it and applies AgentNotificationGate before enqueueing.

  • CLI (CLI/cmux.swift): adds hasActiveClaudeBackgroundWork, notifyMeta, and hadPendingBackgroundWorkAtStop cache; keeps pane lifecycle .running while background work is live; tags Stop and Notification payloads for Claude, Grok, and Antigravity.
  • App (Sources/TerminalController.swift, Sources/AgentNotificationGate.swift): strict 4th-segment meta parser accepts only the three known category literals; agentNotificationShouldDeliver is a pure nonisolated free function with exhaustive unit-test coverage of the decision table.
  • Settings: three new DefaultsKeys, Settings UI rows (two toggles + an enum picker), cmux.json/schema bridge, and Localizable.xcstrings entries for en/ja/ko/uk.

Confidence Score: 4/5

Safe to merge with one small correctness fix noted; legacy behavior (untagged payloads always deliver) is fully preserved for non-cmux callers and older claude clients.

The fallback classification for pre-2.1.145 claude clients switches on a localized subtitle string to derive the notification category, but the xcstrings catalog already provides Japanese translations for those exact keys. In a Japanese-locale app the switch always falls through to .other, meaning agentTurnComplete=never and agentIdleReminder=false have no effect on old-client notifications. summary.notifyCategory is directly available and makes the fallback both simpler and reliable. All other paths are correct and well-tested.

The fallback subtitle-switch in the Claude notification hook (~line 23570 of CLI/cmux.swift) is the single place that needs attention.

Important Files Changed

Filename Overview
CLI/cmux.swift Core CLI hook changes: adds hasActiveClaudeBackgroundWork, notifyMeta, hadPendingBackgroundWorkAtStop cache, and per-category meta tagging for Stop/Notification/Antigravity paths. One reliability nit: the notification fallback for old clients switches on a localized subtitle string instead of summary.notifyCategory (already computed), which breaks category gating in Japanese locale.
Sources/AgentNotificationGate.swift New file: pure decision function agentNotificationShouldDeliver + AgentNotificationMeta parser. Strict wire grammar (rejects .other, requires exact two-field form), correct decision table, cleanly nonisolated. No issues.
Sources/TerminalController.swift Adds optional 4th meta segment to parseNotificationPayload and gates all four notify command paths via shouldDeliverAgentNotification. The fold-back logic for non-meta 4th segments preserves legacy byte-identical behavior correctly.
Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/NotificationsCatalogSection.swift Adds three new DefaultsKeys (agentPermissionPrompt, agentTurnComplete, agentIdleReminder) with correct defaults matching the decision table in the PR description.
Sources/KeyboardShortcutSettingsFileStore.swift Wires agentPermissionPrompt/agentIdleReminder as booleanSettings and validates agentTurnComplete against AgentTurnCompleteMode enum values before applying. Correct pattern matching the existing sound validation.
Resources/Localizable.xcstrings Adds 10 new string keys for the three new settings rows (title, subtitle, picker options) with en/ja/ko/uk translations. App string catalog is complete for the four locales it already supported.
cmuxTests/AgentNotificationGateTests.swift 8 unit tests covering the full decision table (all categories x pending/settings combos) and the strict meta parser grammar. Good coverage of corner cases (reordered fields, duplicate keys, trailing semicolons).
cmuxTests/ClaudeBackgroundWorkNotifyTests.swift 7 behavioral integration tests driving the real CLI against a mock socket server. Covers running/empty/cron background tasks, old-client (absent arrays), permission_prompt, no-type cue fallback, and the stop-to-idle_prompt cache propagation chain.
web/messages/en.json Adds three new schema description keys for agentPermissionPrompt, agentTurnComplete, agentIdleReminder in English. The matching translations are missing from the other 18 locale files in web/messages/ (previously noted).

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant C as Claude CLI
    participant CH as cmux CLI Hook
    participant SS as Session Store
    participant APP as App TerminalController
    participant GATE as DeliveryGate
    participant NS as Notification System

    C->>CH: Stop hook
    CH->>CH: hasActiveClaudeBackgroundWork
    CH->>SS: cache hadPendingBackgroundWorkAtStop
    CH->>APP: notify_target_async with meta segment

    Note over C,CH: ~60s later
    C->>CH: Notification idle_prompt
    CH->>SS: read hadPendingBackgroundWorkAtStop
    CH->>APP: "notify_target_async with c=idle-reminder p=cached"

    APP->>APP: parseNotificationPayload extracts meta
    APP->>GATE: agentNotificationShouldDeliver
    alt delivers
        GATE-->>APP: true
        APP->>NS: enqueue notification
    else gated by settings
        GATE-->>APP: false
        APP-->>C: OK dropped
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant C as Claude CLI
    participant CH as cmux CLI Hook
    participant SS as Session Store
    participant APP as App TerminalController
    participant GATE as DeliveryGate
    participant NS as Notification System

    C->>CH: Stop hook
    CH->>CH: hasActiveClaudeBackgroundWork
    CH->>SS: cache hadPendingBackgroundWorkAtStop
    CH->>APP: notify_target_async with meta segment

    Note over C,CH: ~60s later
    C->>CH: Notification idle_prompt
    CH->>SS: read hadPendingBackgroundWorkAtStop
    CH->>APP: "notify_target_async with c=idle-reminder p=cached"

    APP->>APP: parseNotificationPayload extracts meta
    APP->>GATE: agentNotificationShouldDeliver
    alt delivers
        GATE-->>APP: true
        APP->>NS: enqueue notification
    else gated by settings
        GATE-->>APP: false
        APP-->>C: OK dropped
    end
Loading

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

Comment thread Sources/TerminalController.swift Outdated
Comment on lines +82 to +107
enum AgentNotificationGate {
static func shouldDeliver(
category: AgentNotifyCategory,
pending: Bool,
permissionEnabled: Bool,
turnMode: AgentTurnCompleteMode,
idleEnabled: Bool
) -> Bool {
switch category {
case .needsPermission:
return permissionEnabled
case .turnComplete:
switch turnMode {
case .always: return true
case .never: return false
case .whenIdle: return !pending
}
case .idleReminder:
return idleEnabled && !pending
case .other:
// Legacy/uncategorized (codex, grok, antigravity, pre-meta clients):
// deliver exactly as before.
return true
}
}
}

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.

P2 Caseless enum used as a static-function namespace

AgentNotificationGate is a caseless enum whose entire API is a single static func shouldDeliver(...). The cmux-no-ambient-global-state rule explicitly flags "a caseless enum used purely as a static func/static let namespace." The logic has no state, so the idiomatic fix is to promote shouldDeliver to a free nonisolated function (or a method on TerminalController) and drop the namespace type entirely — the test target can still call it via @testable import.

Rule Used: Flag new ambient global state in production Swift:... (source)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in ca6c3cd: replaced the caseless AgentNotificationGate enum with a free nonisolated func agentNotificationShouldDeliver(...) per the no-static-namespace policy. Tests call it directly via @testable import.

— Claude Code

- AgentNotificationGateTests imported only `cmux`; the app-target types
  live in the `cmux_DEV` test-host module, so the bundle failed to compile
  and every unit-test shard failed. Import both, matching every other test.
- Move AgentNotifyCategory / AgentTurnCompleteMode / AgentNotificationMeta /
  AgentNotificationGate out of the 14k-line TerminalController.swift into
  Sources/AgentNotificationGate.swift (pure, better isolated, shrinks the
  file's growth). Wired into the app target.
- Refresh .github/swift-file-length-budget.tsv for the three files that grew
  (CLI/cmux.swift, TerminalController.swift, AppSection.swift).

Co-Authored-By: Claude Opus 4.8 <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.

Actionable comments posted: 3

🤖 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 `@cmuxTests/ClaudeBackgroundWorkNotifyTests.swift`:
- Around line 16-48: runStopHook currently returns storeURL after
context.cleanup() deletes context.root, so callers end up with a stale path and
the cachedPending assertions miss the file. Update runStopHook in
ClaudeBackgroundWorkNotifyTests to read the cached contents before cleanup or
return the cached value instead of the URL, using the existing helpers around
context.state.snapshot() and context.cleanup() to preserve a valid reference for
the later assertions.

In `@Sources/TerminalController.swift`:
- Line 11604: The metadata gate is missing from the metadata-aware notification
paths in `deliverNotificationSynchronously` callers that parse
`parseNotificationPayload(args)` but ignore `meta`. Update the shared delivery
flow used by the `notify_current`, `notify_surface`, and `notify_target`
handlers so `meta` is either validated against the new user setting or
explicitly rejected before calling `deliverNotificationSynchronously`. Prefer
centralizing this check in the common notification delivery logic rather than
duplicating it in each caller.
- Around line 62-76: The metadata parser in `AgentNotify.init?(meta:)` is
currently failing open because `parsedPending` defaults to false, so tagged
metadata without a valid pending flag can still create a notification. Update
`init?(meta:)` to track whether the `p` field was actually present and only
accept it when its value is exactly `0` or `1`; if `p` is missing or malformed,
return nil instead of constructing the notification. Keep the existing category
parsing, but make delivery depend on a valid pending signal so
`TerminalController` fails closed for tagged metadata.
🪄 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: 392d2b63-49e1-43f0-8cdc-b351ed0e6fea

📥 Commits

Reviewing files that changed from the base of the PR and between 2313855 and ae4203a.

📒 Files selected for processing (12)
  • CLI/cmux.swift
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/NotificationsCatalogSection.swift
  • Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift
  • Resources/Localizable.xcstrings
  • Sources/CmuxSettingsJSONPathSupport.swift
  • Sources/TerminalController.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/AgentNotificationGateTests.swift
  • cmuxTests/ClaudeBackgroundWorkNotifyTests.swift
  • web/data/cmux.schema.json
  • web/messages/en.json
  • web/messages/ja.json

Comment thread cmuxTests/ClaudeBackgroundWorkNotifyTests.swift
Comment thread Sources/TerminalController.swift Outdated
Comment thread Sources/TerminalController.swift Outdated

@cubic-dev-ai cubic-dev-ai 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.

4 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift">

<violation number="1" location="Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift:570">
P2: Add search/anchor entries for these new notification rows. As written, their configurationReview paths do not resolve through SettingsSearchIndex, so the new settings are not reachable/highlightable from settings search and the row-anchor contract drifts.</violation>
</file>

<file name="cmuxTests/ClaudeBackgroundWorkNotifyTests.swift">

<violation number="1" location="cmuxTests/ClaudeBackgroundWorkNotifyTests.swift:10">
P3: Use XCTest instead of Swift Testing for tests that spawn the CLI process/socket harness per team guidance. Swift Testing is reserved for pure decision/unit tests.</violation>

<violation number="2" location="cmuxTests/ClaudeBackgroundWorkNotifyTests.swift:194">
P3: Discard the stop result in `idlePromptAfterIdleStopTagsNotPending`. A silent stop-hook failure (non-zero exit) would go undetected, and the notification assertion could pass or fail for the wrong reason. Assert it like the sibling test does.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread cmuxTests/ClaudeBackgroundWorkNotifyTests.swift

// Agent: Needs Permission
SettingsCardRow(
configurationReview: .json("notifications.agentPermissionPrompt"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Add search/anchor entries for these new notification rows. As written, their configurationReview paths do not resolve through SettingsSearchIndex, so the new settings are not reachable/highlightable from settings search and the row-anchor contract drifts.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift, line 570:

<comment>Add search/anchor entries for these new notification rows. As written, their configurationReview paths do not resolve through SettingsSearchIndex, so the new settings are not reachable/highlightable from settings search and the row-anchor contract drifts.</comment>

<file context>
@@ -557,6 +563,46 @@ public struct AppSection: View {
+
+            // Agent: Needs Permission
+            SettingsCardRow(
+                configurationReview: .json("notifications.agentPermissionPrompt"),
+                String(localized: "settings.notifications.agentPermissionPrompt.title", defaultValue: "Agent Needs Permission"),
+                subtitle: String(localized: "settings.notifications.agentPermissionPrompt.subtitle", defaultValue: "Notify when an agent is blocked waiting for your permission to run a tool.")
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in ca6c3cd: added CuratedSettingEntry rows for the three new notification settings (agent-permission-prompt, agent-turn-complete, agent-idle-reminder) with their notifications.* paths as anchor tokens, so they resolve through SettingsSearchIndex and highlight the rows.

— Claude Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks — the curated SettingsSearchIndex entries now cover those three notification rows, so the parent comment is resolved here.

Thanks for the feedback! I've saved this as a new learning to improve future reviews.

/// meta segment, and the `hadPendingBackgroundWorkAtStop` cache the idle_prompt
/// path reads. Drives the real CLI against the mock socket server, exactly like
/// `ClaudeNotificationStatusLifecycleTests`.
@Suite(.serialized)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Use XCTest instead of Swift Testing for tests that spawn the CLI process/socket harness per team guidance. Swift Testing is reserved for pure decision/unit tests.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmuxTests/ClaudeBackgroundWorkNotifyTests.swift, line 10:

<comment>Use XCTest instead of Swift Testing for tests that spawn the CLI process/socket harness per team guidance. Swift Testing is reserved for pure decision/unit tests.</comment>

<file context>
@@ -0,0 +1,214 @@
+/// meta segment, and the `hadPendingBackgroundWorkAtStop` cache the idle_prompt
+/// path reads. Drives the real CLI against the mock socket server, exactly like
+/// `ClaudeNotificationStatusLifecycleTests`.
+@Suite(.serialized)
+struct ClaudeBackgroundWorkNotifyTests {
+    private func notifyLine(_ snapshot: [String], containing needle: String) -> String? {
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Keeping Swift Testing here deliberately: the sibling process-spawning claude-hook suites (e.g. ClaudeNotificationStatusLifecycleTests) already use Swift Testing with this exact harness, so matching the sibling convention keeps one suite style for the claude-hook behavior tests.

— Claude Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The sibling-suite convention point is fair, and the parent comment is too broad for this PR. This ClaudeBackgroundWorkNotifyTests suite matches the existing Swift Testing harness used by the other claude-hook behavior tests, including ClaudeNotificationStatusLifecycleTests.

Comment thread cmuxTests/ClaudeBackgroundWorkNotifyTests.swift Outdated
runStopHook called context.cleanup() (which deletes the temp dir including
claude-hook-sessions.json) before the test read hadPendingBackgroundWorkAtStop,
so the three cache assertions read a deleted file and failed. Capture the
cached value inside the helper before cleanup and return it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: be4cf8ced8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/TerminalController.swift Outdated
Comment on lines +12539 to +12540
if candidate.hasPrefix("c=") {
meta = candidate

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require full meta grammar before stripping body tail

When an existing notify_target_async caller sends a normal body that contains a pipe followed by text starting with c= (for example title|subtitle|result|c=value), this branch treats that tail as metadata and removes it from the delivered body, whereas the previous maxSplits: 2 parser preserved it as part of the body. Since arbitrary socket/CLI notification bodies could already contain |, please only strip the fourth segment after validating the full agent-meta grammar (e.g. category plus pending flag) or otherwise fold it back into the body.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in ca6c3cd: the 4th segment is treated as metadata only when it parses as the FULL c=;p=<0|1> grammar; a body tail like '|c=value' fails the parse (no valid p=) and is folded back into the delivered body. Regression-covered in AgentNotificationGateTests.metaRequiresValidPendingFlag.

— Claude Code

@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)
cmuxTests/ClaudeBackgroundWorkNotifyTests.swift (1)

196-203: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Missing success assertion on the setup Stop hook.

Unlike the sibling test idlePromptAfterPendingStopReadsCachedPending (which calls harness.assertSuccessfulHook(stopResult) at Line 163), this test discards the Stop hook's result with _ = and never verifies it succeeded. If the "idle" Stop hook silently fails, this test would still proceed to assert idle_prompt pending=0 behavior against an unverified/incorrect prior state, potentially masking a real regression.

🧪 Proposed fix
-        _ = harness.runProcess(
+        let stopResult = harness.runProcess(
             executablePath: context.cliPath,
             arguments: ["hooks", "claude", "stop"],
             environment: environment,
             standardInput: #"{"session_id":"\#(session)","cwd":"/tmp/x","hook_event_name":"Stop","last_assistant_message":"ok","background_tasks":[],"session_crons":[]}"#,
             timeout: 5
         )
         `#expect`(handled.wait(timeout: .now() + 5) == .success)
+        harness.assertSuccessfulHook(stopResult)
🤖 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 `@cmuxTests/ClaudeBackgroundWorkNotifyTests.swift` around lines 196 - 203, The
setup Stop hook result is being ignored in ClaudeBackgroundWorkNotifyTests, so
the test never verifies the hook succeeded before asserting idle prompt
behavior. Capture the result from harness.runProcess for the `hooks claude stop`
call and assert it with the same success check used in
`idlePromptAfterPendingStopReadsCachedPending` (for example via
`harness.assertSuccessfulHook(...)`) before waiting on `handled`.
🤖 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 `@cmuxTests/ClaudeBackgroundWorkNotifyTests.swift`:
- Around line 196-203: The setup Stop hook result is being ignored in
ClaudeBackgroundWorkNotifyTests, so the test never verifies the hook succeeded
before asserting idle prompt behavior. Capture the result from
harness.runProcess for the `hooks claude stop` call and assert it with the same
success check used in `idlePromptAfterPendingStopReadsCachedPending` (for
example via `harness.assertSuccessfulHook(...)`) before waiting on `handled`.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 46fc101b-6f7f-42bf-8d12-a4e9b2f61d40

📥 Commits

Reviewing files that changed from the base of the PR and between 8bca48c and be4cf8c.

📒 Files selected for processing (1)
  • cmuxTests/ClaudeBackgroundWorkNotifyTests.swift

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 6 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/AgentNotificationGate.swift">

<violation number="1" location="Sources/AgentNotificationGate.swift:38">
P2: The meta grammar validation is too permissive: `AgentNotificationMeta` returns a valid instance whenever a `c=` field is found (with any unrecognized value falling back to `.other`), without requiring the `p=` (pending) field. This means a legitimate notification body segment like `c=value` would be incorrectly recognized as agent metadata and stripped from the delivered body. Consider requiring both the `c=` and `p=` fields to be present before treating the segment as agent meta, or restrict accepted category values to known cases only (returning `nil` instead of `.other` for unknown values).</violation>

<violation number="2" location="Sources/AgentNotificationGate.swift:46">
P3: Avoid adding `AgentNotificationGate` as a caseless static namespace; make the gate a constructable service owned/injected by the notification path instead of another ambient global API.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread Sources/AgentNotificationGate.swift Outdated
Comment thread Sources/AgentNotificationGate.swift Outdated
The unconditional '@testable import cmux' failed under the Debug cmux-unit
build where the app module is cmux_DEV, not cmux, so the test bundle didn't
compile. Match the repo pattern: #if canImport(cmux_DEV) ... #elseif
canImport(cmux). Caught on the AWS M4 Pro runner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…und work pending

The stop handler set the pane pill to 'Idle' on every turn end, which is
misleading when a background task or Monitor is still running. Reuse the
hasPendingBackgroundWork signal: pending -> 'Running' (bolt), truly-idle ->
'Idle' (unchanged). Hibernation lifecycle (set_agent_lifecycle) is left to
its own PR; this only fixes the visible status pill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread Sources/TerminalController.swift

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9243b82f55

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread CLI/cmux.swift
Comment on lines +23530 to +23532
case "idle_prompt":
notifyCategory = .idleReminder
notifyPending = (mappedSession?.hadPendingBackgroundWorkAtStop == true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Ignore pending idle prompts before changing status

When this idle_prompt path reads a cached pending Stop, the payload is tagged c=idle-reminder;p=1 so the app suppresses the banner, but the hook still continues below to upsert agentLifecycle: .needsInput and send set_status ... Needs input. Because Claude emits idle prompts while background tasks/Monitors are still running, the sidebar/lifecycle flips from the new pending-work Running state to a false waiting-for-input state even though the notification was gated; skip the visible needs-input mutation (or keep running) when notifyPending is true.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in ca6c3cd: a pending idle_prompt (cached background work) no longer upserts .needsInput or sets the 'Needs input' pill; the pane stays Running and the next authoritative Stop reconciles. A genuine (not-pending) idle prompt still flips the pill. Covered by tests.

— Claude Code

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift">

<violation number="1" location="Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift:570">
P2: Add search/anchor entries for these new notification rows. As written, their configurationReview paths do not resolve through SettingsSearchIndex, so the new settings are not reachable/highlightable from settings search and the row-anchor contract drifts.</violation>
</file>

<file name="cmuxTests/ClaudeBackgroundWorkNotifyTests.swift">

<violation number="1" location="cmuxTests/ClaudeBackgroundWorkNotifyTests.swift:10">
P3: Use XCTest instead of Swift Testing for tests that spawn the CLI process/socket harness per team guidance. Swift Testing is reserved for pure decision/unit tests.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

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.

Caution

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

⚠️ Outside diff range comments (2)
cmuxTests/ClaudeBackgroundWorkNotifyTests.swift (1)

189-227: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing hook-success assertion on the precondition stop call.

The initial stop invocation's result is discarded with _ = and never passed to harness.assertSuccessfulHook(...), unlike the analogous precondition step in idlePromptAfterPendingStopReadsCachedPending (Line 174: harness.assertSuccessfulHook(stopResult)). If the stop hook itself fails silently, this test would only surface a downstream failure on the idle_prompt notification assertion, making the root cause harder to diagnose.

🧪 Proposed fix
-        _ = harness.runProcess(
+        let stopResult = harness.runProcess(
             executablePath: context.cliPath,
             arguments: ["hooks", "claude", "stop"],
             environment: environment,
             standardInput: #"{"session_id":"\#(session)","cwd":"/tmp/x","hook_event_name":"Stop","last_assistant_message":"ok","background_tasks":[],"session_crons":[]}"#,
             timeout: 5
         )
         `#expect`(handled.wait(timeout: .now() + 5) == .success)
+        harness.assertSuccessfulHook(stopResult)
🤖 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 `@cmuxTests/ClaudeBackgroundWorkNotifyTests.swift` around lines 189 - 227, The
precondition stop hook result is being ignored in
idlePromptAfterIdleStopTagsNotPending, so a failing stop would be hidden until
later assertions. Capture the result of the hooks claude stop call in a named
variable and pass it to harness.assertSuccessfulHook, matching the pattern used
in idlePromptAfterPendingStopReadsCachedPending, so the test fails at the
correct step if stop is unsuccessful.
CLI/cmux.swift (1)

23229-23306: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Thread pending work into hibernation state

Stop still records .idle when hasPendingBackgroundWork is true. That flag only feeds the idle_prompt notification gate; hibernation reads agentLifecycle, so a session with live background work can still become eligible for suspension once idleSeconds elapse. Thread the pending-work state into the lifecycle path too.

🤖 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 23229 - 23306, The stop-handling path in
cmux.swift still writes `.idle` via the session update and lifecycle setter even
when `hasPendingBackgroundWork` is true, so background work is not reflected in
the hibernation eligibility state. Update the Claude hook stop flow around
`summarizeClaudeHookStop`, `sessionStore.upsert`, and `setAgentLifecycle` so
pending work is threaded into the persisted lifecycle, not just
`hadPendingBackgroundWorkAtStop`, and make the status/notification logic derive
from that shared state.
🤖 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 `@CLI/cmux.swift`:
- Around line 23229-23306: The stop-handling path in cmux.swift still writes
`.idle` via the session update and lifecycle setter even when
`hasPendingBackgroundWork` is true, so background work is not reflected in the
hibernation eligibility state. Update the Claude hook stop flow around
`summarizeClaudeHookStop`, `sessionStore.upsert`, and `setAgentLifecycle` so
pending work is threaded into the persisted lifecycle, not just
`hadPendingBackgroundWorkAtStop`, and make the status/notification logic derive
from that shared state.

In `@cmuxTests/ClaudeBackgroundWorkNotifyTests.swift`:
- Around line 189-227: The precondition stop hook result is being ignored in
idlePromptAfterIdleStopTagsNotPending, so a failing stop would be hidden until
later assertions. Capture the result of the hooks claude stop call in a named
variable and pass it to harness.assertSuccessfulHook, matching the pattern used
in idlePromptAfterPendingStopReadsCachedPending, so the test fails at the
correct step if stop is unsuccessful.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ae82f01d-0d6e-4528-a50d-db618548fc6d

📥 Commits

Reviewing files that changed from the base of the PR and between 4c8e97a and 9243b82.

📒 Files selected for processing (2)
  • CLI/cmux.swift
  • cmuxTests/ClaudeBackgroundWorkNotifyTests.swift

# Conflicts:
#	.github/swift-file-length-budget.tsv

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb4f9f462e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread CLI/cmux.swift
icon: "pause.circle.fill",
color: "#8E8E93"
)
if hasPendingBackgroundWork {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep pending Stop lifecycle running

With agent hibernation enabled and a Claude Stop that still has a running background task or pending cron, this branch only changes the visible sidebar status to Running, but the session store and socket lifecycle were already set to .idle just above. The hibernation planner uses the lifecycle state (.idle is the only hibernatable state), not the status text, so after the idle timeout/max-live-terminal threshold it can hibernate a pane while that background work is still active; publish/store .running for the pending path as well.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in dd4b07a (review round 2): the pending stop now publishes lifecycle .running in BOTH the session-store upsert and set_agent_lifecycle, so the hibernation planner cannot select the pane while background work is live. Test-covered (pending stop asserts set_agent_lifecycle claude_code running and no idle).

— Claude Code

…tion gate

- parseNotificationPayload accepts a 4th segment as gating metadata only when
  it parses as the FULL c=<category>;p=<0|1> grammar (AgentNotificationMeta now
  requires a valid p=0|1); anything else folds back into the body, so a legacy
  body tail like "|c=value" is never stripped or misread (Codex P2, cubic P2,
  CodeRabbit fail-closed note resolved by rejecting invalid meta as meta).
- Apply the settings gate on every metadata-aware delivery path (notify,
  notify_surface, notify_target, notify_target_async) via one shared
  shouldDeliverAgentNotification helper (CodeRabbit).
- Replace the caseless AgentNotificationGate enum namespace with a free
  nonisolated agentNotificationShouldDeliver function per the cmux
  no-static-namespace policy (Greptile/cubic).
- idle_prompt with cached pending background work no longer flips the pane to
  "Needs input" (lifecycle + pill): the banner is suppressed app-side and the
  pane is still Running; a genuine idle prompt still flips it (Codex P2, cubic).
- Settings search: curated entries for the three new notification rows so they
  resolve through SettingsSearchIndex (cubic P2).
- Tests: meta grammar requires p=0|1; idle-pending skips Needs input pill;
  idle-not-pending still sets it; assert the stop hook result in the idle test
  (cubic P3).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread CLI/cmux.swift
lawrencecchen and others added 2 commits July 2, 2026 01:38
CmuxSettingsUI's everyCuratedSettingEntryIsReachable failed in
swift-package-tests: the new curated entries' anchors weren't backed by the
rowConfigPaths fixture. Register the three notifications.agent* row paths.
Verified locally: CmuxSettingsUI 78/78, CmuxSettings 230/230.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A fullyIdle=false waiting cue is gated app-side (idle-reminder p=1), but the
generic handler still flipped the session/lifecycle/status to needs-input,
overriding Running while background work was live. Mirror the Claude pending
idle_prompt fix: keep lifecycle .running and skip the needs-input pill; the
fullyIdle turn boundary reconciles.

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c32a893f0e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread CLI/cmux.swift
Comment on lines +23577 to +23579
case "Completed":
notifyCategory = .turnComplete
notifyPending = (mappedSession?.hadPendingBackgroundWorkAtStop == true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid marking typeless completions as needs-input

When a Claude Notification payload has no notification_type and the fallback classifier recognizes it as Completed, this branch tags the alert as turn-complete, but suppressNeedsInputState remains false so the code below still upserts .needsInput and sends the Needs input status. In the exact older/typeless completion case this fallback is meant to support—especially after a pending Stop where the app may suppress the banner—the sidebar/lifecycle flips to waiting-for-input even though the agent is finishing/running rather than blocked on the user.

Useful? React with 👍 / 👎.

lawrencecchen and others added 4 commits July 2, 2026 03:46
The enum-valued setting rode the generic unvalidated string path, so a typo
like 'nevr' persisted and silently fell back to whenIdle. Validate against
AgentTurnCompleteMode before applying and log invalid values, mirroring the
notifications.sound allowlist handling.

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

Complete the round-12 invariant: the store upsert for a fullyIdle=false
waiting cue now records runtimeStatus .running alongside the .running
lifecycle, so stale-idle protection and duplicate-background-idle
suppression keep seeing the live session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	.github/swift-file-length-budget.tsv
@lawrencecchen
lawrencecchen merged commit 128be4c into main Jul 2, 2026
34 of 36 checks passed
@lawrencecchen
lawrencecchen deleted the feat-agent-notify-gating branch July 2, 2026 11:46
austinywang added a commit that referenced this pull request Jul 2, 2026
…esktop-notifications-row-stuck-on-p

Resolves conflicts from #7129 (per-category agent notification settings):
- AppSection.swift: keep BOTH the desktopNotifications permission model
  (this branch) and the agentPermissionPrompt/agentTurnComplete/
  agentIdleReminder rows (#7129) across the @State decls, init, and the
  startSettingsObservation array; the body auto-merged with both row sets.
- .github/swift-file-length-budget.tsv: regenerated via
  scripts/swift_file_length_budget.py --write-budget (never hand-edited).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lawrencecchen added a commit that referenced this pull request Jul 3, 2026
…agents

PR #7129 landed the same claude Stop-lane background-work gate this branch
carries, plus notification category gating. Resolution unifies the two:

- One background-work parser: hasActiveClaudeBackgroundWork now delegates to
  AgentBackgroundWorkStatus(hookObject:) (unit-tested, fail-closed on
  present-but-unreadable payloads; non-terminal statuses count as live).
  Main's inline predicate (exact "running" only, fails open on malformed)
  is replaced. Main's tests only pin running/empty/cron/absent, all
  compatible.
- Stop lane: main's shape kept (hasPendingBackgroundWork, localized
  Running/Idle pill, hadPendingBackgroundWorkAtStop cache).
- Notification lane: union of main's category gating (notifyCategory,
  suppressNeedsInputState) and this branch's blocking gate — lifecycle
  flips to .needsInput only for genuinely blocking prompts
  (summary.isBlocking && !suppressNeedsInputState). Main's unconditional
  .needsInput write would have reintroduced the routine-reminder clobber
  this PR fixes.
- Generic lane: persisted lifecycle composes suppressPendingWaitingState
  (.running) > isBlocking (.needsInput) > completion (.idle) > preserve.
- AgentHookNotificationSummary carries both isBlocking and notifyCategory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
austinywang added a commit that referenced this pull request Jul 8, 2026
Fixes the noise paths behind grok Build notification spam
(#7611):

- Dedupe every notification status, not just .idle: fingerprints are now
  status + a stable FNV-1a body hash (cross-process safe; the session
  store persists between CLI invocations). .idle keeps the whole-turn
  "idle-turn" fingerprint so incidental completion-keyword messages
  cannot re-ding.
- Dedupe identical permission prompts per turn: live capture from Grok
  Build 0.2.91 shows an identical generic
  {"notificationType":"permission_prompt","message":"Tool permission
  requested"} Notification for every tool step, even in auto-approve
  mode where nothing awaits the user, so long tasks ring once per step.
  Identical bodies now dedupe within the turn, prompt-submit re-arms
  delivery for the next turn, and permission prompts with novel content
  still always deliver.
- Make every summary carry a notifyCategory: the "needs your attention"
  fallback, arbitrary-text attention alerts, and the stale-record
  rebuild path now tag c=idle-reminder, so the per-category settings
  from #7129 can silence them. Errors keep the explicit .other
  always-deliver exemption (unchanged wire behavior).
- Preserve dedupe across grok's mid-session SessionStart re-fires
  (auto-continue/restarts) instead of re-arming the completion ding;
  prompt-submit clearing is unchanged.
- Replace the single-slot emitted-fingerprint store with a small
  per-session map (60 min window, 16-entry cap, legacy back-compat) so
  an interleaved notification cannot evict the idle-turn fingerprint.
- Recognize grok's camelCase "notificationType" payload key in the
  classifier signal (captured from real traffic).

The classification/dedupe policy moves to a new pure file,
CLI/AgentHookNotificationPolicy.swift, compiled into both cmux-cli and
cmuxTests (same pattern as FeedEventClassifier), with unit coverage of
the classification table, fingerprint stability, and the app-gate meta
round-trip. Claude lane wire output and antigravity fullyIdle gating
are byte-identical. No new user-facing strings; existing localization
keys move verbatim.

Closes #7611

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
austinywang added a commit that referenced this pull request Jul 8, 2026
* mux: red regression tests for grok hook notification noise

Integration coverage for #7611
driven through the spawn-the-CLI harness against a mock socket, using
payload shapes captured from a live Grok Build 0.2.91 session:

- repeated identical "waiting for input" Notification events must dedupe
  within a turn (currently every repeat delivers a fresh banner + sound)
- repeated identical permission_prompt notifications must dedupe per
  turn: grok emits {"notificationType":"permission_prompt","message":
  "Tool permission requested"} for EVERY tool step, even in auto-approve
  mode where nothing awaits the user, so a 6-step task rings 6 times;
  a prompt-submit (new turn) must re-arm delivery
- distinct permission prompts must each deliver (always-deliver for
  novel approval content is preserved)
- unparseable payloads rebuilt from the stored session record must carry
  gateable c=idle-reminder meta and dedupe (currently untagged, so the
  per-category notification settings cannot silence them)
- a mid-session SessionStart re-fire must not re-arm the completion
  dedupe (currently clearNotificationEmission re-arms the same ding)
- guards: antigravity error notifications stay untagged, incidental
  completion keywords cannot re-ding after the real turn-complete

Tests are committed first and are red on this commit by design; the fix
lands in the follow-up commit (two-commit red/green policy).

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

* mux: dedupe and gate all grok agent-hook notifications

Fixes the noise paths behind grok Build notification spam
(#7611):

- Dedupe every notification status, not just .idle: fingerprints are now
  status + a stable FNV-1a body hash (cross-process safe; the session
  store persists between CLI invocations). .idle keeps the whole-turn
  "idle-turn" fingerprint so incidental completion-keyword messages
  cannot re-ding.
- Dedupe identical permission prompts per turn: live capture from Grok
  Build 0.2.91 shows an identical generic
  {"notificationType":"permission_prompt","message":"Tool permission
  requested"} Notification for every tool step, even in auto-approve
  mode where nothing awaits the user, so long tasks ring once per step.
  Identical bodies now dedupe within the turn, prompt-submit re-arms
  delivery for the next turn, and permission prompts with novel content
  still always deliver.
- Make every summary carry a notifyCategory: the "needs your attention"
  fallback, arbitrary-text attention alerts, and the stale-record
  rebuild path now tag c=idle-reminder, so the per-category settings
  from #7129 can silence them. Errors keep the explicit .other
  always-deliver exemption (unchanged wire behavior).
- Preserve dedupe across grok's mid-session SessionStart re-fires
  (auto-continue/restarts) instead of re-arming the completion ding;
  prompt-submit clearing is unchanged.
- Replace the single-slot emitted-fingerprint store with a small
  per-session map (60 min window, 16-entry cap, legacy back-compat) so
  an interleaved notification cannot evict the idle-turn fingerprint.
- Recognize grok's camelCase "notificationType" payload key in the
  classifier signal (captured from real traffic).

The classification/dedupe policy moves to a new pure file,
CLI/AgentHookNotificationPolicy.swift, compiled into both cmux-cli and
cmuxTests (same pattern as FeedEventClassifier), with unit coverage of
the classification table, fingerprint stability, and the app-gate meta
round-trip. Claude lane wire output and antigravity fullyIdle gating
are byte-identical. No new user-facing strings; existing localization
keys move verbatim.

Closes #7611

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@lawrencecchen
lawrencecchen restored the feat-agent-notify-gating branch July 18, 2026 10:24

This branch was successfully deployed

1 active deployment
Preview – cmux — 5d4971d1 Deployed Jul 2, 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.

1 participant