diff --git a/apps/web/src/components/app/docs-sections/automations.tsx b/apps/web/src/components/app/docs-sections/automations.tsx
index c899a968..48283ad2 100644
--- a/apps/web/src/components/app/docs-sections/automations.tsx
+++ b/apps/web/src/components/app/docs-sections/automations.tsx
@@ -182,9 +182,10 @@ export function AutomationsContent() {
agent config (prompt, agent type, model, worktree, full access). It
stays out of the Templates list and the palette, and the job's own
forms are how you edit it. Job runs notify through the ordinary
- per-agent channels — a job agent's done,{" "}
- waiting_user, or blocked event reaches Slack
- and web notifications exactly like any other agent's.
+ per-agent channels, with one exception — a job agent's{" "}
+ done, waiting_user, or blocked{" "}
+ event raises a browser notification like any other agent's, but is
+ excluded from Slack.
Dispatch can notify you when agents finish, need input, or get stuck — so you don't have to watch the dashboard. Notifications are delivered - through three independent channels: native browser notifications, Slack, - and local sound cues. All three are configured in{" "} + through three channels: native browser notifications, Slack, and local + sound cues. All three are configured in{" "} Settings → Notifications.
@@ -25,7 +25,8 @@ export function NotificationsContent() {waiting_user — agent needs your input
blocked — agent hit an error it can't resolve
+ blocked — agent is stuck with no further approach to
+ try
Dispatch suppresses browser and Slack notifications for an agent you're already looking at. Tabs that have an agent selected send a diff --git a/apps/web/src/components/app/notification-settings-constants.ts b/apps/web/src/components/app/notification-settings-constants.ts index 6ab9c74f..d84e70f2 100644 --- a/apps/web/src/components/app/notification-settings-constants.ts +++ b/apps/web/src/components/app/notification-settings-constants.ts @@ -21,6 +21,6 @@ export const EVENT_OPTIONS: Array<{ { id: "blocked", label: "Blocked", - description: "Agent hit an error or obstacle", + description: "Agent is stuck with no further approach to try", }, ]; diff --git a/apps/web/src/lib/sound-cues.ts b/apps/web/src/lib/sound-cues.ts index d4f837f6..a938f17d 100644 --- a/apps/web/src/lib/sound-cues.ts +++ b/apps/web/src/lib/sound-cues.ts @@ -154,7 +154,7 @@ export const CUE_INTENTS: Array<{ { intent: "blocked", label: "Blocked", - description: "Agent hit an error or obstacle.", + description: "Agent is stuck with no further approach to try.", }, { intent: "review_finished", diff --git a/apps/web/src/lib/tips/tips.ts b/apps/web/src/lib/tips/tips.ts index d7e7f570..edd0c39b 100644 --- a/apps/web/src/lib/tips/tips.ts +++ b/apps/web/src/lib/tips/tips.ts @@ -308,6 +308,14 @@ export const tips: Tip[] = [ since: "0.35.0", surfaces: ["ambient"], }, + { + id: "focus-suppression", + title: "Quiet While You Watch", + body: "Dispatch skips browser and Slack notifications for the agent you currently have selected in a visible, focused tab. Switch away, hide the tab, or blur the window and alerts for it resume right away.", + docsSection: "notifications#focus-suppression", + since: "0.11.12", + surfaces: ["ambient"], + }, { id: "status-correction", title: "Status Correction", diff --git a/docs/03-api-spec.md b/docs/03-api-spec.md index 2a89e535..d51a383c 100644 --- a/docs/03-api-spec.md +++ b/docs/03-api-spec.md @@ -358,7 +358,7 @@ All fields are optional — the request updates only the fields it contains. } ``` -`notifyEvents` and `webNotifyEvents` are arrays of event-type strings (`done`, `waiting_user`, `blocked`). When a notable agent event fires, Dispatch first attempts an in-app notification via the SSE event stream; if no browser client acks within ~3s it falls back to the Slack webhook (provided the event is enabled there). +`notifyEvents` and `webNotifyEvents` are arrays of event-type strings (`done`, `waiting_user`, `blocked`). When a notable agent event fires, Dispatch first attempts an in-app notification via the SSE event stream; if no browser client acks within ~3s it falls back to the Slack webhook (provided the event is enabled there). Agents belonging to a job run are excluded from that Slack fallback — their status events reach browser notifications only. ### `POST /notifications/ack` diff --git a/docs/17-jobs.md b/docs/17-jobs.md index 13b7b3ad..4dd9d590 100644 --- a/docs/17-jobs.md +++ b/docs/17-jobs.md @@ -140,7 +140,7 @@ Report size limits: 1 MB total, 100 tasks, 500 logs per task, 10 KB summary and **None of it fires today.** The channel lists come from `job.notify`, and no code path writes that column: it is absent from `createJob`'s insert, from `JobConfigUpdate`, from the routes' Zod schemas, and from the MCP `create_job`/`update_job` tools. `buildRunConfig` falls back to three empty arrays, so `getNotifyChannels` always returns none and the notifier returns before sending. Treat this as a data model waiting on a write path, not a shipped feature. -What does notify is the ordinary per-agent path in [docs/16-notifications.md](16-notifications.md): job agents are not excluded from it, so their `done`, `waiting_user`, and `blocked` events reach Slack and web notifications like any other agent's. Agents can also call `dispatch_notify` directly, which is in `JOB_TOOLS`. +What does notify is the ordinary per-agent path, and only half of it: a job agent's `done`, `waiting_user`, and `blocked` events raise browser notifications like any other agent's, but `createNotificationRuntime` skips the Slack send for any agent whose name starts with `job-` and has a job run. So a job run's status events never reach Slack through that path either. Agents can call `dispatch_notify` directly, which is in `JOB_TOOLS`, and that path has no job exclusion. ### API diff --git a/plugins/dispatch/skills/jobs/SKILL.md b/plugins/dispatch/skills/jobs/SKILL.md index 7de66e10..9234f05c 100644 --- a/plugins/dispatch/skills/jobs/SKILL.md +++ b/plugins/dispatch/skills/jobs/SKILL.md @@ -123,9 +123,10 @@ There is no per-job notification config to wire. The `notify` column exists in the data model, but nothing writes it — not the routes, not `create_job`, not `update_job` — so the per-job Slack path never fires. -Job agents are not excluded from the ordinary per-agent notifications, so a run's -`done`, `waiting_user`, and `blocked` events already reach whatever Slack and web -notifications the user has configured. That is the coverage you get for free — -which makes emitting an honest terminal `dispatch_event` the thing that actually -determines whether a failure is visible tonight. Call `dispatch_notify` directly -when a run needs to say something the status event cannot carry. +The ordinary per-agent path covers a job run only halfway: its `done`, +`waiting_user`, and `blocked` events raise the browser notifications the user has +configured, but the Slack send is skipped for any agent named `job-…` that has a +job run. So an honest terminal `dispatch_event` is what makes a failure visible +in the app, and nothing you emit as a status event will reach Slack. Call +`dispatch_notify` directly when a run needs to say something in Slack, or +anything the status event cannot carry.