Skip to content

mux: dedupe and gate all grok agent-hook notifications - #7619

Merged
austinywang merged 2 commits into
mainfrom
issue-7611-grok-notification-noise
Jul 8, 2026
Merged

austinywang merged 2 commits into
mainfrom
issue-7611-grok-notification-noise

Conversation

@austinywang

@austinywang austinywang commented Jul 8, 2026 •

Copy link
Copy Markdown
Contributor

Closes #7611

Problem

Grok Build sessions spam notification sounds. Two report waves, both root-caused:

  1. Only .idle hook notifications were deduped — every repeated Waiting/Error/Attention event delivered a fresh banner + sound; unclassified fallback payloads carried no c=<category>;p=<0|1> meta so the per-category settings from Gate agent notifications on background work + per-category settings #7129 could not silence them; mid-session SessionStart re-fires re-armed the completion ding.
  2. Dogfood follow-up: "long tasks ring on every step." Captured live Grok Build 0.2.91 hook traffic (tee on hook stdin, isolated GROK_HOME): grok emits an identical {"notificationType":"permission_prompt","message":"Tool permission requested"} Notification for every tool step — even in --permission-mode auto where nothing awaits the user. A 6-step task = 6 banners + 6 sounds. Grok has no cmux Feed approval lane (PreToolUse is non-actionable telemetry), so these were classified needsPermission and deliberately exempt from dedupe.

Fix (class-level)

  • Dedupe all statuses: fingerprints are status + FNV-1a(body) (stable across CLI processes — String.hashValue is per-process-randomized and the fingerprint persists in the session store). .idle keeps the whole-turn "idle-turn" fingerprint so incidental completion keywords ("All done…") can't re-ding after the real turn-complete.
  • Permission prompts dedupe per turn: identical permission bodies within a session/window dedupe to one banner; prompt-submit (a real turn boundary) re-arms delivery; permission prompts with novel content always deliver. First prompt of a turn still alerts an away user; the per-step spam dies.
  • Everything gateable: every summary carries a non-optional notifyCategory. The "needs your attention" fallback, arbitrary-text attention alerts, and the stale-record rebuild path tag c=idle-reminder, so "Agent Waiting for Input" silences them. Errors keep the explicit .other always-deliver exemption (wire output unchanged).
  • No re-arm on SessionStart re-fire (grok auto-continue/restarts); other agents unchanged.
  • Multi-fingerprint store: the single-slot emitted-fingerprint record becomes a small per-session map (60 min window, 16-entry cap, legacy back-compat) so an interleaved notification can't evict "idle-turn".
  • Classifier signal now also reads grok's camelCase notificationType key (captured from real traffic).

The classification/dedupe policy moves to a pure new file CLI/AgentHookNotificationPolicy.swift, compiled into both cmux-cli and cmuxTests (same pattern as FeedEventClassifier). CLI/cmux.swift shrinks by 119 lines; no budget TSV changes. Claude-lane wire output and antigravity fullyIdle gating are byte-identical.

Two-commit red/green structure

Commit 1 adds only the integration regression tests (spawn-the-CLI harness, isolated temp HOME/GROK_HOME/state, payload shapes from live capture): repeated-waiting dedupe, per-turn permission dedupe + prompt-submit re-arm, distinct-permission always-deliver guard, gateable fallback rebuild, SessionStart re-fire, antigravity error guard, incidental-keyword guard. Commit 2 turns it green and adds pure policy unit tests. Note: the tests gate treats assertion-only failures as "expected" ((0 unexpected) tolerance), so the commit-1 redness is visible in the shard logs (failing Test Case lines) rather than in the check status.

