---
title: Silent recipe sessions
---
# Silent recipe sessions

## 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. In `executionMode: "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 lingering `needs_you` → `ended` and calls `shouldAutoArchiveStepSession()` → `archiveSession()` before the next step begins.
- **Same-session** — `runSameSessionLoop()` calls `archiveSilentSameSessionWorker()` on every clean exit, which mirrors the multi-session cleanup: force-end any lingering `needs_you`, then route the archive decision through `shouldAutoArchiveStepSession({ 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 `error` or `needs_you`.
- The run reached `completed` status 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:

1. Flips its orchestration run status to `error` and stamps `ended_at` to now.
2. Clears the session's silent flag so the row re-enters the visible set.
3. Emits a `session-status-changed` push (status `error`) 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.json` files. 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 has `silent = 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.mjs` and 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.ts` adds `sessions.silent`, `cron_jobs.silent_mode`, and `orchestration_runs.silent` columns. Tri-state `silent_mode` is `null | '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.ts` declares `silent?: boolean` on `RecipeConfig`.
- **Engine resolution** — `src/main/services/orchestration-engine.ts` reads `cron.silentMode` first; falls back to `RecipeConfig.silent`; snapshots the resolved value into `orchestration_runs.silent` and writes it onto the spawned `sessions.silent` row.
- **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()` when `isSilent === true`).
- **Worker session archive paths** — multi-session: `executeRunStep()` in `src/main/services/orchestration-engine.ts` force-ends `needs_you` then calls `shouldAutoArchiveStepSession()` + `archiveSession()`. Same-session: `runSameSessionLoop()` calls the new helper `archiveSilentSameSessionWorker()` on every clean exit (mirrors the multi-session path); abort branches don't archive. `shouldAutoArchiveStepSession()` short-circuits the trouble-marker check when `isSilent === true`. Pinned by `tests/unit/services/orchestration-engine-silent-same-session-archive.test.ts`.
- **CLI archive bypass** — `src/main/services/cli/cli-server-lifecycle-routes.ts` `enqueueLifecycleAction()` short-circuits to inline archive when target session is silent and action is `session.archive`.
- **Filter helper** — `src/renderer/src/lib/silent-session.ts` `isSilentlyHidden(session)` is the single check used by every sidebar and inbox filter; pinned by `tests/unit/lint/silent-session-filter-coverage.test.ts` so new filters can't quietly diverge. The same helper also gates `src/renderer/src/lib/utils.ts` `computeProjectSessionCounts` — 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 in `src/main/index.ts`. Run-scoped clearer used by the engine: `clearSessionsSilentForRun(runId)` in `src/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 from `finalize()` and the CI tripwire `tests/unit/lint/finalize-clears-silent-flag.test.ts` catches future regressions of the call site.
- **Session-type cron silent** — `src/main/services/cron/cron-session-executor.ts` spawns with `silent: job.silentMode === 'force_silent'` (source `cron-session`, exported `CRON_SESSION_SOURCE`); `src/main/services/silent-run-watchdog.ts` `reapSilentCronSessions()` owns their run-less lifecycle (surface error/stalled, archive needs_you/ended) and the orphan-flag sweep excludes them. Pinned by `tests/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.ts` registers `recipes:get-pileup-count` (read) and `recipes: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 to `markRejected` so a single pile-up entry can't block the rest of the sweep.
- **Surface stuck IPC** — `src/main/ipc/silent-sessions-handlers.ts` registers `silent-sessions:get-stuck-count` (read) and `silent-sessions:surface-stuck` (sweep). The sweep iterates rows from `src/main/db/queries-sessions/index.ts` `listStuckSilentSessions()` (same selector the watchdog uses: `silent = 1` AND status not in `('running','starting')` AND `ended_at IS NULL`), calls `clearSessionSilent` per row, fires a `recipe_silent_run_surfaced` feature event with `reason: 'manual-settings-button'`, and emits a `SESSION_STATUS_CHANGED` push 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](use-recipes.md) — running, scheduling, and chaining recipes; this page only covers the silent visibility layer on top.
- [cron-session-jobs.md](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](archive-a-session.md) — what archive means for regular sessions; silent recipes piggyback on this with the inline-archive fast path.
