---
title: Archive a session
---

# Archive a session

## What it is

Archiving a session takes it out of the sidebar's main list and parks it in a collapsible **Archived** sub-section near the bottom of the project. The CLI process is killed, any running turn is force-stopped, and the row's status flips to `archived`. The conversation history, attachments, and metadata are all preserved — archive is non-destructive. You can unarchive any time, and sending a new message to an archived session **silently un-archives it** so triage stays friction-free.

Archive is the right action when you're done with a session and want it out of your way. It's NOT the same as Pause — pause is "freeze, I'll come back to this"; archive is "I'm done, hide it." It's also not delete — there is no destructive deletion of session rows in normal usage, and archived sessions remain visible under the Archived sub-section indefinitely.

Every archive is undoable. The always-on path is **Ctrl+Z** with focus outside a text box (a session row, the sidebar): it reverses your last session action, and you can press it repeatedly to walk back through recent actions. The session's own message composer also undoes the action via Ctrl+Z when its box is empty (it falls back to the app undo) — but inside any OTHER text box (search, team chat) Ctrl+Z is that box's own text-undo, like a normal editor, never the action. An optional **Undo toast** (a 5-second clickable "Undo" button) can also be turned on under Settings → Notifications ("Session archived" toasts, **off by default**); when on, clicking it calls `unarchiveSession()` and brings the row back. The default keyboard shortcut to archive the active session is **Ctrl+W** (or just **E**, both bound to the `closePanel` action in [keybindings.ts](../../src/shared/keybindings.ts) — the action is internally named `closePanel` but its label is "Archive session"). When a **file, markdown, or image preview** is open on the right (the File Viewer, or the Google Drive preview), **Ctrl+W** closes that panel first — it archives the active session only on a second press, or when no preview is open. This mirrors **Escape**, which already closes the preview.

## Where to find it

### How to use it