Verification

  • Dogfooded on a tagged Debug build against the live app socket with isolated GROK_HOME/state (no real hook configs touched), including real Grok Build 0.2.91 sessions: pre-v2, a 6-tool-step auto-mode task delivered 6 permission banners; post-v2 expectation is 1 (re-verified after the rebuild). Synthetic repro matrix: repeated waiting for input → 1 banner + skipDuplicate; {"unparseable":true} after a permission seed → 1 tagged banner + dedupe; with "Agent Waiting for Input" disabled the app logs socket.notifyTargetAsync.gated category=idle-reminder and shows nothing; incidental "All done…" after a real completion → skipDuplicate idle-turn; SessionStart re-fire + same completion → skipDuplicate.
  • python3 scripts/swift_file_length_budget.py, ./scripts/lint-pbxproj-test-wiring.sh, python3 scripts/normalize-pbxproj.py --check all pass; tagged xcodebuild build-for-testing compiles app + test bundles (no local test execution per repo policy).
  • Localization audit: no new user-facing strings; existing Localizable.xcstrings keys move verbatim into the policy file.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Introduced agent hook notification classification to produce consistent status and categories for emitted notifications.
    • Improved deduplication across a session, including correct handling of repeated turn/session-start events.
  • Bug Fixes
    • Refined notification emission metadata and parsing (including notification type recognition) to prevent incorrect re-sends and ensure proper tagging.
  • Tests
    • Added automated coverage for notification classification and deduplication, including end-to-end “Grok noise” scenarios and permission/idle/completion edge cases.

Note

Medium Risk
Changes notification delivery and dedupe for Grok/Antigravity hooks; misclassification or fingerprint bugs could suppress real alerts or reintroduce spam, though behavior is heavily regression-tested.

Overview
Cuts Grok (and shared Antigravity) agent-hook notification spam by moving classification, dedupe, and notify meta into AgentHookNotificationPolicy.swift and tightening hook behavior in cmux.swift.

Dedupe now applies to all notification statuses for eligible agents, not only idle completions: fingerprints use idle-turn for completions and status + FNV-1a(body) for everything else. The session store keeps a multi-fingerprint map (with legacy single-slot fallback) so interleaved events do not drop the completion fingerprint. Grok skips clearing dedupe on mid-session SessionStart re-fires; prompt-submit still clears for a new turn.

User settings can gate more alerts: summaries always carry a notifyCategory; attention/fallback and stale-record rebuilds tag c=idle-reminder, while errors stay untagged via .other. Wire meta is built through metaSegment(pending:) instead of ad-hoc helpers.

Adds unit (AgentHookNotificationPolicyTests) and CLI integration (CLIGrokNotificationNoiseTests) coverage for repeated waiting, permission dedupe per turn, SessionStart re-fire, gateable fallbacks, and incidental completion cues.

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

@vercel

vercel Bot commented Jul 8, 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 8, 2026 11:25pm
cmux-staging Building Building Preview, Comment Jul 8, 2026 11:25pm

@coderabbitai

coderabbitai Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds notification classification and deduplication policy code, updates the hooks CLI to use it for Grok and antigravity notifications, adds regression tests, and wires the new Swift files into the Xcode project.

Changes

Grok notification classification and dedupe

Layer / File(s) Summary
Policy types and classification
CLI/AgentHookNotificationPolicy.swift
Defines notification status/category types, the summary container, classifier heuristics, and stable dedupe policy helpers.
Hooks CLI notification integration
CLI/cmux.swift
Uses the new classifier and category meta segments, expands dedupe tracking, and updates Grok/antigravity notification emission paths.
Policy and Grok noise tests
cmuxTests/AgentHookNotificationPolicyTests.swift, cmuxTests/CLIGrokNotificationNoiseTests.swift
Adds unit coverage for classification, fingerprints, meta round-trips, and delivery gating, plus integration coverage through the mock hooks pipeline.
Xcode project wiring
cmux.xcodeproj/project.pbxproj
Adds the new policy and test files to the project, groups, and build phases.

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

Possibly related PRs

  • manaflow-ai/cmux#4225: Implements the Grok notification routing and hook flow that this PR extends with classification and dedupe policy changes.
  • manaflow-ai/cmux#7129: Updates the same notification metadata shape used here for category and pending-state gating.

Important

Pre-merge checks failed

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

❌ Failed checks (2 errors)

