Skip to content

Turn desktop notifications on or off per machine - #24518

Merged
brennanb2025 merged 12 commits into
mainfrom
brennanb2025/mute-notifications-per-host
Oct 3, 2026
Merged

brennanb2025 merged 12 commits into
mainfrom
brennanb2025/mute-notifications-per-host

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 16 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​870 $\color{#cf222e}{\Huge{\mathbf{−}}}$​23 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​847
Prod 32 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​575 $\color{#cf222e}{\Huge{\mathbf{−}}}$​21 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​554

ELI5

If you work across more than one machine (this computer, an SSH machine, or another paired Orca), every one of them sends you desktop notifications, and until now there was no way to quiet just one. A busy QA machine could bury the notifications you actually care about. Settings → Notifications now has a Machines list with an on/off switch for each machine you connected to.

What Changed

Before: Desktop notifications could only be turned off for everything, or per kind (agent finished, terminal bell). A paired Orca running lots of agents sent a banner for every finished turn, with no way to silence only that machine.

After: Settings → Notifications shows a Machines section (collapsed by default; the header shows how many machines are off) with one switch for each thing you connected to: this computer, each SSH machine you added, and each paired Orca server. Turning a switch off stops desktop banners (and their sound) for work on that machine.

  • Work reached through a paired server follows that server's switch. That includes the server's own SSH machines and the cloud VMs it creates. A cloud VM this computer creates follows "This computer". The section says this under its heading.
  • Only this computer is affected. The switches are saved in this computer's own Orca settings and are never sent to other machines. Muting M4Air here doesn't mute it anywhere else: the M4Air's own Orca, other computers paired with it, and your phone keep notifying as before.
  • A newly added machine starts switched on.
  • The section appears when you have more than one machine, and also stays visible while any listed machine is muted, so a mute can always be undone.
  • The master "Enable Notifications" switch still overrides everything.
  • Findable in settings search ("machines", "remote", "ssh", "mute").
  • Unchanged: phone push. A muted machine still reaches a paired phone exactly as before. The tray dot, unread markers, and the Settings "test notification" are also unchanged, because the switch only controls desktop banners.

Mechanism:

  • Notification settings gain mutedNotificationSourceIds, the machines whose switch is off. It is a list of what is off, so a machine added later notifies by default. Saved settings are validated on load: bad or duplicate ids are dropped, and a missing list means nothing is muted. The "were saved settings repaired on load" check now compares lists by content, so an unchanged list is not rewritten on every launch.
  • Each notification names its source machine (notificationSourceId). The sender works out the exact workspace the notification came from using the terminal or chat that raised it: the terminal's connection, a parked terminal's captured owner, or the chat's tab and the server connection the completion arrived on. It never guesses from whichever workspace is selected. One function (src/shared/notification-source.ts) then maps that workspace to a switch: this computer, a directly added SSH machine, or the paired server it was reached through. The Settings list is built from the same set of connected machines, so every notification maps to a switch you can see. If the machine can't be worked out, the notification is never treated as muted.
  • Main's delivery service, which already decides whether a banner appears, skips the desktop banner for a muted machine and reports host-muted. It does this after the phone-push step, which runs exactly as before, and before the desktop cooldown, permission check, sound and click handling.

Why

This came from noisy notifications from a paired QA machine. The control people want is "not from that machine".

A switch per connected machine is the common pattern for per-server notification controls. One switch covers everything that machine reaches, so the list stays short and every row is something you recognise. The alternative was a row for every physical place work runs, including a server's own SSH machines and short-lived cloud VMs. It would show raw internal ids, leave rows behind after cleanup, and need server-scoped ids. We rejected it for that reason.

The decision is made in main, where the other desktop notification preferences are applied, so both senders (terminals and agent chats) share one rule. The machine is taken from the terminal or chat that raised the notification, not from a workspace-id lookup, because two machines can publish the same workspace id. A lookup could then silence the wrong machine, or none.

Linked Issue

None. Internal request.

Visual Proof

Checked in a real Electron build of this PR connected to three real paired Orca servers. Full results are in the live-check comment below.

Machines collapsed with 1 off
Machines expanded

Testing

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

  • Delivery: a muted machine gets no desktop banner. Phone push goes through the real push dispatcher and is unchanged, with the same mobile event and cooldown. Other machines and notifications with no known machine still notify. The master switch wins.

  • Machine mapping: for local git and folder workspaces, direct SSH, paired-server workspaces, a paired server's own SSH machine, and cloud VMs (local and via a paired server), the machine a sender names is one the Settings list shows. Workspace-id collisions between machines (local terminal vs SSH sibling, selected or not; a paired chat vs a local workspace) never pick the wrong machine and never drop a chat's completion subscription. A removed SSH machine's leftover workspaces create no row.

  • Settings load and save: valid ids kept, junk dropped, unchanged list not counted as a repair. Quick toggles on two machines both persist.

  • Each regression test was checked to fail against the code before its fix.

  • Node, web and CLI typechecks, changed-code quality gate, and localization checks pass. New strings are translated in es, fr, ja, ko, zh. Live-checked on macOS in a real Electron build with real paired servers. A real bell and real agent completions from a muted server were held back as host-muted, and other machines still delivered. Not live-checked on Windows or Linux as the client.

AI Disclosure

Review

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Cross-platform: no platform-specific code. Machine ids are the same on every OS, and WSL counts as this computer.
  • SSH / remote: SSH machines and paired servers each get a switch. The setting lives on the client that shows the banner, and notification settings are not synced to paired servers.
  • Wire compatibility: no change between machines. The new request field is optional and only travels from the renderer to main on the same machine. What hosts publish and the phone notification event are unchanged.
  • Backwards compatibility: older saved settings load with nothing muted. Known limitation: opening the same profile with a build from before this change, then saving settings, drops the mute list, as it does for any newer setting. Returning to this build then shows every machine switched on.
  • Performance: the machine is worked out once per notification, or when a terminal is parked. Nothing runs per chunk of terminal output.
  • Not in this PR: plugin notifications already ignore all notification settings, including the master switch, so they also ignore machine switches. That predates this change and is left for a separate fix.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

Settings > Notifications lists each machine (this computer, SSH targets,
paired Orca servers) with a switch. Muted machines are stored as an
opt-out list so a newly added machine still notifies. Both notification
senders now name the machine a workspace runs on, and main skips the
desktop banner for a muted one; phone push is unchanged.
@greptile-apps

greptile-apps Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 3/5

[Medium risk] Adds per-machine notification muting to settings.

The PR is not ready to merge because VM-backed notifications can bypass their visible machine switch and rapid switch changes can lose a selection.

Findings

  1. P1 VM notifications bypass muting ▶
  2. P1 Rapid toggles lose selections ▶
  3. P2 Search points to hidden controls ▶

Summary

The PR adds per-machine desktop notification switches, persists muted host IDs, and attaches workspace host IDs to both notification senders.

  • Main suppresses desktop delivery for a muted ID while retaining phone dispatch.
  • VM-backed workspaces can use a hidden SSH ID rather than the runtime switch’s ID, and rapid switch changes can lose a selection.
  • The Machines search result remains visible when its section is hidden.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Workspace notification] --> B[Resolve execution host ID]
  B --> C[Main delivery service]
  D[Machines switches] --> E[Persist muted host IDs]
  E --> C
  C --> F{Host ID is muted?}
  F -- Yes --> G[Skip desktop banner]
  F -- No --> H[Apply other desktop preferences]
  C --> I[Phone dispatch]
Loading

Reviews (1) · Last reviewed commit: "feat(notifications): turn desktop notifi..."

state: StoreSnapshot,
workspaceId: string
): { executionHostId?: ExecutionHostId } {
const executionHostId = getResolvedExecutionHostIdForWorktree(state, workspaceId)

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 VM notifications bypass muting

A VM-backed workspace can resolve to an ssh:runtime-ssh-… ID, while the Machines list hides that SSH target and shows the runtime machine instead. Both notification senders pass the resolved ID to main, which requires an exact match with the muted ID. Turning off the visible runtime switch therefore leaves desktop banners from that workspace enabled.

description={host.detail}
checked={!mutedExecutionHostIds.includes(host.id)}
disabled={disabled}
onToggle={() => onChange(toggleMutedExecutionHost(mutedExecutionHostIds, host.id))}

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 Rapid toggles lose selections

If someone toggles two machines before the first asynchronous settings write updates the rendered props, both callbacks build their lists from the same old value. The second list replaces the first, so the earlier machine can remain on and keep sending desktop notifications.

Comment on lines +82 to +83
{
title: translate('auto.components.settings.notifications.search.machines', 'Machines'),

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 Search points to hidden controls

The Machines search result is always available, but the Machines section is hidden when this computer is the only host. In that case, searching for “machines,” “remote,” or “ssh” leads to Notifications without a matching control, making the result misleading.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@pullfrog pullfrog Bot left a comment

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.

Important

Host-muting is folded into desktopAllowed, which also gates background push to a paired phone — so the "a muted machine still reaches a paired phone" behavior described in the PR is not what the code does. Please confirm the intended behavior and either preserve phone delivery or correct the description and the test name.

Reviewed changes

  • Per-machine mute setting — mutedExecutionHostIds added to NotificationSettings, stored as an opt-out list, normalized on load (junk/dupes dropped, missing → unmuted) and defaulted in the browser client.
  • Sender plumbing — both notification senders now attach the workspace's owning machine via the new getNotificationExecutionHostId; main mutes the desktop banner when that id is in the list and reports host-muted.
  • Repair comparison — persistedNotificationSettingsRepaired now compares arrays by content so an unchanged muted list is not rewritten on every launch.
  • UI + i18n — new NotificationHostToggles "Machines" section (one switch per host, hidden when only this computer), plus settings-search entry and six locales.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/main/notifications/notification-delivery-service.ts Outdated
Comment thread src/main/notifications/notification-delivery-service.test.ts
@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 5dfe17e1-8cf0-48d6-ab55-0e57a07ec6e5

📥 Commits

Reviewing files that changed from the base of the PR and between 26750f6 and 240e85f.

📒 Files selected for processing (6)
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/i18n/locales/ja.json

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The change adds per-machine notification muting to notification settings and desktop delivery. Terminal and structured-chat dispatch resolve notification sources from workspace ownership, and terminal events carry PTY and owner metadata. The settings pane displays mute controls for available machines. Settings normalization, delivery behavior, source resolution, and controls have added test coverage, with locale strings for the controls.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 240e8

Per-machine notification switches now preserve rapid consecutive changes. The latest changes only add translated text, and no open merge-blocking concern remains.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 26750

Machine muting remains separate from execution authority, and the inspected notification paths preserve workspace ownership. The main uncertainty is whether paired phones enforce the intended policy for notifications sent while desktop notifications are disabled. Recovery after failed or interrupted settings saves is also not fully established.

Retained concerns

  • Low · security · inferred: Desktop-disabled events now enter the paired-phone delivery path with their title and body, accompanied by desktopAllowed=false. The inspected local main version stopped those events before mobile fan-out. Preserving the intended phone behavior therefore depends on downstream handling that was not verified; this is a policy-boundary uncertainty, not a verified unauthorized disclosure.
Security review details

Security Blast Radius

  • inferred — The evidenced policy scope is the desktop client's configured local, SSH, and paired-runtime sources, plus phones reached by its existing mobile dispatcher. Runtime-source muting aggregates work attributed to that runtime; it does not confer execution privileges.

Security Findings and Attack Paths

  • observed — When a mobile dispatcher exists, head forwards non-test notification titles and bodies before rejecting desktop delivery. Master/type-disabled events carry desktopAllowed=false. Actual phone presentation, storage, and enforcement of that flag remain outside the inspected evidence.

Trust Boundaries and Controls

  • observed — The inspected structured-chat path retains runtime-target selection, remote subscription capability checks, session/outcome validation, and tab-owned attention addressing. Notification source identity flows through the existing delivery API rather than directly granting access to desktop or mobile implementations.

Resilience and Maintainability Implications

  • observed — Desktop and mobile cooldowns are separate, and machine-muted events return before reserving the desktop cooldown. This prevents muted-source desktop suppression from consuming the desktop delivery reservation.

Hardening Proposals

  • proposed — Validate the desktopAllowed=false contract through phone reception, background push, and presentation, including master/type-disabled events. Treat notification content transfer and visible alerts as separate policy outcomes.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 42 functions across 47 files. (6 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly describes the main change: per-machine controls for desktop notifications.
Description check ✅ Passed The description covers the change, rationale, testing, and visual proof, and identifies the linked issue as an internal request, consistent with the template’s maintainer exception. The AI Disclosure …
Full details: Docstring Coverage

Explanation

Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 42 functions across 47 files. (6 skipped: 6 unsupported.)

  • Fix all pre-merge checks with AI
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: fe7768b8-47f9-44ae-baee-04a99a0c7332

📥 Commits

Reviewing files that changed from the base of the PR and between 24b97e8 and 6bc2055.

📒 Files selected for processing (22)
  • src/main/notifications/notification-delivery-service.test.ts
  • src/main/notifications/notification-delivery-service.ts
  • src/main/persistence/applying-settings/notification-settings-normalization.test.ts
  • src/main/persistence/applying-settings/onboarding-normalization.ts
  • src/renderer/src/components/native-chat/structured-attention-dispatch.ts
  • src/renderer/src/components/onboarding/use-onboarding-flow-persistence.test.ts
  • src/renderer/src/components/settings/NotificationHostToggles.test.tsx
  • src/renderer/src/components/settings/NotificationHostToggles.tsx
  • src/renderer/src/components/settings/NotificationsPane.tsx
  • src/renderer/src/components/settings/notifications-search.ts
  • src/renderer/src/components/terminal-pane/terminal-notification-state.test.ts
  • src/renderer/src/components/terminal-pane/terminal-notification-state.ts
  • src/renderer/src/components/terminal-pane/use-notification-dispatch.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/web/preload-api/web-preference-normalization.ts
  • src/shared/notification-settings-defaults.ts
  • src/shared/notification-settings-types.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.

description={host.detail}
checked={!mutedExecutionHostIds.includes(host.id)}
disabled={disabled}
onToggle={() => onChange(toggleMutedExecutionHost(mutedExecutionHostIds, host.id))}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '1,205p' src/renderer/src/components/settings/NotificationsPane.tsx
sed -n '1,100p' src/renderer/src/components/settings/NotificationHostToggles.tsx

Repository: stablyai/orca

Length of output: 9540


🏁 Script executed:

#!/bin/bash
set -e
rg -n -S "NotificationsPane|updateSettings=\{|updateSettings:" src/renderer/src | head -120
printf '\n--- notification pane tail ---\n'
sed -n '180,360p' src/renderer/src/components/settings/NotificationsPane.tsx
printf '\n--- candidate settings update definitions ---\n'
rg -n -S "function .*updateSettings|const .*updateSettings|updateSettings\s*=" src/renderer/src src | head -120

Repository: stablyai/orca

Length of output: 32776


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- settings slice ---'
sed -n '175,230p' src/renderer/src/store/slices/settings.ts
printf '%s\n' '--- settings renderer matches ---'
rg -n -S -C 8 "NotificationsPane|notifications.*Pane|notificationSettings" src/renderer/src/components/settings src/renderer/src/hooks | head -220
printf '%s\n' '--- notification tests ---'
sed -n '1,180p' src/renderer/src/components/settings/NotificationsPane.test.tsx

Repository: stablyai/orca

Length of output: 31128


🏁 Script executed:

#!/bin/bash
set -e
rg -n -S "persistSettingsUpdates" src/renderer/src src/shared
rg -n -S -C 12 "function persistSettingsUpdates|const persistSettingsUpdates|export .*persistSettingsUpdates" src/renderer/src src/shared

Repository: stablyai/orca

Length of output: 2429


🏁 Script executed:

#!/bin/bash
set -e
rg -n -S "persistVisibilityAwareSettings" src/renderer/src

Repository: stablyai/orca

Length of output: 1363


🏁 Script executed:

#!/bin/bash
set -e
sed -n '1,180p' src/renderer/src/store/slices/worktree-visibility-settings-write.ts

Repository: stablyai/orca

Length of output: 5411


Preserve rapid changes to different machine switches.

Each NotificationHostToggles callback computes its array from the same mutedExecutionHostIds prop. NotificationsPane merges that array into its optimistic settings, so the second callback can replace the first mute change before refreshed props arrive. Pass the host ID to NotificationsPane, then compute the new array from notificationSettingsRef.current.mutedExecutionHostIds.

Suggested fix
diff --git a/src/renderer/src/components/settings/NotificationHostToggles.tsx b/src/renderer/src/components/settings/NotificationHostToggles.tsx
@@
-  onChange: (mutedExecutionHostIds: ExecutionHostId[]) => void
+  onChange: (hostId: ExecutionHostId) => void
@@
-          onToggle={() => onChange(toggleMutedExecutionHost(mutedExecutionHostIds, host.id))}
+          onToggle={() => onChange(host.id)}
diff --git a/src/renderer/src/components/settings/NotificationsPane.tsx b/src/renderer/src/components/settings/NotificationsPane.tsx
@@
-import { NotificationHostToggles } from './NotificationHostToggles'
+import {
+  NotificationHostToggles,
+  toggleMutedExecutionHost
+} from './NotificationHostToggles'
@@
-        onChange={(mutedExecutionHostIds) =>
-          void updateNotificationSettings({ mutedExecutionHostIds })
-        }
+        onChange={(hostId) =>
+          void updateNotificationSettings({
+            mutedExecutionHostIds: toggleMutedExecutionHost(
+              notificationSettingsRef.current.mutedExecutionHostIds,
+              hostId
+            )
+          })
+        }

...updates,
notifications: {
// Why: browser-stored settings can predate a newer notification field.
...defaults.notifications,

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
# Inspect browser preference loading and notification-field validation.
ast-grep outline src/renderer/src/web/preload-api/web-preference-normalization.ts --match mergeSettings --view expanded
rg -n -C 5 'mergeSettings\s*\(|mutedExecutionHostIds|normalizeNotificationSettings' src/renderer/src/web/preload-api

Repository: stablyai/orca

Length of output: 8412


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- normalization ---'
sed -n '100,175p' src/renderer/src/web/preload-api/web-preference-normalization.ts
printf '%s\n' '--- storage loading ---'
sed -n '1,115p' src/renderer/src/web/preload-api/web-preferences-store.ts
printf '%s\n' '--- notification consumers and types ---'
rg -n -C 8 'NotificationHostToggles|mutedExecutionHostIds|interface Notification|type Notification' src/renderer/src | head -240
printf '%s\n' '--- changed diff ---'
git diff --unified=12 8ff6296bc7844fc049357e4e6494aa3b42df8eba 6bc2055c60f76192e4a6610279cab688aaa88ded -- src/renderer/src/web/preload-api/web-preference-normalization.ts

Repository: stablyai/orca

Length of output: 30916


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- component location ---'
rg -l --glob '*.ts' --glob '*.tsx' 'export .*NotificationHostToggles|function NotificationHostToggles|const NotificationHostToggles|mutedExecutionHostIds\.includes|mutedExecutionHostIds' src/renderer/src | grep -v '/i18n/locales/' | head -80
printf '%s\n' '--- focused consumer matches ---'
rg -n -C 12 --glob '*.ts' --glob '*.tsx' 'function NotificationHostToggles|const NotificationHostToggles|mutedExecutionHostIds\.includes|mutedExecutionHostIds' src/renderer/src/components src/renderer/src/shared src/renderer/src/web | head -220
printf '%s\n' '--- notification type declarations ---'
rg -n -C 10 --glob '*.ts' --glob '*.d.ts' 'mutedExecutionHostIds' src | grep -v '/i18n/locales/' | head -160

Repository: stablyai/orca

Length of output: 31257


Normalize mutedExecutionHostIds before merging browser settings.

getStoredSettings() does not validate this field. When storage contains mutedExecutionHostIds: null, the field overrides the default empty array. NotificationHostToggles can then call .includes on null when host controls are displayed.

Normalize the field to an array of valid host IDs before returning merged settings.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 3fb2cc4d-29c5-43d3-a4f2-642c0c1cc106

📥 Commits

Reviewing files that changed from the base of the PR and between c70ab05 and 0f93bee.

📒 Files selected for processing (40)
  • src/main/notifications/notification-delivery-service.test.ts
  • src/main/notifications/notification-delivery-service.ts
  • src/main/persistence/applying-settings/notification-settings-normalization.test.ts
  • src/main/persistence/applying-settings/onboarding-normalization.ts
  • src/renderer/src/attention/notification-sources.test.ts
  • src/renderer/src/attention/notification-sources.ts
  • src/renderer/src/attention/notification-subject-owner.ts
  • src/renderer/src/components/native-chat/StructuredAgentSessionAttentionBridge.test.tsx
  • src/renderer/src/components/native-chat/StructuredAgentSessionAttentionBridge.tsx
  • src/renderer/src/components/native-chat/structured-attention-dispatch.ts
  • src/renderer/src/components/onboarding/use-onboarding-flow-persistence.test.ts
  • src/renderer/src/components/settings/NotificationHostToggles.test.tsx
  • src/renderer/src/components/settings/NotificationHostToggles.tsx
  • src/renderer/src/components/settings/NotificationsPane.test.tsx
  • src/renderer/src/components/settings/NotificationsPane.tsx
  • src/renderer/src/components/settings/notification-host-pending-save.test.tsx
  • src/renderer/src/components/settings/use-notification-source-options.ts
  • src/renderer/src/components/terminal-pane/parked-terminal-byte-watcher.test.ts
  • src/renderer/src/components/terminal-pane/parked-terminal-byte-watcher.ts
  • src/renderer/src/components/terminal-pane/pty-connection-types.ts
  • src/renderer/src/components/terminal-pane/pty-connection/agent-task-complete-notify.ts
  • src/renderer/src/components/terminal-pane/pty-connection/pane-pty-visibility-bind.ts
  • src/renderer/src/components/terminal-pane/pty-transport-connect-spawn.test.ts
  • src/renderer/src/components/terminal-pane/pty-transport.ts
  • src/renderer/src/components/terminal-pane/terminal-pane-lifecycle-types.ts
  • src/renderer/src/components/terminal-pane/terminal-pane-mount-cleanup.ts
  • src/renderer/src/components/terminal-pane/terminal-parked-pty-watcher.ts
  • src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.test.ts
  • src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.ts
  • src/renderer/src/components/terminal-pane/terminal-parked-watcher-registry.ts
  • src/renderer/src/components/terminal-pane/use-notification-dispatch.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/shared/notification-settings-defaults.ts
  • src/shared/notification-settings-types.ts
  • src/shared/notification-source.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review.

Comment thread src/renderer/src/components/native-chat/StructuredAgentSessionAttentionBridge.tsx Outdated

@pullfrog pullfrog Bot left a comment

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.

Important

The structured-chat collision fix only holds while the collided workspace is the active one; switching away flips the remote chat onto the local completion feed. Details inline.

Reviewed changes

  • Phone push preserved under a machine mute — desktopAllowed no longer folds in hostMuted; a muted source skips only the desktop banner after mobile fan-out and its cooldown, and the tests now exercise the real PushDispatcher.
  • Mutes rebound to configured sources — mutedExecutionHostIds/executionHostId became mutedNotificationSourceIds/notificationSourceId; the new shared/notification-source.ts maps an owner to a configured source (unlabelled SSH targets are not sources; recipe VMs and ephemeral cloud VMs collapse to local), and Settings builds its rows through the same mapper.
  • Parked terminals capture their transport owner — parked watchers carry a captured NotificationWorkspaceOwner through reconciliation so the machine still resolves after a park/reveal cycle.
  • Structured completions decoupled from owner resolution — the completion feed follows the worktree environment and is passed into the dispatcher for source labelling, with a test for the active collision.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

})
store.setState({
activeWorktreeId: WORKSPACE,
activeWorkspaceExecutionHostId: 'runtime:env-1',

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.

The workspace is pinned active here (activeWorktreeId: WORKSPACE plus activeWorkspaceExecutionHostId: 'runtime:env-1'), so getRuntimeEnvironmentIdForWorktree short-circuits on the active route and the collision never reaches its ambiguous branch. Switch away from this workspace and the same collision makes that lookup return null, so the bridge subscribes the remote chat to the local completion feed and its remote completions are dropped — the case this commit set out to fix. Widen this test to keep the remote row while the shared workspace is inactive, and derive the feed target from the tab's own host when the worktree lookup is ambiguous.

Technical details
# Collision fix only holds while the collided workspace is active

## Affected sites
- `src/renderer/src/components/native-chat/StructuredAgentSessionAttentionBridge.tsx:19-26` — feed target from `getRuntimeEnvironmentIdForWorktree`; fails closed to `null` on an ambiguous worktree id.
- `src/renderer/src/lib/worktree-runtime-owner.ts:69-80` — `resolveActiveWorkspaceRoute` returns a route only for the active workspace; otherwise `resolveIndexedWorktreeOwner(...).kind === 'ambiguous'` returns `null` (the index marks two rows with different `[repoId, hostId, runtimeOwnerEnvironmentId]` ambiguous at `worktree-runtime-owner-index.ts:177-182`).
- `src/renderer/src/runtime/runtime-client-target.ts:9-10` — `null` becomes `{ kind: 'local' }`.
- `src/renderer/src/runtime/structured-agent-session-turn-completion-feed.ts:39-41,189-199` — the feed is keyed by that target, so the remote host subscription is never opened.
- `StructuredAgentSessionAttentionBridge.test.tsx:405-407` — active route pinned, so the added local sibling only changes `resolveNotificationTabOwner` (asserted `null` at line 473), never the target.

## Required outcome
A remote structured tab on a workspace id that also exists locally keeps its environment feed subscription when the workspace is not the active one. Either derive the target from the tab's stamped host (falling back to the worktree environment) when the worktree lookup is ambiguous, or state that only the active workspace is supported and make the test and claim match.

## Open questions for the human
- Is the non-active collided-worktree case intended to be supported, or is only the active workspace in scope?

@brennanb2025

brennanb2025 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Review and QA summary

The problem

If you work across more than one machine (this computer, an SSH machine, or a paired Orca server), every machine's agents send you desktop notifications, and there was no way to quiet just one. A busy QA machine could bury the notifications you care about.

What changes for the user

Settings → Notifications has a new Machines section, collapsed by default, with an on/off switch for each machine you connected to: this computer, each SSH machine you added, and each paired Orca server, including offline ones.

  • Turning a machine off stops desktop banners and their sound for work on it. Other machines keep notifying.
  • Only this computer is affected. The switches are saved in this computer's own Orca settings and are never sent to other machines. Muting M4Air here doesn't mute it anywhere else: the M4Air's own Orca, other computers paired with it, and your phone keep notifying as before.
  • Work reached through a paired server, including that server's own SSH machines and the cloud VMs it creates, follows the server's switch. A cloud VM this computer creates follows "This computer". The section explains this under its heading.
  • New machines start on. The master switch still overrides everything. While collapsed, its header shows how many machines are off (e.g. "1 off"), and the section stays visible whenever a listed machine is muted, so a mute can always be seen and undone. Settings search finds it ("machines", "ssh", "remote", "mute").
  • Unchanged: phone push, the tray dot, and unread markers. The switch controls desktop banners only.

Fixed during review

Three review rounds, an architecture challenge, and a readiness check of the final diff. The readiness check raised no release-blocking candidates and no P2s.

Severity Problem Introduced by
P1 Muting a machine also stopped its phone pushes Original PR
P2 When two machines shared a workspace id, a notification could be tagged with the wrong machine or none, depending on which workspace was selected Original PR
P2 Muting this computer, then removing the last remote machine, hid the section and left you stuck muted Original PR
P2 Cloud VMs created from a recipe could never be muted Original PR
P2 An SSH machine reached through a paired server had a switch that did nothing Round-1 fix
P2 A paired server's chat could stop notifying entirely when a local workspace shared its id Round-2 fix
P3 Two quick toggles on different machines lost the first change Original PR
P3 VM rows showed raw ids and stayed after cleanup Round-1 fix

How it ended up: each notification takes its machine from the terminal or chat that raised it, never from the selected workspace. One function maps that to a switch, and the Settings list is built from the same set of connected machines. Main applies the mute to the desktop banner only, after the phone step, which is unchanged. The setting is mutedNotificationSourceIds. Each fix has a regression test, and each test was checked to fail against the code before its fix.

Deferred

  • Plugin notifications ignore every notification setting, including the master switch, so machine switches don't affect them either. This predates this PR.
  • Failed saves: after a failed save, the Notifications pane can re-send the failed value with your next change. main already does this for every setting in the pane.
  • Downgrades: opening settings in a build from before this change drops the mute list, as it does for any newer setting.
  • A paired chat on an unselected workspace whose id is shared with another machine falls back to this computer for its connection. main behaves the same.
  • Machine detail labels ("This computer", "Orca server") were already English-only.

Verified

Tests and typechecks: web, node and CLI typechecks plus about 350 targeted tests. CI is green on 240e85f1435. Phone push matches main across 1,152 setting combinations.

Live check: a real Electron build of 240e85f1435, run hidden on a Mac and connected to three real paired Orca servers, and to a directly added SSH machine. Main's real delivery decision was recorded for every notification.

Real event Tagged as Main's decision
Terminal bell on M4Air, switch on M4Air mac delivered
Terminal bell on M4Air, switch off M4Air mac host-muted
3 agent turns finishing on M4Air while it was off M4Air mac host-muted
Agent turn on M4Air after switching it back on M4Air mac delivered
Bell in a local folder while M4Air was off This computer delivered
Agent turns finishing on Windows high spec Windows high spec delivered
Quit and relaunch with M4Air off — still off, still host-muted
Master switch off — disabled (it wins over the machine switches)

Machines section, collapsed by default:

Machines collapsed

M4Air mac switched off: the collapsed header shows "1 off":

Collapsed with 1 off

Expanded: one switch per machine, indented under the heading:

Expanded with M4Air off

Settings search for "machines":

Settings search

Master switch off:

Master switch off

Directly added SSH machine (throwaway Linux container over SSH):

Real event Tagged as Main's decision
Terminal bell in a background SSH terminal, switch on the SSH machine delivered
Same, switch off the SSH machine host-muted
Bell in a local folder while the SSH machine was off This computer delivered
SSH machine switched back on, bell again the SSH machine delivered

SSH machine muted

Not verified

  • Banners on screen. This unsigned dev build isn't allowed to show macOS banners, so the evidence is main's recorded decision per notification, not a banner.
  • A Windows bell. The PowerShell bell commands tried didn't reach the terminal as a bell character, so Windows was checked through real agent completions only. None happened while M4Air was muted.
  • Windows or Linux as the client machine.

@pullfrog pullfrog Bot left a comment

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.

ℹ️ No new issues in this delta. The two new commits are a UI-only refinement of the Machines section; nothing actionable.

Reviewed changes

  • Wrapped the Machines rows in an uncontrolled Collapsible that starts collapsed, moving the off-count, Server icon, and description into the trigger, and added collapse/expand plus pending-save coverage.
  • Replaced the ad-hoc ghost-Button trigger with a new row CVA variant on the shared CollapsibleTrigger primitive, so the disclosure matches the settings rows; existing asChild callers are unaffected because the empty default variant reduces to the caller's own className.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

…ifications-per-host

# Conflicts:
#	src/renderer/src/i18n/locales/en.json
#	src/renderer/src/i18n/locales/zh.json
@brennanb2025
brennanb2025 merged commit 752871b into main Oct 3, 2026
49 checks passed
nwparker added a commit that referenced this pull request Oct 4, 2026
* fix: preserve registered account credentials after enrollment errors

* refactor(orchestration): one function for six agent-to-agent sends (#24901)

* refactor(orchestration): send every agent-to-agent message through one sendAgentTurn

The structured mail-pointer lane and the structured worker preamble each carried a copy of
"send, wait out a pending start, read the verdict", and four dispatch-preamble sites typed into
a terminal directly. They now share sendAgentTurn, built from host.send, the host's settlement
waiter and sendTerminalAgentPrompt. Every caller passes delivery 'now', so nothing sends
differently; a source-scan ratchet keeps new direct sends out.

* test(orchestration): the refusal fixture uses a real wire refusal code

* fix(orchestration): derive the send fingerprint inside sendAgentTurn and pair target with turn

A `queue` send was refused by a real host: callers supplied a fingerprint over the body alone
while the host digests body and delivery. sendAgentTurn now builds the envelope with the
composer's own builder (extracted to structured-agent-session-send-mutation), so the fingerprint
is always over exactly the fields sent; `now` sends keep the identical digest. The pointer lane
no longer carries a fingerprint it cannot get right.

sendAgentTurn takes one argument, a union on kind that carries its own target and turn, resolved
by an exhaustive switch; the terminal turn names its purpose (only the dispatch preamble today)
instead of every terminal send inheriting the task lead line. A queued outcome keeps the host's
draft receipt, and both structured callers read outcomes exhaustively with unchanged behaviour.

The boundary test now also fences direct structured host sends, catches optional, bracket and
bound member uses, has a planted-offender self-test, and pins the unmoved terminal mail pointer
and agent-teams tmux senders.

* fix(orchestration): keep federation.ts under max-lines and satisfy prefer-template in the send ratchet

* fix(orchestration): wait on the answered submission's id when a replayed queue turn was already handed off

* test(orchestration): pin that a replayed queue turn waits for its hand-off to settle

* test(orchestration): fence call-result and cast host sends, raw terminal writes and the launch-prompt helpers

* feat(native-chat): a Claude subagent waiting on a permission prompt reads as waiting (#22634)

* feat(native-chat): a Claude subagent waiting on a permission prompt reads as waiting

A subagent's permission request reaches the parent session's callback naming the
subagent that asked (agent_id) and the tool call it gates (tool_use_id). The
pending request is recorded with the asking agent. On every drain the child-work
producer re-derives which children a pending request blocks and hands that set
to the Claude child decoder, the one owner of each child's live edges: a blocked
child reads waiting on every live edge it reports, and a child that starts or
stops waiting is a live edge of its own. Answering, denying or cancelling the
request returns the child to its prior live state; nothing is stored beyond the
pending requests.

A live task_updated carrying an error now reaches the record as the child's last
message, without an ending or a new state.

The replay test drives a scrubbed capture of the real CLI (foreground allow,
deny, interrupt, background allow, and the main agent's own request) through the
real adapter into the host's child records.

* test(native-chat): a subagent's request names it before its tool call is read

* docs(agent-status): a subagent asking for approval waits in every lane; the parent row keeps the session's own attention

* test(native-chat): hand canUseTool the asking agent without widening the helper's cast

* test(native-chat): an interrupted Claude subagent settles cancelled, not failed

A captured interrupt shows the spawn call's error result ("The user doesn't want
to proceed…") arriving before the subagent's own `task_updated {status: killed}`.
The spawn result ends nothing (the child ends only on its own terminal frame), so
the child stays live until its `killed` status settles it cancelled. A genuine
failure, captured with the subagent on a model that does not exist, sends its
`failed` status before the error result and still ends failed. Both captures now
replay through the real adapter into the host's records.

* fix(native-chat): a Claude subagent's prompt makes the parent row wait, not block

A subagent's pending prompt made the whole session `attention`, which reads as
the main agent's own `blocked` and outranks the fold's waiting arm, so the
parent row read blocked where a CLI Claude parent reads waiting. The main
agent's state now reads only its own pending prompts.

- A Claude prompt row carries the linkage of the agent that raised it: the one
  the permission request names, or the owner of the tool call it gates. The
  same join decides which child reads waiting, so the two cannot disagree.
- The status summary projects the session's own status from root prompts only;
  every other reader (delivery gates, teardown, restart) still asks whether
  anyone is waiting on a human.
- An answer keeps the prompt row's linkage by the journal's own rule: a
  revision that names no producer keeps the row's existing one.
- The child-tool queries gain the prompt's producer, so a prompt row and a
  child record answer "which agent" from the same join.

* test(native-chat): say which ids the permission capture scrubs and which are its own

* fix(native-chat): the status clock dates attention by the session's own asks only

The session's status is now `attention` only for its own pending prompt, so the
clock's fallback to a subagent's ask could no longer be reached, and it read the
journal by a different rule than the status it dates. Both now read root prompts.

The journal also stamps a Codex subagent's prompt with its thread (#22532), so a
Codex child's approval is that child's wait in the Codex lane too. Two tests
written for the earlier rule are updated: a subagent's ask leaves a running
session `working` on its turn's clock, and a Codex child's answered approval
leaves the settled parent's Activity row done with nothing unread.

* fix(native-chat): a completion still says the user is asked when a subagent asks

The turn-completion feed marked a completion `awaitingUser` from the status
summary's `attention`. The status now means the session's own agent is waiting,
so a subagent's pending approval stopped reaching the completion. The projection
now also says whether anyone is waiting on the user, as the delivery gates,
teardown and restart ask it, and the completion reads that.

The waiting-subagent replay answers its prompt with the adapter's current
response shape.

* revert(native-chat): a live Claude task's error stays out of the child's last message

No capture shows a live task_updated carrying an error, and it is unrelated to
a subagent waiting on a permission request; it leaves this PR.

* test(native-chat): settle the Claude session's startup before replaying a subagent's request

A startup frame drained child work during the first await, so answering a
request freed the child even with the answer's own republish removed.

* fix(native-chat): a Claude subagent's prompt row names it as its other rows do

The prompt row stamped only the asking agent's id, so a nested subagent's
request lost the agent that spawned it, its spawn call and its run. It now takes
the linkage the asker's own rows take: the gated tool call's, when that names the
same agent, else the one resolved through the agent's spawn call. The provider's
agent id stays the asker's id.

* fix(native-chat): a subagent's request makes the parent row wait without a child record

The parent row learned that a subagent needed the user only from that
subagent's child record, so a request no record carried (a Codex child the host
never registered, a Claude task past the live cap) left the row working or done
while the approval card sat in the chat.

"Someone in this session must answer" is now one derived session fact. The
projection names two facts instead of a mode flag: the main agent's own status
(attention only for its own request) and structuredAgentSessionAwaitsUser (any
pending prompt). The status summary publishes the second as an optional
awaitsUser, and the shared fold reads it: the main agent's own ask is blocked,
otherwise awaitsUser or a waiting child record makes the row wait. Every caller
picks the fact it means: the completion edge's awaitingUser and the delivery
gate read awaitsUser; the quit snapshot folds the same two inputs the sidebar
does.

* fix(native-chat): a client that predates awaitsUser still reads a subagent's request as attention

A status summary's status is now the main agent's own, so a client built before
the split would read a subagent's request as working (or idle) and fold it with
code that has no awaitsUser input. Clients advertise
agent-session.status-awaits-user.v1; at agentSession.subscribeStatus the host
sends any client that does not the pre-split summary: attention whenever
awaitsUser is set, without the main agent's own tool line, verdict and clock.
The feed and every in-process reader keep the canonical summary. Transitional,
like the turn-item downgrade.

* test(native-chat): a Codex subagent's approval makes its settled parent's Activity row wait

The test pinned the parent row done while a Codex child asked, through a harness
that fed no child records, so it proved nothing about the ask. It now drives the
ask twice through the real host status store: with no child record (the
session's awaitsUser alone) and with the child's own record waiting from
thread/status/changed. Both read waiting with needsAttention while the ask is
open, then done with nothing unread.

* docs(agent-status): a subagent's request reaches the parent row through awaitsUser in every structured lane

The store reference said a Codex child's request still read as the main agent's
blocked and that only the Codex hook lane fed a waiting child. Both structured
lanes stamp the asking child and feed child records, and awaitsUser carries the
request when no record does. The liveness comment goes back to main's: a child's
blocked is a failed task on an older host's legacy rows.

* fix(native-chat): the restart dialog still headlines a subagent's pending approval

The quit snapshot now records the main agent's own state, so a subagent asking
while the main agent worked recorded `working` and the dialog said "Was
mid-reply" where it used to say "Waiting for your approval". The headline now
comes from the snapshot's pending prompt, whoever raised it, with the existing
copy; `state` stays the main agent's own.

* test(orchestration): a subagent's pending approval holds structured mail delivery

Scoping the delivery gate to the main agent's own request left every gate test
green; a subagent's request now has its own case.

* fix(native-chat): a subagent's request is dated by when it was raised, on every client

Since the summary's clock became the main agent's own, nothing dated a wait
that only a subagent's request held: a pre-split client was sent attention with
no clock, where the old host dated it by the subagent's prompt, and a new
client's waiting row fell back to the time it first saw it, so after a reload a
request the user had already read could read unread again.

The session fact is now when someone started being asked: awaitsUserSince, the
oldest pending prompt whoever raised it, and its presence is what awaitsUser
meant. A row waiting on someone else's request takes that as its clock; the
downgrade for a client without the capability dates its attention by it, which
is what the old host published. A cross-version test pinned to the last
pre-split release runs the same journals through that release's projection and
through this one plus the downgrade, and compares the whole summary. The Codex
end-to-end test also reads the host's own status row, and keeps a read ask read
through a later row and a reload.

* test(native-chat): the pre-split parity check compares only the fields the split owns

An additive summary field is safe for old clients, so comparing whole summaries
against the pinned release would redden on one. The wire comment now says how
the downgrade dates attention: the main agent's own oldest ask, else
awaitsUserSince.

* test(runtime): an aged host-held working summary states that nobody is asked

The test built its working summary by overriding the status of a published
approval summary, which still carried awaitsUserSince, so the row correctly
read waiting. It now drops the request as its scenario says.

* fix(native-chat): the chat's subagent block says waiting when the strip does

While a Claude subagent's request was open, the sidebar and the composer strip
read waiting but the subagent block in the chat history a few pixels above
still read "Kicked off 1 subagent working": it shows the journal's roster
state, and the journal records no wait.

The structured chat now hands its transcript the subagents the strip shows
waiting, read from the host's child records through the strip's own row model
and matched by the provider id the roster names each one by. A running entry
the host says is waiting reads waiting in the group row, its entry and its
section head, with the strip's word and the question colour; it reads the
journal's state again as soon as the host stops reporting the wait.

* fix(native-chat): a collapsed subagent group shows a wait beside a failed sibling

A failed sibling took the group row's one alert slot, so a group with a waiting,
a working and a failed child read "1 working +1 failed" and hid the wait; it
now reads "1 working +1 waiting +1 failed". The waiting set keeps its identity
while a child frame changes no wait, so the transcript's subagent rows do not
re-render on every frame, and the test of a wait ending now updates one mounted
row instead of remounting it.

* refactor(claude): one needs-input state on the parent; the asking subagent alone reads waiting

Drop the split of the main agent's own status from a session-wide "someone must
answer" fact: awaitsUserSince, the agent-session.status-awaits-user.v1
capability and its old-client downgrade, and every reader change that only
consumed them (fold, equality, ingest, delivery gate, turn-completion feed,
quit snapshot, resume headline, status clock, status bridge, attention
dispatch) go back to main. The parent row again reads one needs-input state
for a pending request whoever asked, dated as before.

Kept: a request's owner recorded once on its prompt row with full producer
linkage; the asking subagent's own record reads waiting, re-derived on every
update; the chat history's subagent block reads that same state; an answered
subagent request stays in its subagent's group.

A subagent now waits only on a request the user can still answer (its card
open, no answer underway), and the adapter frees it before the host records an
answer or dismissal. So a waiting child record always sits beside the pending
card, and main's fold never reads the parent as waiting on it: no window after
an answer, and no ~3 s wait after a card dismissed by Stop.

* fix(claude): a subagent waits only beside its committed card

A subagent's wait was pushed to the host as soon as its request arrived,
while the request's card row reached the journal at least a microtask later.
So every subagent request published the parent row as waiting before
blocked (the main agent's own fold reads a waiting child that way), and
Activity got an extra unread "waiting" event that main never shows.

The card is now the one record of an open request. The translator records
the asker on the card once (its row's linkage) and counts the card open only
after the sink confirms its rows landed, then publishes the wait; anything
that closes the card (an answer underway, a dismissal handed to the host,
Claude's own withdrawal, the session's end) frees the subagent first. So
every publish that shows a subagent waiting also shows its pending card, and
the parent reads one needs-input state, exactly as on main.

This retires the registry's view of pending requests (unclaimed(), the
asking-child join) and the translator's holdsOpen. The prompt row's linkage
takes one rule: the agent the provider names, else the gated call's owner.

The parent-row proof now runs through the real deferred sink, durable
journal and status feed, publishing as production does, and checks at every
publish that waiting subagents have pending cards and that the parent row
matches a host fed no waits.

* fix(native-chat): a closed sink's dropped writes never read as landed

The sink's written() resolved ok when the sink was closed with writes still
queued, so a subagent's prompt card could count as open with no row in the
journal. written() now reports a close that dropped writes admitted so far
as not landed; drained() and lifecycleBarrier() keep reading a closed sink as
settled.

Tests: a card never opens when its sink closes first; a card Claude withdraws
while the sink holds the cancelled row back closes at once; two subagents
asking at once, and the main agent asking beside a subagent, keep the parent
row as before with each waiting subagent beside its own card; a process that
dies mid-request leaves no subagent waiting.

* fix(claude): a withdrawn subagent request frees its child before its card closes

Main's sink now hands each write to the journal as it is submitted, and an idle journal commits it
and runs the publication at once. Claude's own withdrawal of a subagent's request therefore closed
the card and published the parent row before the child's wait was freed, so one publish showed the
subagent waiting beside no pending card (fg-interrupt replay). The child's wait now also requires the
request to still be open in the registry, and a withdrawal republishes child work before the
journal takes the close.

* refactor(claude): trim subagent request waiting to the common pattern and its essential tests

The chat history no longer marks a subagent block as waiting: the approval card itself carries the
request, and the asking subagent's row in the sidebar and composer strip reads waiting, as before.
NativeChatWaitingSubagentsProvider, native-chat-waiting-subagents.ts and their renderer changes go.

A subagent waits while its request is still open and unanswered in the prompt registry and its card
has landed in the journal. The registry check also covers a withdrawal under backpressure, so the
card list no longer filters pending cancellations itself.

Tests: one integration file replays the captured CLI frames through the real adapter, sink, journal
and status feed (renamed claude-subagent-permission-request.test.ts), with the asking subagent's
state timeline, attribution, nested linkage and a card write that waits for the journal. The
producer-harness waiting test, the redundant prompt-card cases, the harness reducer swap and three
unused captures (deny, interrupt, main agent, failed subagent) are removed.

* test(claude): pin the parent row's dating when a subagent asked first, and narrow the oracle's claim

* Turn desktop notifications on or off per machine (#24518)

* feat(notifications): turn desktop notifications on or off per machine

Settings > Notifications lists each machine (this computer, SSH targets,
paired Orca servers) with a switch. Muted machines are stored as an
opt-out list so a newly added machine still notifies. Both notification
senders now name the machine a workspace runs on, and main skips the
desktop banner for a muted one; phone push is unchanged.

* fix(notifications): preserve phone alerts and honor machine mutes

* fix(notifications): bind machine mutes to configured sources

* fix(notifications): preserve chat completion subscriptions on owner collisions

* fix(notifications): reduce machine settings clutter with a collapsed section

* fix(notifications): align machine disclosure with settings rows

* fix(notifications): clarify machine switches affect only this computer

* fix: distinguish unavailable account metadata from absence

* fix(accounts): resolve registered profile UUIDs consistently

* Replace agentArgsOverride with unified removeAgentArgs approach (#25091)

Consolidate override detection and removal into removeAgentArgs. Previously
catalogs used agentArgsOverride to detect conflicts and removeAgentArgs to
strip them; now removeAgentArgs handles both by returning stripped tokens.
This eliminates redundancy and makes the intent clearer.

Enhance removeAgentArgOption with optional value filtering to support
selective removal for complex cases like Codex config overrides.

* Reuse the ancestor path while building mobile agent rows (#24539)

Preserve traversal order and cycle guards using one call-local path Set rather than a copy at every depth.

* Release waiting terminal output when a mobile subscription fails to start (#24547)

Dispose the failed subscription record’s existing terminal backlog before rethrowing its original start error.

* Avoid cloning terminal agent owners twice in relay listings (#24713)

Capture the existing host-age expression at its original time, then use one call-local fresh owner clone for the length check and unchanged published owner field.

* Reuse prepared statements for internal worker checks (#24573)

* Reuse prepared statements across remaining Dispatch reads

Use the existing schema-pinned complete Dispatch column list in five remaining read modules, enabling the existing bounded statement cache without caching results.

* Reuse prepared statements for internal worker checks

Use the existing complete Dispatch projection only for named-field internal consumers; preserve original wildcard reads for every returned failure snapshot.

* Prepare the Activity search query once per filtered list (#24701)

Lazily reuse a call-local search predicate inside the existing filter, using unchanged byte gates and thread text cache.

* Avoid rescanning shared chunks in the plain Node build guard (#24717)

Reuse a successful unchanged chunk-code verdict within one synchronous writeBundle invocation, while preserving traversal and current-code reads.

* Select the latest stable release tag in one pass (#24786)

Replace filter/sort/last selection with one traversal using the unchanged stable-tag regex and numeric comparator; update on equality to preserve the original last spelling. Both actual callers own private plain arrays from Git stdout.

* Avoid Resource Manager renders when terminal removal leaves its inventory unchanged (#24806)

Reuse the private React state object only when the existing single/bulk removal helper returns its identical inventory; keep every lifecycle revision, tombstone, known-ID write and helper invocation in its original order.

* Cancel pending cursor updates when a terminal closes (#24566)

Reuse the existing pane frame tracker to cancel owned deferred focus-class updates during existing cleanup, with disposed guards against late or reentrant delivery.

* Cancel terminal preview fit frames when the preview closes (#24712)

Reuse the existing frame tracker inside the box-fit owner, suppress scheduling after disposal and cancel pending frames at the start of the existing effect cleanup.

* Skip new mobile toast work after feedback owner cleanup (#24759)

Reuse the existing mobile mounted-ref lifecycle pattern at toast presentation entry, preserving admitted clipboard outcomes and every live animation/sequence/timer operation.

* Release terminal side effects after they have been delivered (#24548)

Clear consumed queue slots after successful apply or overflow carry; preserve backing identity across reentrant callbacks and report actual retained slots.

* Release relay handshake timers when a host connection leaves (#24554)

Reuse the existing first-frame finish pattern to remove stage-owned timer/message/close callbacks on receipt, timeout or close; check OPEN after successful async assignment verification.

* Avoid restarting error timers on closed mobile relay links (#24567)

Check the existing irreversible closed flag before scheduling the existing missing-close fallback timer.

* Avoid restarting error timers after mobile relay pairing closes (#24568)

Check the existing pairing owner closed flag before allocating its missing-close fallback alarm.

* Delete unreturned clipboard cache files after a failed write (#24599)

Reuse existing provider-copy best-effort deletion for a newly created clipboard cache file whose write fails before its URI reaches the caller.

* Skip new legacy file inventories after mobile search cleanup (#24792)

Reuse the existing mobile mounted-ref pattern only at legacy fallback entry after completed passive cleanup; preserve admitted work and all live search/authority/cache paths. Correct only the strict fully-unmounted inventory scenario and its sole golden.

* Discard completed AI Vault cancellation IDs in the relay worker (#24820)

Use the relay child's existing pending Set to ignore cancellation IDs after completion, matching its desktop sibling; preserve admitted active/queued cancellation and the bundled private sender's complete replies, errors and ownership.

* Release completed updater setup timers and callbacks (#24920)

Cancel completed deferred updater fallbacks in finally, clear only their exact pending callback, and drop the captured timer before the original fallback guard runs; preserve destroyed admission, successor ownership and updater/quit callbacks.

* Check daemon idle state without building discarded terminal inventories (#24749)

* Check daemon idle state without building discarded terminal inventories

Replace the private idle predicate full public inventory with a host-local full pass that still reads every Session liveness getter in original order.

* Remove overwritten daemon test mock assignment

* Preserve the registered account ID when selecting UUID variants

* fix(updater): keep macOS Orca open when background instances block updates (#24952)

* fix(updater): guard macOS installs against running app instances

* fix(updater): match native app blockers and preserve quit lifecycle

* fix(updater): keep ordinary macOS quit on Squirrel's install-on-exit path

Converting every quit with a staged update into quitAndInstall made Cmd+Q
relaunch Orca, refused the quit when background instances existed, and
hijacked app.relaunch()+app.quit() restart flows (profile switch, admin
restart) into an update install racing the relaunched old app. Only
Update & Restart runs the running-instance preflight now; the
quit-without-install allowance is no longer reachable and is removed.

* fix(updater): preserve quit intent through macOS staging

* test(native-chat): explicitly model legacy published tab ownership

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
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