Notifications¶
This page names how SASE stores, displays, and delivers notifications.
Overview¶
Sase includes a notification system that surfaces important events from background
processes (axe, workflows, mentors) to the user through sase's TUI. Notifications are
stored as JSONL and persisted to ~/.sase/notifications/notifications.jsonl.
Plan, epic-plan, question, agent-launch, sudo, and task-triage approvals use the
notification row as a typed transport projection of a durable interaction gate. The
reviewed content, option-query branches, validation schemas, and hash-verified commands
live in ~/.sase/interaction_requests/<kind>/<request-id>/; sase's TUI, mobile,
Telegram, and typed CLI actions all resolve that same bundle. Sudo requests add a
terminal-only approval path; see Sudo Requests.
Remote Attention¶
When remote machines are enrolled, sase's TUI normal refresh polls their global pending
attention inventory independently of the Agents rows currently visible or followed. Each
authorized remote question or gate becomes a durable unread notification in the
Attention panel, labeled with the owning machine alias. Selecting the notification
opens the same answer/approve modal as the corresponding remote Agents row and submits
the decision through the durable sase machine attention operation.
The inbox reconciles requests by origin installation, request ID, and revision. Read and dismissed are local browsing state, never an answer to the remote request: re-polling the same revision refreshes the row's content but keeps your read or dismissed choice, along with existing mute and snooze choices. A new revision creates a fresh unread row that needs renewed review, and sase's TUI dismisses the superseded revision. When a fresh, complete host inventory no longer contains a request, sase's TUI dismisses the stale row. Only rows sase's TUI dismissed itself come back, unread, if that same revision is reported pending again. A failed, unavailable, cached, stale, partial, or paged (more results pending) host inventory does not settle missing requests, so a transient outage cannot silently remove a pending decision. If the owning alias is later removed or quarantined, submission stops with a diagnostic instead of routing the answer elsewhere.
Viewing Notifications¶
Press i on any tab in sase's TUI to open the notifications modal. The modal receives
the complete unread, non-silent inbox dataset, so every populated tab and every eligible
row remains accessible even when the backlog is large. Rows in the list show relative
timestamps (e.g., "2m ago", "1h ago") and can be marked as read or dismissed. Dismissed
rows are not gone for good: the dismissed view lists them again for
review and one-key restore. The detail pane shows the selected notification's absolute
send time alongside its relative age (sent today 13:18:42 · 4m ago), tiered as
today HH:MM:SS / yesterday HH:MM / Mon D HH:MM / Mon D 'YY HH:MM in the
configured timezone.
Modal Keybindings¶
Open the modal with i from any tab; that binding is on the generated
TUI keymaps page. Keys inside the modal:
| Key | Action |
|---|---|
j / k |
Navigate between notifications |
Enter |
Select notification (jump to PR, approve plan, etc) |
d |
Open Gate Debug for the highlighted row |
x |
Dismiss notification, or dismiss marked rows when marks are present |
u |
Restore (undismiss) the highlighted dismissed row, or marked rows |
m |
Toggle the per-row mark on the highlighted notification |
M |
Toggle mute on the highlighted notification, or marked rows |
s |
Snooze the highlighted notification, or marked rows (opens duration picker) |
e |
Open attached file in $EDITOR |
V |
Open the current image attachment in the image viewer |
Ctrl+N / Ctrl+P |
Cycle through attached files |
Ctrl+D / Ctrl+U |
Scroll file content down / up |
g / G |
Jump the detail pane to the top / bottom of its contents |
1–9 / 0 |
Jump to the 1st–9th or 10th current tab; a missing position does nothing |
[ / ] |
Cycle notification tabs |
+ |
Cycle newest-first through the selected row's +1 evidence, then default |
R |
Mark every unread notification in the active tab read (confirms first) |
S |
Toggle the active tab between sectioned and newest-first rows |
T |
Toggle between the unread inbox and the dismissed-notifications view |
Esc / q |
Close modal |
The detail-pane scroll keys work with either the notification list or detail pane
focused. Apostrophe jump mode consumes hint keys first, so g and G select matching
jump hints while hints are visible instead of scrolling the detail pane. The same
first-refusal applies to digits: 2 selects the second tab in normal mode, while '
then 2 (or a two-character hint such as 10) jumps to the matching row without
changing tabs.
Every gate-backed notification (plan, epic, question, launch, workflow HITL, custom,
sudo, task-triage, flag-triage, snooze, stale-cleanup, and required-plugin) and every
remote attention row requires confirmation (y / n) before
dismissal to prevent accidental loss of pending decisions. The same y / n
confirmation is used for bulk dismissal when at least one marked protected notification
is included in the batch.
Dismissed View¶
Press T to switch the modal between the unread inbox and the dismissed view, which
lists dismissed rows that are still unread; the title reads Notifications (dismissed)
while that view is active. Press u to restore the highlighted row (or every marked
row) to the inbox, then press T again to return to the normal inbox.
A dismissed live gate stays unread — selecting it never marks it read, and dismissing it
never does either — so it always resurfaces in the dismissed view until it is answered
or restored. Rows that are both read and dismissed (for example after R marks their
tab read) are outside the inbox's unread scope and stay out of both views; the pending
gate bundle itself remains reachable through the gate CLI (sase gate list /
sase gate show).
R is scoped to the tab you are on, not the whole inbox, and it is a wider write than
it looks: it marks the tab read in the notification store, which includes rows matching
that tab that sase's TUI has not loaded into the visible list. Because of that, it opens
a danger confirmation naming the tab (Mark Notification Tab Read?) that defaults to
Cancel; it cannot be undone from sase's TUI. The target is frozen when the prompt
opens, so switching tabs while the confirmation is up cannot redirect the write to a
different tab, and a tab with nothing in the visible list never prompts at all. The
mutation itself runs as a proc, so a slow store write does not block the modal. On
confirmation the marked-read rows leave the visible list immediately, the emptied tab
disappears from the tab strip, and the modal moves to the nearest surviving tab (or
shows No unread notifications when that was the last tab). A toast reports how many
notifications the store marked read.
Gate Detail Pane¶
Highlighting any gate-backed row — plan, epic, question, launch, custom, sudo,
task-triage, flag-triage, snooze, stale-cleanup, required-plugin, or workflow HITL —
always renders a live decision card in the right pane: a status line
(Awaiting your decision, Answered, Cancelled, Timed out, or
Gate details unavailable), the notification's context and tags, a Decision block
listing every branch in canonical query order with the primary branch marked, and an
Attachments line when the gate has files. The card renders instantly from the
notification row and enriches itself with the verified bundle a moment later without
blocking navigation. When a bundle cannot be resolved, hashed, or parsed — a deleted
directory, a corrupted request.json, or an earlier bundle layout — the card degrades
to ▲ Gate details unavailable rather than going blank; press d to open Gate Debug
and see exactly why. Every other notification, including attachment-less ones, gets a
compact summary card instead of an empty pane.
+1 Evidence¶
A sender can append corroborating evidence to an existing notification without creating
a duplicate row. The list renders a [+N] badge after the title; the count includes
older evidence dropped by bounded retention. The default detail card adds a
+1 EVIDENCE block with each retained sender, age, and note. Report cards also show the
total and latest retained evidence age in their provenance line.
Each row retains its newest 500 entries and counts older dropped entries in N. SASE
collapses whitespace in each note and stores at most 2,000 characters. Repeated entries
from the same sender remain separate occurrences.
Press + to replace the detail pane with the newest retained +1, then walk backward
through older entries. One more press after the oldest entry wraps to the normal detail
pane. Changing rows or tabs also resets the cycle. A row with no retained evidence shows
a status hint and leaves the pane unchanged.
Appending a +1 is deliberately evidence-only: it does not change the notification's
read, dismissed, muted, or snoozed state, activity ordering, or delivery cursors.
Tabs and Ordering¶
The modal renders a compact tab strip above the list whenever there is at least one tab.
It is hidden only at zero tabs, which is also when the list itself is replaced by the
No unread notifications message — so the strip never pops in and out, and the list
never shifts, as dismiss, mute, and snooze actions collapse the tabs down to one.
Every notification belongs to exactly one tab — the Rust core decides which one by a
fixed precedence, so the panel, the top-bar indicator, and the mobile snapshot always
agree; see Tags below for that precedence in full. Tabs
then sort by priority descending, with
the core's order as the tiebreak at equal priority. The tabs, at their default
priorities:
| Tab | Icon | Priority | Contents |
|---|---|---|---|
Gates |
⚑ |
60 |
Plan and epic approvals, user questions, workflow HITL prompts, launch approvals, and generic gates without a declared panel. |
| Panel | ◆ |
50 |
Gates with presentation.panel. At equal priority they keep the core's label order after Gates; remote questions and gates use Attention (?), while built-in task triage gates use Beads (◈) and woken BeadSnooze, due FlagTriage, and BeadStaleCleanup gates land there too. The shipped beads priority is 0, so Beads sits between custom tags and Snoozed and renders a ▾ mark. |
Errors |
✖ |
40 |
Axe digests, failed file hooks, and agent errors (axe, file-hooks, or user-agent with ViewErrorReport). |
General |
✉ |
30 |
Untagged, unmuted notifications with no other classification. |
Done |
# |
20 |
Notifications carrying the done tag, pinned before other custom tags. |
| Custom | # |
10 |
Other normalized notification tags, sorted alphabetically after Done at equal priority. Sudo requests declare no panel, so they land in the sudo tag tab rather than Gates. |
Snoozed |
☾ |
-10 |
Muted notifications with a future wake time — snoozed notifications and notifications for snoozed task beads alike. |
Muted |
⊘ |
-20 |
Muted notifications with no wake time. |
Each tab's icon resolves through the same chain as its color; see
Tab icons below. A tab whose effective priority
differs from its default renders one extra cell immediately after the count: ▴ (amber
#FFAF00) when raised, ▾ (grey #8A8A8A) when lowered. Compact-mode tabs keep the
mark after shedding the label. The top-bar indicator chips omit it — a chip is already
<icon><count>, and the badge's left-to-right order plus the hover tooltip
(▾ priority 0) spell out the deviation.
The first ten tabs, in that same current order, carry an unboxed shortcut digit
immediately before the icon: 1–9, then 0 for the tenth. The mapping is positional
and is resolved from the live classified order on every keypress and every render, so
dismiss, mute, snooze, mark-read, and priority changes cannot leave a stale digit. Tabs
past the tenth stay unnumbered and remain reachable with [ / ] or the mouse.
Pressing a digit with no tab at that position is a no-op that stays inside the modal.
The strip reflows to fit its measured width rather than clipping, in three tiers. Full shows shortcut, icon, label, count, and optional priority mark for every tab. When that would overflow, compact keeps the shortcut on every tab, sheds inactive labels, and retains the active name. When compact still does not fit, micro packs shortcut, icon, and count with tighter separators. Shortcut digits are never shed. Shedding labels is what keeps a tab from falling off the end of the line, where it would be both invisible and unclickable; a resize re-renders the strip only when the width actually changed.
A row with multiple tags therefore occupies exactly one tab, not one per tag; dismissing it removes the row from at most one tab's count.
Sections¶
Tabs can render their rows under ordered section headers. The shipped strategy is
enabled for Beads, where task-bead rows are grouped by task type in catalog order:
Bug, CI failure, Feature, Flaky test, and Memory, followed by Due, Cleanup, and Other
for rows without a task-type chip. Rows inside each section keep the same activity
ordering described below.
Press S to toggle the active tab between grouped sections and recent, a flat
newest-first render. The choice is per tab and lasts only for the current sase's TUI
process. Tabs without a grouping strategy stay flat and report that they have no
sections.
Within the active tab, rows are ordered newest-first by their activity time —
resurfaced_at when a snooze has expired, otherwise timestamp (see
Activity Ordering). Rows with equal activity times keep their
original arrival order, and rows whose activity time can't be parsed fall to the bottom
rather than breaking the modal. The sort runs on every modal rebuild, so live actions
like mark-read, dismiss, mute, and snooze update the visible order immediately.
Switching tabs with [ / ] or a mouse click clears modal-local marks so a hidden row
is never bulk-dismissed by accident.
Marks and Bulk Actions¶
Press m on a notification to toggle a per-row mark. Marks are scoped to the open modal
— closing the modal clears them. While at least one row is marked, x, M, and s
target every live marked row instead of the highlighted row. Gate-backed and remote
attention rows in a marked dismiss batch use the same y / n confirmation prompt as a
single dismissal.
Successful marked mute, unmute, snooze, and dismiss actions consume the acted-on marks.
Stale marks are pruned; if no live marks remain, M and s fall back to the
highlighted row rather than writing an empty batch. Marked mute uses one shared state
for the whole target set: if any marked target is unmuted, M mutes all targets;
otherwise it unmutes all targets and cancels any pending snoozes.
Mute and Snooze¶
Press M on a notification to toggle its muted state, or on marked rows to toggle the
whole marked set. Muted notifications are dimmed in the list, prefixed with ~, and
moved to the Muted tab. They are still delivered to the JSONL store, remain visible in
the modal, and get their own counted chip in the top-bar indicator — only the toast
pipeline and arrival bell ignore them.
Press s to snooze a notification, or marked rows, for 15m, 1h, 4h, or until
tomorrow morning. A marked snooze computes one deadline and applies it to every target.
Snoozed notifications are implicitly muted, but a future wake time routes them to the
Snoozed tab rather than Muted (see Tabs and Ordering), and
they display a ⏰ <remaining> badge counting down to the snooze expiry. Toggling mute
off cancels any pending snooze. The snooze deadline is persisted as a canonical UTC
instant, so the notification re-emerges from Snoozed on its own once the deadline
passes — see Snooze Expiry and Resurfacing for the
exact state transitions, timing guarantee, and recovery behavior.
Snooze Expiry and Resurfacing¶
Exact Elapsed Versus Calendar Time¶
Duration presets (15m, 1h, 4h) mean exactly that much elapsed time, so they are
added on the UTC timeline. A four-hour snooze started just before a DST transition still
expires after four real hours, not three or five. Calendar presets such as "tomorrow
morning" resolve in the configured IANA timezone first (09:00 local) and are then
converted to UTC for storage. Display formatting stays local; the stored deadline is
always a canonical UTC RFC-3339 instant.
New snooze writes accept only timezone-aware, parseable, future instants. A naive, malformed, or already-past deadline is rejected before any row changes, and a rejected bulk snooze leaves every target untouched. Snoozing a dismissed or unknown notification is likewise a no-op rather than a silent success, so the modal never shows a false "Snoozed" state — a store failure or stale row reloads authoritative state and shows an actionable error instead.
State Transitions¶
| Event | muted |
snooze_until |
read |
resurfaced_at |
Delivery result |
|---|---|---|---|---|---|
| Snooze active row | true |
validated future UTC instant | unchanged | unchanged | hidden from active delivery until due |
| Resnooze | true |
replacement future UTC instant | unchanged | unchanged | only the replacement deadline is scheduled |
| Expire active row | false |
null |
false |
expiry instant | one new activity generation becomes visible |
| Explicit unmute | false |
null |
unchanged | unchanged | timer is cancelled, not treated as an expiry |
| Dismiss | unchanged | null |
unchanged | unchanged | pending snooze is cancelled and can never alert later |
Expiry is atomic and batched: every row that is due at the same reconciliation stamps
the same resurfaced_at instant, becomes unmuted and unread, and clears its deadline.
A row that was marked read while snoozed still returns to the inbox as unread. Permanent
mutes (muted with no deadline) are never touched, and dismissed rows are skipped
entirely, so a dismissed notification can never ring later.
Timing Guarantee¶
While a supporting long-lived consumer is running, a snoozed notification becomes current within that consumer's tolerance of its wall-clock deadline:
- sase's TUI session — one second, including after suspend/resume, a restart, or a
system-clock change, and independently of
--refresh-interval. sase's TUI schedules a deadline-driven coordinator rather than relying on the general refresh tick. - Mobile gateway — the next authenticated list or detail read, which expires the row
and publishes a
notifications_changedevent so connected clients refresh. - Telegram outbound job — the next scheduled job run.
If every consumer is offline, no alert is emitted at the deadline; the durable guarantee is instead that the first later current-state read atomically catches the row up before returning any state. Expiry is therefore never lost, only deferred to the next reader.
Because expiry happens under the store lock, exactly one concurrent reader observes a
given row in its expired_ids transition metadata. Every other consumer still observes
the result through the persistent muted, read, and resurfaced_at fields, so a
losing process never misses the resurfacing. sase's TUI emits one toast and one tmux
bell per observed resurface batch and does not repeat it on later polls.
Activity Ordering¶
Every consumer orders notifications by an effective activity key rather than the raw creation time:
activity_at(notification) = resurfaced_at ?? timestamp
activity_cursor(notification) = (activity_at, notification_id)
timestamp keeps its immutable meaning as the original creation time and is what the UI
displays as the sent time. The activity key is what makes a resurfaced snooze
first-class recent activity: an old row moves to the top of sase's TUI modal, the first
sase notify list page, and the first mobile page instead of staying buried. The
notification-ID tie-breaker is required anywhere a cursor is persisted (mobile
newer_than/high-water, Telegram delivery) so two rows sharing an activity instant
cannot hide one another.
Unparseable deadlines¶
A stored row whose snooze_until is unparseable or timezone-naive is treated as
immediately due: the row resurfaces at once and is reported as an expiry, while every
new mutation path rejects such a value outright. Rows written before the resurface field
existed default to resurfaced_at: null. See
Automation migration notes.
Raw Versus Current-State Reads¶
Raw audit reads (load_notifications(), the non-expiring snapshot read) deliberately
never mutate time-driven state, so inspection and export paths cannot cause a resurface
as a side effect. User-facing "current inbox" reads
(read_current_notification_snapshot() and the CLI, sase's TUI, mobile, and Telegram
projections built on it) atomically expire due rows under the store lock before
projecting rows, counts, expired_ids, and the next active deadline.
Top-Bar Indicator¶
The inbox: group in the TUI top bar renders one colored <icon><count> chip per
notification-panel tab (see Tabs and Ordering), in the panel's own
left-to-right order, so the badge and the panel always agree on what each count means:
- Nothing pending — a dim
inbox: 0. - Snoozed only —
inbox: ☾4: the count renders in the Snoozed tab's resolved color at a dimmer weight than an actionable count, prefixed by the Snoozed tab's own icon instead of a trailingzsuffix. - Anything else — one
<icon><count>chip per visible tab, each the tab's icon and count in its own resolved color at full weight, joined by a single space, for exampleinbox: ⚑2 ✖3 ◈1. Each chip is self-identifying, so there is no separator glyph between chips — the·separates groups, not tab chips — and no✉anchor: oncegeneralowns✉as its own tab icon, prefixing the whole badge with it would render the same glyph twice, meaning two different things. As soon as any non-snoozed tab has a count, the Snoozed chip drops out of the badge entirely — it does not compete for the limited chip budget — but the snoozed count still appears in the tooltip. - Overflow — at most
ace.notification_indicator_max_countschips (4 by default), taken in panel order; any remaining tabs collapse into one trailing dim+Kchip, joined by the same single space. Every suppressed tab is still described in the tooltip.
The chip budget is display-only. It does not limit the notification modal dataset, tab
counts, or the Agents Enter action. On the Agents tab, Enter searches the complete
unread dataset for the selected agent's pending gate (every gate kind), so older pending
agent decisions remain reachable regardless of indicator overflow or backlog size.
Hovering the indicator opens a tooltip briefing: a header count of unread rows (snoozed
and muted rows are informational and excluded from that header count) followed by one
line per tab showing its icon, its count, its oldest unread activity (oldest 14m ago)
or, for the Snoozed tab, when it next wakes (next wakes in 43m). Tab labels in the
tooltip are colored the same as their indicator chip, so the tooltip doubles as a
legend. See Tab colors and
Tab icons for how each tab's color and icon are
resolved.
Silent notifications never contribute to the indicator (see Silent Notifications below).
Visual and Audible Delivery¶
New unmuted notifications remain visually prominent through the top-bar indicator and
action-specific toasts. A genuinely new PlanApproval or EpicApproval rings once on
arrival, alongside its priority inbox row, warning toast, and the producer's desktop
notification. sase's TUI toast says Tale ready or Epic ready; an epic adds the
gate-time phase, dependency-wave, and non-zero phase-size counts, while batched toasts
count tales and epics separately. Already-handled plan reviews discovered during polling
and the intermediate post-approval handoff remain silent. Task triage, stale-cleanup,
questions, launch/custom/HITL/sudo gates, errors, agent completions, and ordinary
notifications retain their arrival bell. Priority actions include PlanApproval,
EpicApproval, UserQuestion, LaunchApproval, TaskTriage, BeadSnooze,
FlagTriage, BeadStaleCleanup, PluginsRequired, GateExecutionFailed,
JumpToMentorReview, and RemoteAttention.
Snooze expiry is an explicit reminder chosen by the user and, by default, remains audible for every notification class, including a snoozed tale or epic review. A delivery rule that matches the resurfaced row still applies to it.
Every toast and arrival sound described here is the default. ace.notification_rules
overrides TUI behavior per notification and may suppress Telegram delivery; see
Delivery Rules.
Delivery Rules¶
ace.notification_rules is an ordered list of rules that decide, per new notification,
whether sase's TUI shows a toast, how the arrival is announced (the terminal bell, a
sound file of your choosing, or silence), and whether the row may be delivered to
Telegram. With no rules configured every notification toasts and rings the bell once per
poll, exactly as described above, and Telegram delivery is permitted (subject to the
transport's own eligibility checks).
ace:
notification_rules:
- name: quiet-task-beads
description: Task-bead triage is too noisy to announce right now.
match:
tab: beads
toast: false
sound: none
- name: mac-chime
sound: /System/Library/Sounds/Glass.aiff
| Key | Type | Meaning |
|---|---|---|
name |
string | Label for sase notify rules, doctor output, and diagnostics |
description |
string | Free prose explaining why the rule exists |
priority |
integer | Evaluation weight, -1000..1000; higher is consulted earlier |
match |
mapping | Criteria. Omitted or {} matches every notification |
toast |
boolean | Whether a TUI toast is shown |
sound |
string | bell, none, or the path of a sound file |
telegram |
boolean | Whether Telegram delivery is permitted (false suppresses) |
Every key is optional, and an unknown key drops the whole rule rather than being
ignored: a rule applies exactly as written or not at all. A rule with no name is
reported as rule[<index>], counting from 0 across the rules that survived. sase
versions that predate delivery rules report ace.notification_rules as an unknown key
in sase config validate and sase doctor.
Matching¶
Six criteria, each drawn from something visible on the notification row:
| Criterion | Matched against |
|---|---|
tab |
The tab that owns the row (see Tabs and Ordering): beads, hitl, errors, general, a tag, and so on |
sender |
The notification's sender |
action |
The notification's action; an empty string for a row with no action |
tags |
Any one of the notification's tags |
title |
The first note, which the panel shows as the row headline |
note |
Any line of the notification's notes |
- Every value is a case-insensitive glob:
*,?,[abc],[a-c], and[!abc]. A value with no wildcard is therefore an exact, case-insensitive match. There is no substring or fuzzy matching:title: "Plan ready"does not matchPlan ready for review, whiletitle: "Plan ready*"does. Write[*]for a literal*. A[with no closing]matches a literal[. - A criterion takes one string or a list of strings; a list is any-of.
- Several criteria in one
matchare all-of. tab: gatesis an alias fortab: hitl, because the panel labels that tabGates. The muted and snoozed tabs are__muted__and__snoozed__.
The tab a row matches is the tab the panel shows it under, because both come from the
same core classification. sase notify rules --explain reports the tab of the row as it
is stored now, so a muted or snoozed row reports __muted__ or __snoozed__.
Resolution¶
Rules are consulted in descending priority, with ties broken by position in the merged
list. For toast, sound, and telegram independently, the first matching rule
that sets that field decides it. A field no matching rule sets keeps the default: toast
shown, sound bell, Telegram permitted. A rule that sets only toast therefore never
blocks a later rule from choosing the sound or suppressing Telegram. telegram: true
permits Telegram delivery but never bypasses the transport's read/mute/silent,
first-run, cursor, or other eligibility checks.
The merged list follows config layering: rules from ~/.config/sase/sase.yml come
first, then rules from machine overlays such as sase_<machine>.yml, then project-local
rules. A machine-wide sound rule in an overlay can sit under a narrower rule in
sase.yml without re-stating it. Use priority only when a later layer must
deliberately override an earlier one.
Because the first match wins, no negation is needed. To silence everything except gates, say what gates do, then silence everything else:
ace:
notification_rules:
- name: gates-announce
match:
tab: gates
toast: true
sound: bell
- name: silence-the-rest
toast: false
sound: none
A gate row matches gates-announce, which decides both fields before the second rule is
consulted. Every other row falls through to silence-the-rest, whose empty match
matches all notifications.
Sounds¶
sound is a single value with two reserved words, compared case-insensitively:
| Value | Announcement |
|---|---|
bell |
The terminal bell through tmux, three rings (the default) |
none |
Silence |
| anything | The path of a sound file, with ~ and $VAR expanded and then played |
A file literally named bell or none is addressed as ./bell. sase's TUI picks the
first audio player found on PATH:
| Platform | Players, in order |
|---|---|
| macOS | afplay |
| Linux | paplay, aplay, ffplay -nodisp -autoexit |
A missing file, a missing player, a player that fails or hangs, or an unsupported
platform never interrupts the poll: the announcement is simply silent. A player that
runs longer than 30 seconds is stopped. bell rings through tmux only, using the
tmux_ring_bell helper found on PATH: when sase's TUI is not running inside a tmux
pane ($TMUX_PANE unset) or the helper is missing, bell is silent. Playback runs on a
worker thread, so it cannot stall the TUI. A sound file also plays detached from the
poll that triggered it — a long chime never holds up the notification tick or the agent
refresh behind it — and only one file plays at a time, so notifications arriving faster
than the file is long cannot stack up players.
Each poll announces at most one sound. It is the resolved sound of the first new
notification, in activity order, whose sound is not none; if every arriving row
resolves to none, the tick is silent. A burst of five notifications is one chime, not
five.
What a rule does not change¶
toast: false suppresses the toast, and sound: none the sound; nothing else changes.
The notification is still stored, still unread, still counted in the top-bar indicator,
and still listed in the panel under its tab. Rules never affect whether a notification
is created, read, muted, or snoozed. Suppressed rows are also left out of grouped
toasts: a batch of six with three suppressed produces three individual toasts rather
than one group of six.
telegram: false suppresses only the Telegram announcement (including its attachments
and keyboards). The row stays stored and unread, gate requests and TUI access are
unchanged, direct Telegram command replies still work, and already-sent messages are
left alone. Suppression is evaluated at each outbound poll: it does not mutate the
notification or advance the delivery cursor, so a later eligible row still advances the
cursor normally and removing the rule may make older unread rows ahead of the cursor
eligible again. There is no suppression ledger or backfill.
Example: keep Beads rows in SASE without sending them to Telegram:
ace:
notification_rules:
- name: quiet-task-beads-telegram
description: Keep Beads notifications in SASE without sending them to Telegram.
match:
tab: beads
telegram: false
Inspecting rules¶
sase notify rulesprints the merged rules in the order they are consulted, with the config layer each came from, its criteria, and the behaviors it sets. Entries that were dropped as malformed are listed with the reason.sase notify rules --explain <id>(-e) takes a stored notification ID or unique prefix and prints the fields the rules match on, then fortoast,sound, andtelegramthe rule that decided it or that the default applied. Telegram is reported as rule permission/suppression, not a guarantee that Telegram is enabled or that a message was sent. Add-j/--jsonto either form for machine-readable output.sase doctor -C config.notification_rulesflags unknown keys, amatchthat is not a mapping, a glob with an unclosed[, an empty criterion list, asoundfile that does not exist, a sound file with no audio player available on this platform, and a rule that sets neithertoast,sound, nortelegram.
Related¶
- Notification types
- Notification tags
- Notification CLI
- Command-backed interaction gates
- Notification storage
- Automation migration notes
_[project]: A sase project is a named unit of work registered with SASE. _[workspace]: A sase workspace is a numbered clone of a project's primary repo, managed by the workspace store and tracked in that project's registry.json. _[agent]: A sase agent is an agent session or a single agent that does not belong to a session. _[prompt]: A prompt is the text SASE hands to a provider to start an agent turn. _[macro]: A macro is a reusable prompt triggered with #name in agent prompts. _[Patch]: A Patch is SASE's local unit of change. _[bead]: A bead is one tracked work item in SASE's git-native issue tracker. _[plan]: A plan is a persisted, approved artifact recording the intent behind agent work. _[TUI]: The TUI is SASE's terminal interface for running agents and reviewing results. _[epic]: An epic is an executable multi-phase plan run with sase bead work. _[gate]: A sase gate is a durable, command-backed request for one user decision, created when work must pause for the user. _[monitor]: A sase monitor is a monitor turn that runs one long command under a detached supervisor, so the command outlives the agent that started it. *[stitch]: A stitch is the lightweight ordered change record inside a Patch's STITCHES: section.