Check name Status Explanation Resolution
Cmux User-Facing Error Privacy ❌ Error New notification copy forwards raw agent messages and interpolates built-in names like "Grok"/"Antigravity" into user-visible alerts. Use agent-neutral, sanitized fallback text; keep raw upstream messages and provider names out of notify_target_async payloads and logs only.
Cmux No Ambient Global State ❌ Error FAIL: CLI/AgentHookNotificationPolicy.swift adds caseless enum namespaces (AgentHookNotificationClassifier/Policy) with only static funcs/lets, which the rule forbids. Move the classifier/policy behavior onto an instantiable type, or make the helpers private file-scope functions; keep only the data enums/structs.
✅ Passed checks (23 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes align with #7611 by deduping Grok statuses, tagging notifications for gating, and adding regression tests.
Out of Scope Changes check ✅ Passed The changes stay focused on Grok notification policy, CLI wiring, and tests, with no obvious unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Cmux Swift Actor Isolation ✅ Passed The new production types are plain module-scope values/helpers; no new @MainActor leakage, Sendable reference sharing, or UI-store cross-actor access was introduced.
Cmux Swift Blocking Runtime ✅ Passed No new blocking/sleep/sync primitives in production Swift; the added waits are confined to test scaffolding, which the rule allows.
Cmux Browser Automation Off-Main ✅ Passed PR only touches Grok notification policy/tests and pbxproj; it doesn't modify browser automation routing files or any browser.* worker/main-actor paths.
Cmux Expensive Synchronous Load ✅ Passed Diff only refactors notification dedupe/category logic and adds tests; no new main-actor or interactive agent-history load calls were introduced.
Cmux Cache Substitution Correctness ✅ Passed The new fingerprint store is disk-backed and freshness-checked, with cold-cache fallbacks and pruning; no fresh authoritative read was replaced by an unsafe cache.
Cmux No Hacky Sleeps ✅ Passed Diff is Swift-only plus pbxproj wiring; no new JS/shell/build scripts or fixed sleeps/polling hacks were added.
Cmux Algorithmic Complexity ✅ Passed PASS: the only new sort/filter work is on a capped 16-entry per-session map; token scans are on single notification strings, not scalable collections.
Cmux Swift Concurrency ✅ Passed No new legacy async patterns were added; the diff is pure notification/classification logic plus XCTest harness code, with no added DispatchQueue/Task/Combine/handler APIs.
Cmux Swift @Concurrent ✅ Passed Touched Swift files add no async functions or @concurrent annotations; new helpers are synchronous, so the concurrent-annotation rule isn’t violated.
Cmux Swift File And Package Boundaries ✅ Passed New production file is 225 lines of pure notification policy/classifier logic, not oversized or mixed, and cmux.swift shrank while tests cover it—fits the focused-extraction allowance.
Cmux Swiftpm Lockfiles ✅ Passed Diff only adds Swift files and Xcode file refs; no .gitignore or Package.resolved changes, and no SwiftPM package-reference edits.
Cmux Swift Logging ✅ Passed No new production logging APIs or ad hoc diagnostics were introduced in the changed Swift files; the diff only refactors notification policy/test logic.
Cmux Full Internationalization ✅ Passed PASS: The PR reuses preexisting localized keys; no new user-facing Swift copy or catalog/Info.plist entries were added, and the touched strings already have translations.
Cmux Swiftui State Layout ✅ Passed PASS: The diff only adds CLI/test notification policy code; no SwiftUI views, @Observable/@published, GeometryReader, lazy row stores, or render-time state writes appear.
Cmux Architecture Rethink ✅ Passed Pure policy extraction plus tests; dedupe state stays in the existing session store, and no sleeps/observers/duplicate wiring were added.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR only adds CLI policy/tests and pbxproj wiring; no user-visible NSWindow/WindowGroup code or cmuxAuxiliaryWindowIdentifiers changes were introduced.
Cmux Source Artifacts ✅ Passed Changed paths are intentional source/test/config files under CLI, cmuxTests, and xcodeproj; no logs, tmp, cache, build, or artifact dirs appear.
Cmux No Test Or Debug Seam In Production Source ✅ Passed PR only changes CLI/ and Tests/ files; no **/Sources/** production file gained a debug/test seam or wrapper accessor.
Title check ✅ Passed The title clearly matches the main change: deduping and gating Grok agent-hook notifications.
Description check ✅ Passed The description covers the problem, fix, and verification well, but it omits the template's demo video, review trigger, and checklist sections.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-7611-grok-notification-noise

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.

@greptile-apps

greptile-apps Bot commented Jul 8, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR updates Grok agent-hook notification handling to reduce duplicate alerts. The main changes are:

  • Shared notification classification and dedupe policy in a new CLI file.
  • Per-session multi-fingerprint notification tracking.
  • Gateable metadata for fallback and attention notifications.
  • Preserved Grok dedupe state across repeated session starts.
  • Unit and integration tests for the notification policy.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
CLI/AgentHookNotificationPolicy.swift Adds shared notification classification, category metadata, and stable dedupe fingerprints.
CLI/cmux.swift Uses the shared policy, preserves Grok dedupe state across repeated session starts, and stores recent fingerprints per session.
cmuxTests/CLIGrokNotificationNoiseTests.swift Adds integration coverage for Grok notification dedupe, fallback gating, permission prompts, and related notification behavior.
cmuxTests/AgentHookNotificationPolicyTests.swift Adds unit coverage for classification, fingerprint stability, and notification gate metadata.
cmux.xcodeproj/project.pbxproj Wires the new policy source and notification tests into the Xcode project.

Reviews (3): Last reviewed commit: "mux: dedupe and gate all grok agent-hook..." | Re-trigger Greptile

let notifications = notifyCommands(in: Array(context.state.snapshot().dropFirst(fallbackStart)))
XCTAssertEqual(notifications.count, 1, "Unclassified fallback re-notification should dedupe, saw \(notifications)")
XCTAssertTrue(
notifications.first?.hasSuffix("|c=idle-reminder;p=0") == 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.

P1 Fallback Seed Produces Untagged Alert

The fallback test seeds the session with a permission notification, so the later unclassified payload rebuilds from a needs-input state rather than an idle state. That path emits the attention fallback without the c=idle-reminder suffix, so this test can fail even when the CLI follows the current notification contract.

@austinywang austinywang closed this Jul 8, 2026
@austinywang austinywang reopened this Jul 8, 2026
defer { context.cleanup() }

try runGrokNoiseHook(context, "session-start", payload: grokNoisePayload(context, event: "SessionStart"))
try runGrokNoiseHook(context, "notification", payload: grokNoisePayload(context, event: "Notification", message: "Grok needs permission to run rm"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Seed idle fallback This test still seeds the fallback rebuild with a permission notification, so the saved session status is the permission or needs-input state rather than the idle-reminder state asserted below. When the later unclassified JSON has no hook event or message, the CLI rebuilds from that saved record; a correct implementation can produce the prior permission classification, or an untagged fallback, instead of |c=idle-reminder;p=0. That keeps this regression test tied to the wrong precondition and can fail even when idle fallback reminders are correctly gateable. Seed this case with an idle or waiting record, or split the permission fallback behavior into its own assertion.

austinywang and others added 2 commits July 8, 2026 12:27
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>
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
austinywang force-pushed the issue-7611-grok-notification-noise branch from 3d0890d to 57cf562 Compare July 8, 2026 19:27

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 57cf562. Configure here.

Comment thread CLI/cmux.swift
status: mapped?.lastNotificationStatus,
isFallback: false,
notifyCategory: mapped?.lastNotificationStatus == .idle ? .turnComplete : nil
notifyCategory: mapped?.lastNotificationStatus == .idle ? .turnComplete : .idleReminder

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale error alerts wrongly gated

Medium Severity

When a generic fallback notification is rebuilt from the session store, every non-idle lastNotificationStatus gets notifyCategory idleReminder, including .error. Fresh errors use .other and untagged wire meta so they always deliver; rebuilt error replays get c=idle-reminder and can be silenced by “Agent Waiting for Input” settings.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 57cf562. Configure here.

@austinywang
austinywang enabled auto-merge (squash) July 8, 2026 19:50
@austinywang
austinywang merged commit 5db24c1 into main Jul 8, 2026
29 of 31 checks passed
@vercel
vercel Bot temporarily deployed to Preview – cmux July 8, 2026 23:25 Inactive

This branch was previously deployed

1 inactive deployment
Preview – cmux — 57cf5629 Deployed Jul 8, 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.

Grok Build sessions spam notification sounds: non-idle hook notifications bypass dedupe, unclassified payloads bypass the per-category gate

1 participant