From 7051cde73c3fb02ce00f9902ad31da0f36a38ffe Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Wed, 19 Aug 2026 22:05:46 -0600 Subject: [PATCH] Correct the Notifications docs and the job-agent Slack claim Nightly docs audit, deep-diving the docs-pane Notifications section. Verified clean: the three notify events as a set in both directions against EVENT_OPTIONS and NOTIFY_EVENT_TYPES, the five sound-cue preview buttons (four CUE_INTENTS plus the mobile tap), every

block the settings pane renders, the ~3s ack timeout, the 15s focus heartbeat and 30s TTL, and every dispatch_notify parameter, limit and tool-set claim. Fixed: - Four surfaces claimed a job agent's status events reach Slack like any other agent's. createNotificationRuntime skips the Slack send for any agent named `job-...` that has a job run, so those events raise browser notifications only. Corrected in docs-sections/automations.tsx, docs-sections/notifications.tsx, docs/17-jobs.md, docs/03-api-spec.md and plugins/dispatch/skills/jobs/SKILL.md. - docs/17-jobs.md linked to docs/16-notifications.md, which doesn't exist. - The Slack section never mentioned Save. The webhook and the Slack event list only persist on Save, while the browser toggles above them save as soon as they're flipped. - `blocked` was described as "hit an error it can't resolve", which contradicts the Status Events section and the injected launch guidance. Aligned the docs and the settings-pane copy (EVENT_OPTIONS, CUE_INTENTS) with the canonical wording. - Dropped "independent" from the intro; browser and Slack are chained. Added a focus-suppression ambient tip, since a user who doesn't know about it reads the silence as a broken notification. Co-Authored-By: Claude Opus 5 (1M context) --- .../app/docs-sections/automations.tsx | 7 ++++--- .../app/docs-sections/notifications.tsx | 21 ++++++++++++------- .../app/notification-settings-constants.ts | 2 +- apps/web/src/lib/sound-cues.ts | 2 +- apps/web/src/lib/tips/tips.ts | 8 +++++++ docs/03-api-spec.md | 2 +- docs/17-jobs.md | 2 +- plugins/dispatch/skills/jobs/SKILL.md | 13 ++++++------ 8 files changed, 36 insertions(+), 21 deletions(-) diff --git a/apps/web/src/components/app/docs-sections/automations.tsx b/apps/web/src/components/app/docs-sections/automations.tsx index c899a968c..48283ad2c 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.

diff --git a/apps/web/src/components/app/docs-sections/notifications.tsx b/apps/web/src/components/app/docs-sections/notifications.tsx index a0abc019d..3a263e659 100644 --- a/apps/web/src/components/app/docs-sections/notifications.tsx +++ b/apps/web/src/components/app/docs-sections/notifications.tsx @@ -6,8 +6,8 @@ export function NotificationsContent() {

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
  • @@ -60,10 +61,14 @@ export function NotificationsContent() { > Slack incoming webhook {" "} - URL and use Send Slack test to verify it. Configured - events fire to Slack whenever a browser notification isn't delivered - (no tab open, permission denied, or the event isn't in your - browser-notification list). + URL and use Send Slack test to verify it, then click{" "} + Save — the webhook and the Slack event list are only + persisted when you save, unlike the browser toggles above, which save + as soon as you flip them. Configured events fire to Slack whenever a + browser notification isn't delivered (no tab open, permission denied, + or the event isn't in your browser-notification list). Agents launched + by a job are the exception: their status events go to browser + notifications only, never to Slack.

    @@ -93,7 +98,7 @@ export function NotificationsContent() {
    -

    Focus-aware suppression

    +

    Focus-aware suppression

    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 6ab9c74f4..d84e70f21 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 d4f837f6b..a938f17d2 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 d7e7f5705..edd0c39b7 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 2a89e5356..d51a383c3 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 13b7b3ad0..4dd9d590f 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 7de66e10e..9234f05ce 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.