Notification storage¶
This page describes where SASE stores notification state.
Storage¶
Notifications are stored in JSONL format at ~/.sase/notifications/notifications.jsonl.
The production store backend is sase_core_rs: appends and state mutations take a
shared sidecar lock, and rewrites use a tempfile plus rename so multiple axe processes
and the TUI can access the file without truncate-before-lock exposure. Common state-only
updates such as mark-read, mark-all-read, mute, snooze, and dismiss use a count-only
Rust mutation path unless the caller needs rehydrated notification rows; this keeps
inbox counters cheap when sase's TUI or a bridge process only needs mutation metadata.
sase's TUI snapshot reads memoize the parsed store against the JSONL path, the
include_dismissed flag, and an mtime+size change token, so an unchanged live file is
not re-parsed on the TUI refresh cadence. A parsed snapshot is cached only when the
change token is the same before and after the read, so a write that lands mid-read is
picked up by the next read instead of being masked. A due next_snooze_deadline still
forces a re-read when the caller asked to expire snoozes.
The store keeps itself O(live). The hourly notification_store_compact housekeeping job
drives compaction so a multi-megabyte re-parse does not sit on an interactive path. When
notifications.jsonl crosses 4 MiB or holds at least 1,000 dismissed rows, a
cache-missing read or rewrite may still compact under the same exclusive lock: dismissed
rows older than a 3-day retention window are appended to a sibling
notifications-archive.jsonl and dropped from the live file, then the live file is
replaced atomically so a crash mid-compaction loses no row. Snoozed rows are never
archived while snoozed, and dismissed rows inside the retention window stay put so the
inbox UI's include_dismissed view is unchanged. Unread and still-actionable rows stay
in the live file. sase logs pack reads the archive alongside the live file, so a
window older than the retention period still packs complete.
The Rust store also owns every temporal semantic: it validates and normalizes snooze
deadlines, expires due rows atomically under the same lock as the read, stamps
resurfaced_at, and reports both expired_ids and the earliest remaining
next_snooze_deadline alongside the projected counts. Consumers schedule their timers
from that projected deadline instead of polling. Concurrent expiring readers converge on
one store state without losing appended rows.
Source: src/sase/notifications/
Related¶
_[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.