Scheduler housekeeping¶
This page describes the scheduler housekeeping routine.
housekeeping (1-hour interval)¶
Periodic maintenance:
| Job | Description |
|---|---|
error_digest |
Send error notification digests (creates ViewErrorReport notification action) |
notification_store_compact |
Archive old dismissed notifications out of the live JSONL store |
managed_tmp_reap |
Prune stale scratch under every registered managed SASE temp root |
proc_runtime_sweep |
Prune stale rowless proc runtime directories |
disk_pressure |
Notify on disk pressure and run owner-safe cleanup early |
bead_stale_cleanup |
Sweep stale sub-threshold ready task beads into one BeadStaleCleanup gate |
gate_turn_reclaim |
Settle pending gate turns whose gates answered, canceled, expired, or vanished |
artifact_link_backfill |
Derive and reconcile artifact links, drain reads, and repair refs after a rename |
artifact_run_prune |
Preview old ace-run directories and empty shard cleanup |
The error_digest job summarizes recent errors into a digest file stored at
~/.sase/axe/error_digests/digest_<timestamp>.txt. The notification includes a
ViewErrorReport action that opens the digest in $EDITOR when selected in sase's TUI
notification modal. For failed script subprocesses, the digest includes the job run ID,
exit code, source log path, and a bounded subprocess output excerpt captured at failure
time. The excerpt is redacted, stripped of terminal control sequences, and retained in
recent_errors.json, so the digest remains useful even after per-run logs are pruned.
Silent, missing, unreadable, malformed, and truncated output are called out explicitly
instead of being reported as a Python traceback.
The notification_store_compact job bounds ~/.sase/notifications/notifications.jsonl
by moving dismissed rows older than 3 days into notifications-archive.jsonl. Unread
and still-actionable rows stay in the live file. sase's TUI snapshot reads are memoized
against an mtime+size token, so this pass lives on housekeeping rather than the TUI
refresh cadence.
The managed_tmp_reap job bounds every managed SASE temp root writers actually used.
Each get_sase_managed_tmpdir() call registers its resolved root ($SASE_TMPDIR, else
~/.sase/tmp) in a Rust-owned registry at $SASE_HOME/managed_tmp/roots.json
(sase-core's managed_tmp_roots module), and this job reaps the effective root plus
every registered root that still exists instead of only the root its own environment
resolves. A root nested inside another covered root is folded into it, so a per-launch
root registered under an already-registered ancestor is not reaped or counted twice. The
actual age/pressure decision runs in sase_core_rs (sase-core's managed_tmp crate);
this Python job resolves the configured horizons and thresholds and calls that binding
once per root. Horizons are per subdirectory: command scratch (editors/, wrappers/,
viewers/, commit-messages/, agent-tmp/, …) goes after 12 hours by default, handoff
files (handoff/, gh-diffs/, muse-prompts/ — a provider re-reads the latter
mid-run) after 3 days, build targets (cargo-targets/, build-targets/) after 1 day,
and artifacts sase's TUI and screenshot tooling reads back (launch-prompts/,
screenshots/, workflow-artifacts/) after 14 days. Launched agents default
TMPDIR/TMP/TEMP, CARGO_TARGET_DIR, and CARGO_BUILD_BUILD_DIR to per-launch
directories under those managed buckets, so shell scratch and Cargo targets do not fall
back to host-global /tmp. Launched agents also get line-tables-only debug info for the
dev and test Cargo profiles, which keeps per-launch targets small. They get
CARGO_INCREMENTAL=0 by default, or CARGO_INCREMENTAL=1 when
managed_tmp.agent_cargo_incremental is
enabled (athena only, where the splitting rustc wrapper runs metadata-only units
incrementally direct and strips incremental from codegen units before sccache). A runner
also removes its own launch-assigned agent-tmp/ and cargo-targets/ children at exit
when they still match its exported TMPDIR and CARGO_TARGET_DIR and it can prove,
through procfs, that no live process still references either tree (by environment or
working directory). That removal goes through the same Rust reaper as the hourly pass.
Monitor/gate handoffs leave cleanup to the reaper: a follow-up turn mints a fresh
scratch key from launch_spawn._managed_agent_scratch_env, so the handoff outcome skips
runner-exit cleanup and the old key goes quiet. The hourly pass then runs a dead-launch
backstop over the launch-keyed buckets (agent-tmp/, cargo-targets/,
build-targets/): entries whose newest descendant write predates
managed_tmp.dead_launch.grace_seconds (2 hours by default) and that the batch procfs
observer reports as held by no live process are removed largest-first within the removal
budget. Held and incomplete-observation entries are preserved and counted
(dead_launch_preserved_live, dead_launch_preserved_incomplete), and hosts without
readable procfs report dead_launch_observer: unobservable and leave the age horizons
as the fallback. A zombie or dead process has already released its address space and
cwd, so it is never counted as a holder or as an incomplete observation.
Each run removes at most 2,000 entries by default so a long-neglected root converges
over several passes instead of stalling one. The reaper also runs a pressure pass when
the managed root grows beyond 16 GiB by default, or when the filesystem holding it falls
below SASE's shared disk-pressure warn threshold: the larger of 3 GiB and
disk.pressure.warn_free_percent (5% by
default) of that filesystem. Under pressure, aged entries of at least 64 MiB in
regenerable build-output buckets (cargo-targets/, plus build-targets/), and
cargo/core target-shaped top-level residue, are removed largest-first until the root is
estimated below 8 GiB, available space is estimated back above that same warn threshold,
or the removal budget is reached. Pressure pruning is liveness-aware while the procfs
observer is available: a held entry is never removed, an entry whose liveness is
incomplete is preserved, and an unheld entry needs only the dead-launch grace rather
than the pressure minimum age. Pressure pruning otherwise waits 12 hours, but once the
free-space floor is breached it uses the lower configured low-space age, 1 hour by
default, without weakening the fresh-descendant check. The horizons, removal budget,
root-size limits, pressure ages, and minimum pressure entry size are configurable under
managed_tmp in sase.yml; see
Configuration. The housekeeping job,
disk_pressure, and sase disk reap all derive the free-space floor and recovery
target from the disk.pressure policy rather than from
managed_tmp.pressure.min_available_bytes and recovery_available_bytes. Generic agent
scratch, handoff buckets, artifact buckets, unknown buckets, symlinks, and build trees
with fresh descendants are not early pressure candidates. The job summary reports
scanned, selected, and removed counts with their byte totals, the same counts
split into ordinary_*, launch_*, dead_launch_*, and pressure_* passes,
dead_launch_observer, dead_launch_preserved_live,
dead_launch_preserved_incomplete, pressure_trigger, pressure_available_bytes,
pressure_recovery_available_bytes, pressure_min_age_seconds, deindexed, skipped,
failed, incomplete_observations, and capped=1 when it hit that budget, plus the
roots it scanned and their count as roots_scanned. Every run logs one line per root
it scanned plus an informational line listing all scanned roots. A mismatch between the
effective root and the default $SASE_HOME/tmp is harmless — both are reaped — so there
is no warning for it. Reaped directories are dropped from the agent artifact index too,
since a workflow launched without an explicit artifacts_dir gets one under
workflow-artifacts/. It lives on housekeeping rather than an interactive path
because the first pass over a neglected root walks tens of thousands of entries.
The proc_runtime_sweep job bounds ~/.sase/procs/runtime. Both halves of proc runtime
retention run in the Rust proc runtime-retention owner. Proc-row retention deletes
runtime directories for rows it actually pruned in the same operation as log cleanup.
This hourly sweep handles historical rowless runtime directories separately: it removes
only canonical proc-id directories that are direct, non-symlink children of the runtime
root, older than the configured procs.runtime_orphan_horizon_seconds, absent from the
proc store after a locked re-read, and within procs.runtime_orphan_max_removals.
Fresh, invalidly named, symlinked, active, or otherwise retained entries are preserved.
The summary reports scanned, selected, removed, skipped, errors,
reclaimable_bytes, reclaimed_bytes, and capped.
The disk_pressure job measures the filesystems that hold the managed temp root and
~/.sase (once per distinct filesystem) and classifies them through the Rust
disk-pressure policy: a filesystem is under pressure when its free space falls below the
larger of 3 GiB and
disk.pressure.warn_free_percent of its size.
With no pressure, the job exits with reason space_ok without scanning the disk
footprint. Under pressure, it logs the largest owners from sase disk list and, when
any qualify, sends a notification naming them; when the inventory's unattributed row
(filesystem used minus attributed bytes on the SASE_HOME filesystem) exceeds the
largest attributed owner, the notification leads with that row instead of pointing at a
small owner. The job then runs unattended owner-safe cleanup early: the managed-temp
reaper, using the measured free space and warn threshold as its pressure floor and
recovery target (with the same dead-launch backstop and liveness-aware pressure the
hourly pass runs), and the proc cleanup owner (proc-row and log retention plus the
rowless runtime sweep). Those owners encode their own deletion policy, and one owner's
failure is reported without skipping the next. Workspace Git-object compaction is left
to an explicit sase disk reap --apply, and artifact run directories, backups, and
unowned Cargo-shaped strays are reported for human action; this job never touches them.
The summary reports the free percentage, effective warn threshold, owner and step
counts, and how many steps changed or failed.
The bead_stale_cleanup job is the other half of the task-bead +1 bar. Ready task
beads that never clear their
effective +1 bar stay ready (the
five-minute bead_task_triage job withholds their TaskTriage gate) and would
otherwise accumulate forever. Once at least
bead.task_triage.stale_cleanup_min_beads of
them have sat below that bar for
bead.task_triage.stale_after_days days, this
hourly pass raises one human-only BeadStaleCleanup gate for the whole backlog — one
gate across every enabled project, not one per project. The offered roster is capped at
50 beads, oldest first, with a (project, bead_id) tie-break; any remainder is named in
the preview as omitted_count and is offered on a later tick. Lane state holds the
pending request, a generation counter, and a fingerprint over the offered roster plus
the three thresholds (not the pinned stale_as_of date), so an unchanged roster leaves
the pending gate alone and a changed roster replaces it. When the backlog drops below
the bar the pending gate is canceled. A project whose store cannot be read is skipped
and cannot cancel a healthy pending gate, because the true roster is then unknown. Run
sase axe job run bead_stale_cleanup to raise or refresh that gate without waiting for
the hour.
The gate_turn_reclaim job is the backstop for
gate turns. It scans gate-turn
session members across projects, settles turns whose gate bundle is already terminal,
cancels gates that reached their own deadline, and force-settles a turn as lost once
gate.turn.reclaim_grace_seconds (one hour by default) has passed after that deadline.
Reclaim skips a turn whose gate creation is still running (its session's gate-creation
lock is held), so an %auto gate that is still executing its selected commands is never
marked lost; a turn whose creator died before recording a bundle is still settled lost
on a later pass. It then diagnoses settled gates whose requested successor never
recorded, reading the agent artifact index once per pass and resuming from a per-project
cursor. Work left when the pass-wide time budget runs out is deferred to the next tick.
The artifact_link_backfill job runs four bounded jobs per enabled project. It sweeps
older documents for deterministic derived links, resuming from a per-project checkpoint
when one tick's budget is exhausted; drains audited-read outbox rows whose agents have
since published; reconciles cross-workspace local aggregates; and repairs dangling refs
from Git rename history. A large derivation backlog converges over several hourly ticks
instead of rescanning the full corpus each time. Run
sase axe job run artifact_link_backfill for an immediate pass. The link model and
doctor counters are documented in Artifact Links.
The artifact_run_prune job is a read-only preview for old artifacts/ace-run/
directories. Protection facts are gathered in Python, while the preview classification,
symlink safety check, and empty-shard walk run in the Rust run-retention owner. The
preview keeps the newest artifacts.retention.keep_recent_run_months calendar months
whole and protects runs that are incomplete, running, waiting, or asking a question;
runs referenced by artifact-file rows, text refs, agent names, or continuation ancestry;
runs tied to non-closed beads; and symlinked run directories. If continuation retention
planning fails, every candidate is protected (continuation_unavailable) instead of the
job crashing. The summary reports candidates, selected run directories,
empty_shards outside sase's TUI startup watch window, reclaimable bytes,
protected, and unavailable protection sources; any unavailable source turns the run
into check_error. Whenever the preview finds candidates or protection problems, the
job upserts an Axe ViewReport notification that names the preview command
(sase artifact prune-runs). The notification is deduplicated by a fingerprint of the
findings, so an unchanged preview adds a +1 note to the existing notification rather
than a new one. Artifact-run deletion is currently preview-only; apply requests fail
closed before any run directory, empty shard, or artifact-index row is removed.
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.