Silent recipe sessions
Some recipes run dozens of times a day in the background — newsletter summarizers, link-archivers, audit cycles — and you don't want to see their sessions in the inbox or sidebar, period. Silent mode hides the orchestrator session from the inbox and the project sidebar for the entire run — including attention statuses like needsyou, error, and stalled — then auto-archives the session inline on clean completion (no "Archive ses…" row in the inbox). Silent really means silent.
What it is
Some recipes run dozens of times a day in the background — newsletter summarizers, link-archivers, audit cycles — and you don't want to see their sessions in the inbox or sidebar, period. Silent mode hides the orchestrator session from the inbox and the project sidebar for the entire run — including attention statuses like needs_you, error, and stalled — then auto-archives the session inline on clean completion (no "Archive ses…" row in the inbox). Silent really means silent. If a silent run finishes (any terminal state — clean, errored, aborted, or soft-refusal), the engine's finalize() clears the flag immediately. If the engine never reaches finalize() because the process died, the 6-hour silent-run watchdog rescues the row. And if the engine finished but somehow forgot to clear the flag, the watchdog's orphan-flag sweep catches it on its next tick — see below.
Silent mode is opt-in. The default for every recipe is the existing visible behavior — orchestrator session shows in the sidebar with a green dot during the run, and on completion you get an inbox approval to archive it. The new toggle just gives you a per-recipe (and optionally per-cron-job) escape hatch from that approval pile-up.
Trade-off you accept by turning silent on: a silent run that hangs without ever finalizing (e.g., the engine crashes mid-flight) is invisible until the 6-hour watchdog fires. That latency is the cost of "silent really means silent." Earlier designs surfaced silent sessions on attention statuses; that was reverted on 2026-05-13 after newsletter summarizers kept lighting up the inbox at the exact moment users least wanted them visible.
Where to find it
How to turn it on
There are three controls, layered:
1. Per-recipe toggle (the default)
Open the Recipes view, click any recipe to edit it, scroll down to Silent on success in the Recipe Editor. Flip the toggle on and save. From this point forward every run of this recipe — manual, scheduled, agent-triggered — defaults to silent.
The toggle's helper text in the editor reads:
Hide this recipe's session from the sidebar and inbox for the entire run — even if it errors out mid-flight. Auto-archives on completion. Stuck runs become visible after 6 hours.
That's the spec. The flag is stored on the .recipe.json file alongside the rest of the recipe config — it travels with the recipe across import/export, backup, and /recipes/run calls.
2. Per-cron-job override (the tri-state)
Cron jobs can override the recipe-level default in either direction. Open Cron Jobs → edit a job that runs a recipe → the Visibility card is a 3-way radio group:
- Use recipe default — inherit whatever the recipe's "Silent on success" toggle says (the common case; leave it on this).
- Force silent — this cron's runs are always silent, even if the recipe itself isn't a silent recipe.
- Force visible — this cron's runs always surface in sidebar/inbox, even if the recipe is silent. Useful when a recipe is normally silent but you want to babysit one specific schedule for a while.
The cron-level setting wins over the recipe-level setting. If you flip a recipe to silent, every cron job pointing at it that's still on "Use recipe default" will quietly go silent on its next run — no extra clicks needed.
3. Settings → Features → "Clear archive-approval pile-up"
If you turned silent mode on after the fact, you'll already have a pile of unresolved "Archive ses…" rows in the inbox from past visible runs. Open Settings → Features, scroll to Clear archive-approval pile-up. The button label shows the live count (Clear 12 approvals) and is disabled when the count is zero. Click it — every pending session-archive approval that came from a recipe run is archived in one sweep. Approvals from other sources (manual archives, cron-spawned sessions, etc.) are untouched.
The count refreshes live as the inbox changes, so you can use this card to drain a pile without reopening Settings.
4. Settings → Features → "Surface stuck silent sessions" (manual recovery)
The 6-hour watchdog is the safety net for crashed runs, but sometimes you want recovery now — you noticed a project chip badge that doesn't match the inbox, or you remember kicking off a silent run that should've finished hours ago and you don't feel like waiting until hour six. Open Settings → Features, scroll to Surface stuck silent sessions. The button label shows the live count (Surface 2 sessions) and is disabled when the count is zero. Click it — every silent session that matches the watchdog's selector (silent flag still set, status not in the running/starting mid-run carve-out, no ended_at) gets its silent flag cleared in one sweep, exactly as if the watchdog had just fired against it.
After surfacing, the rows re-enter the regular sidebar and inbox filters with whatever terminal status they ended on (error, needs_you, stalled, etc.) — same as the watchdog's output. The count refreshes live via SESSION_STATUS_CHANGED so you can use the card to drain matches without reopening Settings.
This is a manual lever for the same rare condition the watchdog catches automatically. You're trading the 6-hour latency of the safety net for an immediate surface; the end state is identical.
How it behaves
Two sessions per run: orchestrator vs worker
A recipe run spawns two distinct sessions (or two kinds — depending on execution mode):
- Orchestrator session — the engine's bookkeeping/coordination row. The user-visible "this recipe is running" indicator. Always exists, never executes step prompts itself.
- Worker session(s) — where the step prompts actually run as Claude Code processes. In
executionMode: "multi-session"(the default for multi-step recipes that need clean context per step) there's one worker per step. InexecutionMode: "same-session"(digest-style recipes that thread context across all steps) all steps share one worker session.
Silent mode hides both kinds during the run — both rows stamp silent = 1. Both must reach archived before finalize() clears the flag, otherwise the worker re-enters the visible set on flag-clear and pops into the inbox as needs_you. The engine handles this transition through different code paths per mode:
- Multi-session — each step's
executeRunStep()forces a lingeringneeds_you→endedand callsshouldAutoArchiveStepSession()→archiveSession()before the next step begins. - Same-session —
runSameSessionLoop()callsarchiveSilentSameSessionWorker()on every clean exit, which mirrors the multi-session cleanup: force-end any lingeringneeds_you, then route the archive decision throughshouldAutoArchiveStepSession({ isSilent: true, … }).
For silent runs, the archive decision short-circuits the trouble-marker check: a step summary containing STEP_FAILED: or similar recovery-style prefix is normally treated as a signal to leave the row visible for inspection — but a silent recipe is hidden by intent, so the marker text is irrelevant. The thing that surfaces a silent worker is an engine-level abort (abortRequested, run status not 'completed'), not summary text. Silent run aborts skip the archive entirely so the user can investigate; clean exits always archive.
What "clean finish" means
A recipe run is treated as silent-clean only when the orchestrator session ends in one of the terminal-success states the engine emits at finalize:
- The last step completes without throwing.
- No step transitioned the orchestrator session into
errororneeds_you. - The run reached
completedstatus in the orchestration runs table.
A clean finish skips the archive-approval inbox row — the engine archives both the orchestrator session (always inline) and the worker session(s) (per the mode-specific paths above). A non-clean finish (any of those conditions failing) does NOT automatically surface the session. The session stays hidden until the engine's finalize() runs — which it does on every terminal path (clean completion, abort, error, soft-refusal). finalize() calls clearSessionsSilentForRun(runId) unconditionally inside the same if (this.runId) block that updates orchestration_runs, so the flag clears in the same logical step as the run ending. At that point the row re-enters the regular sidebar/inbox filters as an error (or whatever terminal status the engine ended on). If the engine never reaches finalize() at all (process crash mid-step), the session stays hidden until the 6-hour stuck-run watchdog fires.
This is intentional. Earlier designs cleared the silent flag the moment a session entered an attention status (needs_you / error / stalled), which meant mid-run hiccups in newsletter summarizers and audit recipes would light up the inbox exactly when the user did NOT want them visible. That bypass was removed on 2026-05-13 — silent really means silent. You accept some surface-latency on stuck runs in exchange for a quiet inbox.
Stuck silent run watchdog (the safety net)
Because silent flips a session out of the visible filters AND attention statuses no longer auto-clear the flag, any silent run whose flag survives past the engine's reach would otherwise stay permanently invisible — not just "you'd never know it failed", but "you'd never know it existed". The one-hour-tick silent-run watchdog in the main process catches two failure modes per tick:
Sweep A — stuck-run rescue. For runs whose engine crashed (or was OS-killed) before finalize() could write ended_at, the watchdog scans for any silent recipe run that's been running more than 6 hours with no ended_at timestamp and:
- Flips its orchestration run status to
errorand stampsended_atto now. - Clears the session's silent flag so the row re-enters the visible set.
- Emits a
session-status-changedpush (statuserror) so the renderer surfaces it without needing a refetch.
After Sweep A fires, you'll see the session in the inbox with an error status — same as any other failed session, no special UI for "this was silently stuck for 6 hours." The 6-hour threshold is intentionally generous: a long audit recipe legitimately takes 1-2 hours, and the watchdog needs enough headroom to never misfire on slow-but-healthy runs. Once Sweep A has surfaced a row (set ended_at), subsequent ticks skip it idempotently.
Sweep B — orphan-flag backstop. Catches the opposite failure: a run whose engine reached finalize() cleanly, updated orchestration_runs.ended_at, but somehow left sessions.silent = 1 set on the run's step sessions. The selector is silent = 1 AND is_deleted = 0 AND (orchestration_run is closed OR missing). For each match, Sweep B clears just the silent flag — it does not touch orchestration_runs.status and it does not invent a fake error push. The session-status-changed push carries the row's actual current status (whatever the engine wrote: needs_you, ended, archived, etc.), so the renderer routes the row into the right bucket without refetching. Sweep B has no time cutoff: any orphan is surfaced on the next tick, because the engine clear is supposed to be synchronous and any flag that survived it is a bug to be reversed immediately. Telemetry distinguishes the two sweeps by reason (watchdog vs watchdog-orphan) so the analytics don't conflate a healthy backstop firing with an unhealthy crash recovery.
The watchdog is one of three clearers in the system: the engine's finalize() (every branch — clean or dirty), the 6-hour stuck-run sweep, and the orphan-flag backstop. It is not configurable from the UI — it's a fail-safe, not a tuning knob. If you find yourself hitting Sweep A regularly, the right fix is to look at why those recipes are taking so long, not to widen the threshold. If you find yourself hitting Sweep B regularly, the right fix is to find the bug in the engine clear — Sweep B is a backstop, not a primary mechanism.
Who can spawn silent sessions
Silent sessions are produced exclusively by the orchestration engine — you can't manually flip a regular Claude session to silent from the UI, and there's no IPC route that creates a silent session ad-hoc. The flag flows from RecipeConfig.silent (or the cron's silentMode = 'force_silent' override) into a snapshot on orchestration_runs.silent at run start, then onto sessions.silent for the spawned orchestrator session for the lifetime of the run. There are exactly three clearers: the engine's finalize() (every branch — clean or dirty, via clearSessionsSilentForRun(runId)), the watchdog's 6-hour stuck-run sweep (for runs the engine never finished), and the watchdog's orphan-flag sweep (for flags the engine somehow left behind). Attention-status transitions do NOT clear the flag — that bypass was removed on 2026-05-13. There's no other writer for a recipe silent session. (A session-type cron with force_silent spawns its own silent session on a separate, run-less path — cleared/archived by reapSilentCronSessions, not these three; see below.)
Session-type cron jobs (force_silent)
Silent mode is no longer recipe-only. A session-type cron job (one that spawns a plain Claude session, not a recipe run) honors the same tri-state: set its Visibility radio to Force silent and the src/main/services/cron/cron-session-executor.ts spawns the session hidden (sessions.silent = 1, source cron-session). Force visible / Use recipe default spawn a normal visible session, and Run-Now of a force_silent job also spawns silent (silent is a property of the job, like a recipe's).
A bare cron session has no orchestration run, so the three recipe clearers above do not apply — and the orphan-flag sweep deliberately excludes it (source = 'cron-session') so it can never wrongly un-hide it. Its end-of-life is owned by a dedicated watchdog sweep, reapSilentCronSessions():
- error / stalled → SURFACE (clear silent, re-enter the inbox) so a broken scheduled job is never left invisible.
- needs_you / ended (finished) → ARCHIVE (
silent-auto) so it stays out of the sidebar/inbox and frees the account slot. A silent session that ends by asking a question (needs_you) is archived, not surfaced — only real failures surface. - running / starting → left alone (still working; visible while running, per
isSilentlyHidden).
Hang detection uses the stalled STATUS, not a wall-clock timer, so a legitimately long session is never force-surfaced.
Behaviour summary
Applies to both the orchestrator session and the worker session(s) — the table assumes a clean silent recipe; archive happens to both kinds on clean exit (orchestrator via finalize()'s inline archive, worker via the mode-specific paths above).
| Run outcome | Sidebar row | Inbox row | Archive approval |
|---|---|---|---|
| Clean completion | Hidden during, archived | Hidden during, archived | Skipped (auto-archive inline) |
| Mid-run attention state | Hidden (silent) | Hidden (silent) | None until finalize() or watchdog |
| Step error | Hidden (silent) | Hidden (silent) | Surfaces once finalize() clears |
| Crash >6h with no end | Watchdog surfaces it | Watchdog surfaces it | Normal flow once session is visible |
| Recipe set silent=false | Visible (regular recipe) | Visible (regular recipe) | Normal flow |
Out of scope (carry forward)
- Per-cron-run silent override. No way to flip silent for a one-off run from the Run dialog; you have to edit the cron job. The UI surface area didn't justify it for v1.
- Backfill existing recipe configs. The flag is opt-in — no migration touches existing
.recipe.jsonfiles. If you want a recipe silent, flip the toggle and re-save. - Tuning the trouble-marker heuristic. Silent mode bypasses the heuristic entirely; tuning it is a separate effort that would affect non-silent recipes too.
For agents
Auto-archive inline (CLI route fast path)
When a silent run completes cleanly, the engine's finalize step archives the orchestrator session itself — no "Archive ses…" inbox row, no manual click. Two paths get the same fast-path treatment:
- The engine's own internal archive call inside finalize.
- External CLI calls to
POST /session/:id/archive— when the target session hassilent = 1, the route applies the archive inline regardless of auth class (cli-token or in-app session) and skips the approval queue entirely. This is the door the user's external janitor scripts (archive-ended-sessions.mjsand similar) walk through cleanly.
Other lifecycle actions on silent sessions (pause, snooze, unpause) still go through the normal approval flow — only archive gets the silent fast path.
Where it lives in code
- Schema (v159) —
src/main/db/incremental-migrations.tsaddssessions.silent,cron_jobs.silent_mode, andorchestration_runs.silentcolumns. Tri-statesilent_modeisnull | 'force_silent' | 'force_visible'. (Originally slotted as v157 on the feature branch; renumbered to v159 at merge time when master had advanced through v157/v158.) - Recipe schema —
src/shared/types/recipes.tsdeclaressilent?: booleanonRecipeConfig. - Engine resolution —
src/main/services/orchestration-engine.tsreadscron.silentModefirst; falls back toRecipeConfig.silent; snapshots the resolved value intoorchestration_runs.silentand writes it onto the spawnedsessions.silentrow. - Engine finalize bypass — same file's finalize step calls archive-on-clean-exit and bypasses the trouble-marker heuristic that drives the regular archive-approval flow (the silent path short-circuits
shouldAutoArchiveStepSession()whenisSilent === true). - Worker session archive paths — multi-session:
executeRunStep()insrc/main/services/orchestration-engine.tsforce-endsneeds_youthen callsshouldAutoArchiveStepSession()+archiveSession(). Same-session:runSameSessionLoop()calls the new helperarchiveSilentSameSessionWorker()on every clean exit (mirrors the multi-session path); abort branches don't archive.shouldAutoArchiveStepSession()short-circuits the trouble-marker check whenisSilent === true. Pinned bytests/unit/services/orchestration-engine-silent-same-session-archive.test.ts. - CLI archive bypass —
src/main/services/cli/cli-server-lifecycle-routes.tsenqueueLifecycleAction()short-circuits to inline archive when target session is silent and action issession.archive. - Filter helper —
src/renderer/src/lib/silent-session.tsisSilentlyHidden(session)is the single check used by every sidebar and inbox filter; pinned bytests/unit/lint/silent-session-filter-coverage.test.tsso new filters can't quietly diverge. The same helper also gatessrc/renderer/src/lib/utils.tscomputeProjectSessionCounts— without it the per-project chip badge in the sidebar over-counts silent attention sessions that no list actually renders (a 2026-05-14 regression of the 2026-05-13 sweep). - Watchdog —
src/main/services/silent-run-watchdog.ts, 1h tick / 6h threshold (Sweep A) + no-cutoff orphan sweep (Sweep B), started/stopped alongside other periodic services insrc/main/index.ts. Run-scoped clearer used by the engine:clearSessionsSilentForRun(runId)insrc/main/db/queries-sessions/index.ts(returns rowcount; cheap WHERE silent=1 means non-silent runs see a no-op write). The engine wires it fromfinalize()and the CI tripwiretests/unit/lint/finalize-clears-silent-flag.test.tscatches future regressions of the call site. - Session-type cron silent —
src/main/services/cron/cron-session-executor.tsspawns withsilent: job.silentMode === 'force_silent'(sourcecron-session, exportedCRON_SESSION_SOURCE);src/main/services/silent-run-watchdog.tsreapSilentCronSessions()owns their run-less lifecycle (surface error/stalled, archive needs_you/ended) and the orphan-flag sweep excludes them. Pinned bytests/unit/services/silent-run-watchdog-cron-sessions.test.ts+tests/unit/services/cron-session-executor-silent.test.ts. - Recipe Editor toggle —
src/renderer/src/features/recipes/RecipeEditor.tsx"Silent on success" row. - Cron Editor radio —
src/renderer/src/features/cron/JobEditorDialog.tsx"Visibility" fieldset with the 3-way radio group. - Cleanup IPC —
src/main/ipc/recipe-cleanup-handlers.tsregistersrecipes:get-pileup-count(read) andrecipes:clear-silent-archive-pileup(sweep). The sweep wraps each row in try/catch — successful archives transition the pending row to approved+dispatched in a single transaction, failures fall through tomarkRejectedso a single pile-up entry can't block the rest of the sweep. - Surface stuck IPC —
src/main/ipc/silent-sessions-handlers.tsregisterssilent-sessions:get-stuck-count(read) andsilent-sessions:surface-stuck(sweep). The sweep iterates rows fromsrc/main/db/queries-sessions/index.tslistStuckSilentSessions()(same selector the watchdog uses:silent = 1AND status not in('running','starting')ANDended_at IS NULL), callsclearSessionSilentper row, fires arecipe_silent_run_surfacedfeature event withreason: 'manual-settings-button', and emits aSESSION_STATUS_CHANGEDpush so the renderer picks it up without a refetch. - Cleanup button —
src/renderer/src/features/settings/sections/features/FeaturesSettings.tsx(Settings → Features → "Clear archive-approval pile-up" and "Surface stuck silent sessions").
Related
- use-recipes.md — running, scheduling, and chaining recipes; this page only covers the silent visibility layer on top.
- cron-session-jobs.md — the regular cron → session spawn path; a session-type cron ALSO honors silent mode via
silentMode = 'force_silent'(see "Session-type cron jobs" above). - archive-a-session.md — what archive means for regular sessions; silent recipes piggyback on this with the inline-archive fast path.
Last verified 2026-09-28