1. **To archive the active session via keyboard.** Press **Ctrl+W** or **E** with no input focused (or with a session focused). The session row vanishes from the sidebar and is added to the Archived sub-section. **Ctrl+Z** undoes it when focus is outside a text box — and also from the session's message composer when that box is empty (the composer falls back to the app undo). Inside any OTHER text box (search, team chat), Ctrl+Z undoes your typing there, not the archive — the focused text editor owns Ctrl+Z like a normal editor (owner request, 2026-08-06). Press Ctrl+Z again from outside a text box to walk back through earlier actions. If you've enabled the optional "Session archived" toast (Settings → Notifications, off by default), its **Undo** button is a focus-independent alternative. Undo restores the session to its previous status if it was in an attention state (e.g. "Needs You" — `ATTENTION_STATUSES`); otherwise it returns to `ended`.
2. **To archive via the right-click context menu.** Right-click any session row in the sidebar (or right-click + drag for a multi-select). The context menu opens with just **Archive** at the top — the command only, not the session name (you already know which row you right-clicked). Selecting more than one row first changes the label to **Archive N sessions** — bulk archive runs through `bulkArchiveSessions` and shows a single toast with the total. Bulk archives of more than 3 sessions trigger an extra confirmation modal before the actual archive runs (so you can't accidentally bulk-archive 50 sessions with one wrong click).
3. **To archive on mobile via the X button.** On mobile (`md:hidden` viewport), each session row in the sidebar shows a small X / close icon at the right edge. Tapping it archives that single session, undoable the same way (Ctrl+Z, or the optional toast). **Desktop never shows an X** — the X is mobile-only by design. On desktop, archive via middle-click, right-click menu, the overflow menu, or the keyboard shortcut. Inside an **open** session on mobile there's a second path: the **Archive button in the header's top-right corner** — it sits in the session header's action row, right after the ⋯ menu, centered on that row and hugging the right edge, so it stays lined up with the ⋯ and the mic no matter how tall the header gets (it renders inline for exactly that reason — when a taller control like the voice mic was added, an older fixed-position version drifted above the row; inline it can't). The inbox detail views (Texts, Email, digests, alerts, drips, …) use one shared pinned button at that same corner. Rule locked by [mobile-archive-placement-contract.md](../../.claude/memory/contracts/mobile-archive-placement-contract.md). When the session is already archived, that same slot shows **Unarchive**.
4. **To archive via desktop middle-click.** Middle-click any session row in the sidebar (mouse button 1 — the scroll-wheel button) and the row archives immediately, undoable the same way (Ctrl+Z, or the optional toast). Middle-click is the desktop equivalent of the mobile X. Note that the middle-click handler is wired through `onMouseDown` (not `onAuxClick`) because Chromium's autoscroll mode eats `auxclick` inside scrollable ancestors. **Next-session behavior (2026-06-20):** middle-clicking the session you're currently viewing advances the cursor to the **row rendered immediately below it** (or the row above when it was the last) — uniformly for every status, so you can triage a pile of gray/paused sessions row-by-row and the list **stays where you are** instead of jumping. (Earlier builds advanced a gray/paused row to the top "Needs You" item, which felt like being kicked to the top of the list.) **Per-section advance (2026-06-29):** for **Interrupted** (gray/`ended`) and **Paused** rows specifically, the next pick stays **within that same section** — bouncing back to the previous row when you archive (or snooze) the last one — so clearing an Interrupted session never jumps you into the Paused section; each behaves like the Needs You area. See `project-pile-triage` in [inbox-navigation-contract.md](../../.claude/memory/contracts/inbox-navigation-contract.md). **Cursor-stable scroll (restored 2026-07-13):** middle-clicking the **highlighted** row slides the next row into the **exact on-screen slot the dismissed row occupied — under your stationary mouse** — so you can middle-click the same spot repeatedly to clear a stack without ever moving the pointer (the Gmail archive-and-advance feel). A stray middle-click on some _other_ row never yanks an unrelated item to your mouse. Near the **top** edge — where a row physically can't reach the slot — it lands as close as the list allows (no jarring snap). And at the **bottom** of a full list, a tiny invisible spacer grows by exactly the shortfall so the next row **still lands under your mouse** — the same trick the phone uses; it trims away as you scroll clear, so no empty space lingers (**desktop bottom-slack, ported 2026-08-14 at the product owner's request**, reversing the earlier best-effort-at-the-bottom behavior). Keyboard **E** keeps its calm behavior (it only scrolls when the next row would otherwise land off-screen), because the mouse is _on_ the row for a middle-click but nowhere for **E** — the feel is deliberately per-gesture. (This shipped 2026-06-21, was briefly removed 2026-07-09 over a mistaken "inconsistent with **E**" read, and was restored at the product owner's request.) **Mobile × keeps its place (2026-07-16):** on a phone the × does the touch equivalent — tapping it to dismiss an inbox row holds the list steady as the row leaves, so the next row's × lands where your finger already is, letting you clear a stack top-to-bottom without re-aiming your thumb. (The desktop highlight-follow mechanism isn't reused as-is — a phone × is tapped on any row, not just the highlighted one — so the mobile path simply pins the scroll position across the row's removal.) See [cursor-stable-dismiss-contract.md](../../.claude/memory/contracts/cursor-stable-dismiss-contract.md).
5. **To archive via the overflow menu.** Open a session, click the three-dot menu (`MoreHorizontal`) in the session header, and pick **Archive** (archive icon) from the **quick-actions group** at the top of the menu — Snooze / Pause / Send Later / Archive, with a divider beneath it. (Archive used to sit under the collapsed **More** section; it moved up into this top group on 2026-08-09.) The shortcut hint shows `Ctrl+W` next to it. Archive is now offered for **any non-archived session, including a running one** — clicking it on a live session auto-terminates the CLI, then archives (the same two-step backend the keyboard shortcut and middle-click already use), so you no longer have to reach for those just to archive a running session. The entry is replaced with **Unarchive** when the session is already archived. You can also **right-click Archive — or any other quick-action row — and pick "Pin to session bar"** to promote it to a one-tap button on the session header (desktop only); right-click a pinned header button to unpin it. Pinning rules: [session-bar-commands-contract.md](../../.claude/memory/contracts/session-bar-commands-contract.md).
6. **What happens to a running session on archive.** Omniscio first calls `processManager.terminate(sessionId)` to kill the CLI, force-flips the status to `ended`, then runs the archive write. So archiving an active session is a two-step backend operation that you don't see in the UI — from your perspective the row just vanishes. Worktree merges are triggered as part of the archive flow if the session was running on an isolated git worktree.
7. **Auto-unarchive on Send.** Open the Archived sub-section, click an archived session to view its history, type a new message, and Send. Omniscio calls `queries.unarchiveSession()` from `sendResponse()` and re-spawns the CLI for the new turn — the session is back in the live list and the conversation continues. You don't need to explicitly unarchive first.
8. **Manual unarchive without sending.** Open the archived session, click the three-dot menu, and pick **Unarchive** (archive-restore icon). The status is restored to `ended` (or whatever you pass as `restoreStatus`) and the row reappears in the live sidebar list. Toasts and selection state are not preserved across the unarchive — it's a clean state restoration.
9. **P / H "deposit" an archived session into the live list.** Pressing **P** (pause) on an archived session you currently have open un-archives it INTO the paused bucket — atomic, optimistic-slice-migrating, sidebar-visible immediately. Pressing **H** (snooze) does the same but routes through `ended` first so the existing snooze flow can take it from there. Bulk **P** with a mixed selection (some archived, some active) runs both arms in parallel and clears selection on settle. The single-session paths require the row to be the active surface (so a stale archived row in `archivedSessions[]` can't be silently dragged out when you're looking at something else). Codified as invariant **`keybindings-deposit-across-the-archive-boundary`** in [archive-session-contract.md](../../.claude/memory/contracts/archive-session-contract.md). Before this fix (2026-05-23), P/H on an archived session were silent no-ops because the keyboard handler only looked in `sessions[]`.
10. **Browse the Archived sub-section.** Scroll to the bottom of any project's session list and click the **Archived (N)** chevron to expand. See [browse-archived-sessions.md](browse-archived-sessions.md) for navigation through archived sessions (mobile swipe, J/K keys, prev/next chevrons).
11. **Phantom sessions.** Omniscio sometimes creates a "blank" session and then soft-deletes it (`softDeleteOtherBlankSessions`) — for example when launching a new session that has no draft and no messages, an existing blank session is reused instead. If you somehow archive a soft-deleted phantom (rare race), the IPC returns "Session not found" and the renderer drops the row from `sessions` instead of rolling it back to visible. From your perspective the row stays gone — no error toast.
12. **Archiving an already-archived session is idempotent (self-heal).** If a session was archived from another client (web/mobile), or its archive status push was missed (listener not yet mounted, a stale startup fetch), the row can be stranded in your LIVE SESSIONS list showing a gray (`ended`) dot even though it is already `archived` in the database. Clicking Archive on it again does NOT fail: Omniscio detects the session is already archived, reports success, and re-broadcasts the archive status event so every connected client (including the one with the stale row) moves it out of the live list and into Archived. From your perspective the stuck gray row finally disappears. (Before 2026-05-19 this returned a misleading "may still be running" error and the row snapped back, staying stuck forever — see the `session-archived-stuck-gray-in-live-list` postmortem.)
13. **Every archive is attributed — the "Archived by" chip.** Open any archived session in the dedicated Archive view (the Archive sidebar / search). The header shows a small chip beside the project name with an archive icon and a human label — for example "Keyboard shortcut", "AI Router", "Recipe step", "Mobile swipe", "Voice command", "Close button", "Auto-cleanup", "CLI direct", or "Agent (self-archived)". Every archive path records who triggered it (the closed `ARCHIVE_TRIGGERS` set, see [src/shared/types.ts](../../src/shared/types.ts)) into `sessions.archived_by` at write time. Pre-v226 rows that were archived before the column existed show **"Unknown"** as a graceful fallback rather than a blank chip. Unarchiving clears the stamp so a subsequent re-archive starts fresh. The chip exists so when a session "disappears" you can see exactly which surface put it there — keyboard shortcut, recipe step, AI router decision, etc. — instead of guessing.
14. **Search the full archive — the search box loads everything.** The dedicated Archive view lists the first 50 archived sessions on open (a performance cap — a 4,000-row archive would otherwise freeze the renderer at startup). As soon as you type into the search box, Omniscio auto-loads the rest of the archive in the background — a status line under the search input says "Loading all N archived sessions so search covers every one..." while the IPC is in flight. The filter then matches over the full archive, not just the first 50. You don't need to hit a "Show all" button first; the moment you start searching, the whole archive becomes searchable. This guarantee — "search always sees every archived session, no matter how many there are" — is what makes the archive a safe discoverability fallback when a session seems to have vanished.
15. **Stop an entire spawned run — and closes that stick.** When an automation spawns a **controller** session that in turn spawns many **worker** sessions (an audit, a recipe, an agent orchestration), closing the controller now stops the WHOLE run — every worker it spawned is stopped with it, in one action, instead of you killing them one at a time while it spawns more. You don't even have to find the controller: **right-click any worker row and pick "Stop entire run"** — it resolves up to the controller and stops the whole tree. And anything you close **stays** closed: a session you close yourself is marked so NO automatic process — crash-recovery on a restart, the retry sweeper, an automation messaging it — can silently reopen it; only you re-opening it (unarchive, or sending it a message yourself) brings it back. A controller you've closed also can't spawn any more workers.
16. **A session can archive itself (agent self-archive).** A running session — really the AI agent inside it — can archive its OWN session when its work is done, **two equivalent ways**: the simplest is to **emit the marker `[[OMNISCIO_SELF_ARCHIVE]]`** on its own line in its final message — Omniscio spots it when the turn ends and archives the session automatically, no command to run (agents reliably forget a curl call, so the marker is the dependable path); or it can call the CLI control server route `POST /session/self-archive` directly. Both trigger the SAME archive through one shared dispatch, so everything below applies identically. The marker matches only on its own line (a mid-sentence or code-fenced mention never fires, so an agent _discussing_ the feature can't archive itself) and is stripped from the shown message. There's no id involved — the target is always the caller, resolved from its `$AMC_SESSION_ID` (the `X-AMC-Source-Session-Id` header), so a session can only ever archive **itself**, never another. It behaves like any other archive (the running turn is finalized then the process stops, the row moves into Archived, reversible via Unarchive) and is stamped with the **"Agent (self-archived)"** attribution chip so you can tell at a glance that the session ended itself rather than you or a recipe ending it. It honours your "require approval for CLI session actions" setting — applies immediately by default, or queues an inbox approval when that setting is on — **unless** you turn on the dedicated **"Let finished sessions archive themselves without approval"** toggle (Settings → CLI Control, **off by default**), which lets a session's OWN self-archive apply immediately with nothing landing in your inbox **even while approval stays on for every other lifecycle action** (only self-archive is exempt — archiving _another_ session over the CLI still asks). Turning that toggle on is itself an approval-gated settings change, so an agent can never grant it to itself. It also has its own on/off kill-switch, **Session self-archive** (default on); when off, both triggers refuse (the route returns `403`, the marker is a no-op). Full reference: [agent-self-archive.md](agent-self-archive.md) (the focused how-it-works page), [session-self-archive-contract.md](../../.claude/memory/contracts/session-self-archive-contract.md), and the omniscio-control [sessions spoke](../../.claude/skills/omniscio-control/sessions.md).

## How it behaves

Archiving is non-destructive and reversible. The CLI process is killed and any running turn is
force-stopped, the row's status flips to `archived`, and the conversation history, attachments
and metadata are all preserved — the session parks in the collapsible **Archived** sub-section
near the bottom of its project and remains visible there indefinitely. Every archive can be
undone: **Ctrl+Z** with focus outside a text box reverses your last session action and can be
pressed repeatedly to walk back through earlier ones, and sending a new message to an archived
session silently un-archives it. The one limit worth knowing before relying on it: inside any
other text box (search, team chat) Ctrl+Z is that box's own text-undo, not the action undo.

## For agents

### How it works

The keyboard handler is `handleClosePanel` in [inbox-close-action.ts](../../src/renderer/src/hooks/keyboard-shortcuts/inbox-close-action.ts) — a context cascade that closes the visible surface first: an open **file/markdown/image preview** (the File Viewer, or the Google Drive preview when that view is active — the preview is overlay-precedence #2 in `computeOverlayKey`, so whenever it is open it IS the visible surface, mirroring what Escape closes), then bulk-archive (when sessions are selected in the sidebar), gmail/sms/RSS archive (when those contexts are active), digest dismissal (digest project), or single-session archive of the active session as the default. The single-session path calls `state.archiveSession(activeSessionId)` on the [session-store.ts](../../src/renderer/src/stores/session-store.ts) and registers the undo through the `registerSessionActionUndo` chokepoint (which wires both Ctrl+Z and the optional `sessionArchived` toast in one call); the inverse calls `unarchiveSession`. All archive surfaces route through that same chokepoint — see [session-action-undo-contract.md](../../.claude/memory/contracts/session-action-undo-contract.md).

`archiveSession()` in the store does an optimistic update first: it captures a four-slice snapshot (`sessions`, `activeSessionId`, `activeInboxSmsPhone`, `activeInboxDigest`) per the CLAUDE.md "Optimistic rollback" rule, removes the session from `sessions`, prepends it to `archivedSessions` with `status: 'archived'`, and resolves the next active session via `resolveNextSession()`. Then it invokes `IPC.SESSION_ARCHIVE`. **The store action returns a boolean** — callers MUST check the return value before showing a success toast or registering an undo, per the CLAUDE.md `archiveSession returns boolean` rule. On `success: false` the four slices are restored from the snapshot; on `not found` (phantom session), the session is dropped from `sessions` without rollback.

The IPC handler in [session-handlers.ts](../../src/main/ipc/session-handlers.ts) delegates to `archiveSessionService()` in [session-lifecycle-service.ts](../../src/main/services/session/session-lifecycle-service.ts). That service: gets the session row, returns idempotent success early if the row is **already** `archived` (re-emitting `SESSION_STATUS_CHANGED` so stale-row clients self-heal — parity with `pauseSessionService`'s already-paused early return), otherwise calls `processManager.terminate(sessionId)` and force-flips status to `ended` if the session was active, clears rate-limit recovery state, adds a Sentry breadcrumb, calls `queries.archiveSession(id)` (which UPDATEs `status='archived'` only when current status is in `('ended', 'error', 'paused', 'needs_you')` — returns false otherwise), clears draft and snooze, triggers a worktree merge if the session has an active worktree, and emits `SESSION_STATUS_CHANGED` with `previousStatus` set to the original status (captured before the force-flip).

The middle-click handler is in [SidebarSessionRow.tsx](../../src/renderer/src/features/dashboard/SidebarSessionRow.tsx) — `onMouseDown` checks `e.button === 1` (per the CLAUDE.md "Middle-click row handlers" rule — `onAuxClick` does NOT work). The mobile X button in the same file uses `md:hidden` and stops event propagation. Both call `onCloseSession(session)`, which `SessionsSidebar.tsx` wires to `handleCloseSession` — that handler runs `terminateSession` first if the status is in `('running', 'needs_you', 'stalled', 'starting', 'ready')`, then calls `archiveSession`, then registers undo. Auto-unarchive on Send is in [session-service.ts](../../src/main/services/session/session-service.ts) `sendResponse()` — `if (session.status === 'archived') queries.unarchiveSession(session.id)` happens before the CLI respawn, and a `SESSION_STATUS_CHANGED` push fires with `previousStatus: 'archived'` so the renderer animates the row back into the visible list. That `previousStatus: 'archived'` marker is load-bearing: the renderer only migrates a row OUT of the Archived list on a **genuine revive signal** — `previousStatus === 'archived'` (an explicit un-archive) or `status === 'starting'` (a real resume). A stray late termination/error push from a still-running orchestrator or a crash-recovery watchdog is ignored, so an archived session can't be dragged back into the live list and the database's archive stays authoritative — see [archived-session-resurrection-contract.md](../../.claude/memory/contracts/archived-session-resurrection-contract.md).

**Attribution (`archived_by`)** is stamped at the SQL write site: [`queries.archiveSession(id, trigger)`](../../src/main/db/queries-sessions/archive.ts) takes the trigger as its second arg, validates it against the closed union `ArchiveTrigger` (declared in [src/shared/types/archive-trigger.ts](../../src/shared/types/archive-trigger.ts) — 31 values, e.g. `user-keyboard`, `recipe-step`, `silent-auto`, `cli-direct`), and writes it into `sessions.archived_by` in the same UPDATE that flips `status='archived'`. The IPC seam at [src/shared/ipc-schemas.ts](../../src/shared/ipc-schemas.ts) `archiveSessionSchema` carries the trigger end-to-end with `z.enum(ARCHIVE_TRIGGERS)`. Renderer-side, `mapSession()` reads the column via `narrowEnumOrNull` — if the row is `archived` but `archived_by` is NULL (pre-v226 backfill) or corrupted (an unrecognised string from a future revert), it falls back to `'unknown'` so the chip never blanks out. `unarchiveSession()` clears the column to NULL so the next archive starts fresh. The chip itself lives in [ArchiveView.tsx](../../src/renderer/src/features/archive/ArchiveView.tsx) and uses the `ARCHIVE_TRIGGER_LABELS` Record to humanize the enum value. Migration v226 ([src/main/db/database.ts](../../src/main/db/database.ts)) adds the nullable column with `ALTER TABLE sessions ADD COLUMN archived_by TEXT DEFAULT NULL`.

**Search auto-load** is one `useEffect` in [ArchiveView.tsx](../../src/renderer/src/features/archive/ArchiveView.tsx) that watches `searchQuery`, `archivedSessions.length`, and `archivedTotalCount`. When the user has typed any non-whitespace query AND the count is known AND the dataset is partial AND no fetch is already in flight (ref-guarded), it calls `fetchArchivedSessions()` with no `limit` — which hits the unlimited IPC and populates the `__all__` cache key. The status line under the search input shows "Loading all N archived sessions so search covers every one..." while in flight, suppresses the otherwise-misleading "No sessions match your search" empty state for the same window, and disappears once the cache lands. After the auto-load resolves, the filter naturally matches over the full archive.

**Authoritative close + cascade-stop.** A direct user close (any `user-*` archive trigger, `SESSION_TERMINATE`, or a cascade on your behalf) stamps `sessions.user_closed_at` — the authoritative "a human closed this" marker. Every automated revival path (`listCrashRecoverableSessions` + its external/suspended siblings, the stuck-session sweeper, and the auto-unarchive-on-message paths) gates on `user_closed_at IS NULL`, so a session you closed is never resurrected — not even an app-crash victim, not even a worker the reconciler flips to `error` mid-cascade. Closing a session also cascade-stops its spawned descendants: `stopSpawnedSessionTree()` resolves the whole tree via a recursive `parent_session_id` walk, marks it all user-closed in ONE transaction FIRST (so the close is crash-safe), then paces the terminations; the **Stop entire run** menu item calls `stopEntireRunFrom()`, which resolves up to the root controller first. A stopped controller's later CLI spawn requests are refused at `routeAiSessionSpawnRequest`. Unarchiving or sending your own message clears the marker so a deliberately-reopened session recovers normally. Full rules: [authoritative-user-close-contract.md](../../.claude/memory/contracts/authoritative-user-close-contract.md).

## Related

- [agent-self-archive.md](agent-self-archive.md) — how a session archives ITSELF when its work is done (the `[[OMNISCIO_SELF_ARCHIVE]]` marker / `POST /session/self-archive`, the "Agent (self-archived)" chip, the two CLI-Control settings)
- [pause-or-stop-a-session.md](pause-or-stop-a-session.md) — Pause vs Archive: pause keeps the row visible but frozen; archive hides it
- [snooze-a-session.md](snooze-a-session.md) — Snooze vs Archive: snooze hides until a time, the session keeps running in the background
- [browse-archived-sessions.md](browse-archived-sessions.md) — open and navigate between archived sessions (J/K, swipe, prev/next chevrons)
- [send-a-message.md](send-a-message.md) — auto-unarchive on Send and the rest of the Send pipeline
- [bulk-select-sidebar.md](bulk-select-sidebar.md) — multi-select and bulk-archive via right-click menu
- [bulk-stop-restart-sessions.md](bulk-stop-restart-sessions.md) — stop or restart a hand-picked set of sessions (vs. "Stop entire run", which stops one spawned tree)
- [delete-a-project.md](delete-a-project.md) — when you want to delete a whole project (and all its sessions) instead of just archiving one session
