---
title: Session Search (sidebar virtual project)
---

# Session Search (sidebar virtual project)

## What it is

**Session Search** lets you find past Omniscio sessions by describing what you're looking for in plain English — "that Stripe refund thing from last week", "the session where I debugged auth", "help me find the script I was writing for invoices". It ships as a permanent **Session Search** entry in the sidebar (the `__search__` virtual project): a chat with Claude where you ask in plain English and Claude does the legwork — calling the local search endpoints, verifying candidates, and replying with real `omniscio://session/<id>` deep links you can click to jump straight into a session.

The sidebar surface is backed by a local embedding index: titles and first messages of every session are converted to 384-dimensional vectors stored in SQLite, and your query is embedded the same way so the server can rank sessions by meaning rather than keyword overlap.

> **The Ctrl+K palette no longer has a separate "Semantic" tab.** That UI was removed in the 2026-04-26 search redesign. Semantic ranking now blends silently into the regular Ctrl+K results per existing eligibility — there's no mode toggle, no AI badge, and no "Top N matches" wording. See [semantic-search.md](semantic-search.md) for the silent-blend behaviour. If you want a Claude-driven, conversational search experience, use the sidebar Session Search project documented below.

## Where to find it

### How to use it

1. **Find it in the sidebar.** A project named **Session Search** sits in the sidebar list alongside your real projects (seeded on first launch, idempotent — it survives deletion attempts).
2. **Click Session Search.** Clicking the sidebar entry drops you straight into a session with the reply textarea focused — no "+ New Search" click needed. If a previous search chat is waiting on you (a question or error), you land on it; otherwise you land in a fresh blank with the textarea focused, ready to type your next query. Behind the scenes Omniscio spawns the Claude CLI with a system prompt that tells Claude how to query the local search endpoints. Blank-session reuse on the backend keeps repeated clicks from spamming new rows — the last empty chat is recycled.

   **Ctrl+T / N** also work while Session Search is active — same as in any other session-hosting project — so you can spin up a fresh search chat without leaving the keyboard. (See [keyboard-shortcuts.md](keyboard-shortcuts.md) for the full list.)

3. **Ask in plain English.** Type something like _"what was that session where I was debugging the Gmail OAuth flow?"_ and send. Claude will call `/search/semantic` first, fall back to `/search/fts` if needed, verify the top 1–3 candidates by reading their recent messages, and reply with a short synthesized answer plus a list of clickable session links in the format `- [<session name>](omniscio://session/<id>) — <one-line reason>`.
4. **Click a result.** The `omniscio://session/<id>` links render as real buttons in the chat — clicking one opens the session in-app without a page reload (see [deep-links.md](deep-links.md)).
5. **Ask follow-ups.** Since it's a normal Claude session, you can say "show me the last three in that series" or "only the ones in the Gmail project" and Claude keeps searching.

### Sidebar Session Search vs Ctrl+K — when each wins

| Use the sidebar Session Search project when…                                                | Use Ctrl+K (global search) when…                                                                     |
| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| You only remember the _gist_ and want a synthesized natural-language answer with reasoning. | You remember an exact phrase, error string, or file name.                                            |
| You want Claude to verify candidates by reading recent messages before recommending one.    | You want to filter by channel (SMS, Slack, Telegram, RSS, webhook) — sidebar search is session-only. |
| You want to ask follow-ups in the same conversation ("now narrow to Gmail project").        | You want raw results back instantly — no LLM round-trip.                                             |
| The query is fuzzy enough that keyword search wouldn't match.                               | You need phrase quoting (`"refund flow"`), exclusion (`-word`), or `OR` syntax.                      |

### Opening from Ctrl+K — Ask AI (scoped background search)

The Ctrl+K palette shows an **Ask AI** button (the sparkle icon) — desktop in the modal header, mobile in the Filters row. Clicking it opens a small **popover** (it no longer launches directly), **pre-filled** with whatever you'd already typed, where you tweak what to search for. Submitting spawns a Session Search chat **in the background** that **auto-runs** your query — you are not pulled out of what you were doing. A toast confirms _"Searching in the background…"_ with an **Open** button that jumps you into the chat; the chat also appears in the sidebar with its normal status, so you can switch in when it's ready. The chat bubble shows only your question — the scope instructions ride hidden in the sent prompt (via the `displayText` override).

**The search is scoped to the tab you're on.** On **Conversations** it searches your sessions/messages and folds in your active filters (project, channel, dates, tags). On **Notes** it searches your KMS vault (`/kms/search`, `/kms/semantic-search`). On **Settings** it answers from your settings catalog, injected as context (there is no settings search endpoint). The **All** tab searches conversations + notes. Meetings / Voice / Recordings / Helpdesk currently fall back to searching conversations + notes — dedicated per-surface AI search for those is a planned follow-up.

Submit stays disabled until the box is non-empty; a launch failure shows an error toast and keeps the popover open to retry. The button hides itself if the Session Search project hasn't been seeded yet (very first launch) or outside the desktop app. Behaviour is locked by [search-ask-ai-contract.md](/.claude/memory/contracts/search-ask-ai-contract.md).

## How it behaves

### Dependencies and failure modes

Session Search depends on Omniscio's **local HTTP server** at `127.0.0.1:19519` — the same localhost-only server documented in [cli-control.md](cli-control.md). The server exposes four read-only endpoints the feature relies on:

- `GET /search/semantic?q=<query>&limit=<n>` — embedding similarity over session titles + first messages.
- `GET /search/fts?q=<query>&limit=<n>` — full-text keyword search.
- `GET /session/<id>` — session metadata.
- `GET /session/<id>/messages?limit=<n>` — recent messages, used by Claude to verify a candidate is the right session.

All four endpoints require `Authorization: Bearer <token>` where the token is read from `~/.amc/cli-token` (`%USERPROFILE%\.amc\cli-token` on Windows). Missing or invalid token returns `401 { ok: false, error: "unauthorized", code: "AUTH_REQUIRED" }`. The bundled `search-sessions` skill that Omniscio writes into each Session Search workdir's `CLAUDE.md` reads the token file and adds the header for in-Omniscio sessions automatically; external CLI users (Claude Code outside Omniscio, curl, scripts) must add the header explicitly. Auth is gated on these endpoints because they expose private session content — FTS hits, semantic neighbours, full transcripts — that localhost-only binding alone does not protect from other processes on the host.

All responses are `{ ok: true, ... }` or `{ ok: false, error: "...", code: "<ERROR_CODE>" }`. **Every failure body carries a machine `code`** alongside the human `error` string — the `error` strings are frozen, and `code` is the discriminator to branch on, so a client never has to match on prose or special-case which branch it hit (`AUTH_REQUIRED` on 401, `RATE_LIMITED` on 429, `VALIDATION_FAILED` on 400, `NOT_FOUND` on a gated 404, `REQUEST_TIMEOUT` / `SERVICE_UNAVAILABLE` on 503, `INTERNAL` on a 500).

**When `GET /search/fts` cannot finish (2026-09-25).** The full-text route runs on its own
short-lived database host rather than the shared one, so a common-word search can no longer stall
or recycle the worker every other read depends on. If that host cannot complete the search, the
route answers `503 { ok: false, error: "<plain reason>", code: "REQUEST_TIMEOUT" | "SERVICE_UNAVAILABLE" }`
instead of hanging. The distinction matters to a caller: an empty `results` array ALWAYS means the
search ran and matched nothing, never "we gave up".

**When the server is unreachable.** Inside a sidebar Session Search chat, Claude itself runs into the refused connection and tells the user _"Omniscio does not appear to be running"_ in its reply. The feature fails soft; it does not crash the session. The inline Ctrl+K semantic blend (see [semantic-search.md](semantic-search.md)) goes through a main-process IPC proxy with a 5-second timeout — on failure it silently falls back to pure keyword results without any user-visible alert, since semantic and keyword hits no longer have separate UI surfaces.

**Port override caveat.** The bundled `search-sessions` SKILL.md (which Omniscio copies into the session's workdir as `CLAUDE.md`) hardcodes `127.0.0.1:19519`. If you run Omniscio with the `AMC_CLI_PORT=<n>` override, the sidebar Session Search chat will not find the server. Fix planned; acceptable Phase-1 limitation.

### One call instead of many: the sessions digest

`GET /sessions/digest` answers the question Session Search takes several round-trips to reach — *what is every agent doing right now, and what did it last say to me?* — in a **single** call. Where a caller would otherwise list sessions and then fetch each one's messages, the digest returns, per session: its status, the original ask, the last **real user** message, and the last N agent messages.

```text
GET /sessions/digest?repo=<folder path or project name>&statuses=needs_you,running&prose=3
```

Every parameter is optional: `repo` (one repo; omitted means every project) or `projectId` (exact, wins over `repo`), `statuses` (default `needs_you,running`), `prose` — how many agent messages per session, 1–20, default 3 — `maxChars` (per-message clip, 1–5000, default 600), `sessionLimit` (1–500, default 200), and `format=text|json` (default `text`).

**Prefer the digest when you want the state of a whole board; use Session Search when you are hunting for one specific conversation.** The digest is scoped by repo and status, not by relevance — it has no query, so it cannot find *the* session where something was discussed, only report what each session is doing now.

**It needs the full-trust token.** The digest returns message bodies for every session across every project, so it is `cliTokenOnly`: the scoped per-session `$AMC_CLI_TOKEN` an agent is spawned with is refused, and only the global `~/.amc/cli-token` works.

### Relationship to the `search-sessions` skill (external/CLI users)

There is a user-level skill at `~/.claude/skills/search-sessions/` that documents the exact same endpoints and search strategy for Claude Code sessions running **outside** Omniscio. If you're using the Claude CLI directly (not through Omniscio) and the skill is active, you can say _"find my Omniscio session about X"_ and that external Claude will curl the same `127.0.0.1:19519/search/semantic` endpoint. **The skill is the single source of truth.** In-app Session Search sessions load the SAME skill body — Omniscio reads `.claude/skills/search-sessions/SKILL.md` at spawn time (or, in packaged builds, from `resources/bundled-skills/search-sessions/SKILL.md`), strips the YAML frontmatter, and writes it to `~/Claude/search-sessions/CLAUDE.md` so the Claude CLI auto-loads it on startup. One file, both surfaces — no drift risk, no second copy to keep in sync.

### Relationship to inline semantic search in Ctrl+K

Omniscio has a separately documented feature called [Semantic Search](semantic-search.md) (Settings → Sessions → AI-Powered Search) — that feature **silently augments** keyword Ctrl+K results by blending a few vector-similarity hits at the top of the response. As of the 2026-04-26 redesign there is **no UI distinction** between keyword and semantic hits in Ctrl+K (no AI badge, no Local/Semantic toggle, no "Top N matches"); semantic ranking just runs invisibly under the hood when the eligibility criteria match (clean query, Most Relevant sort, first page, no source/date filters).

The sidebar Session Search project documented above is a different surface entirely:

- It's a **chat with Claude**, not a results palette — Claude verifies candidates and synthesizes a natural-language answer.
- It calls the same **local HTTP server** (`/search/semantic`, `/search/fts`, `/session/<id>/messages`) that the inline Ctrl+K semantic blend uses, but does so via the spawned CLI agent, not via IPC.
- It returns conversational replies with deep links, not score-ranked rows.

Both features use the same underlying embedding pipeline, so either surface finds the same sessions — they just present differently.

### Self-exclude — why the search agent never finds itself

The search agent is itself running in an Omniscio session, so without protection its own session would be the strongest match for any query (the user's request to the search agent is the most relevant document for any query the user just typed). Omniscio prevents that deterministically:

1. **Spawn-time env injection.** When Omniscio spawns the Claude CLI for a search session (or any other session), `process-manager.ts:spawnLocalStreamJson` sets `spawnEnv.AMC_SESSION_ID = session.sessionId`. The env var rides in the child process environment alongside `CLAUDE_CODE_OAUTH_TOKEN` and Omniscio's other per-spawn stamps.
2. **Ambient strip.** `getStrippedExact()` in [/src/main/process/clean-spawn-env.ts](/src/main/process/clean-spawn-env.ts) includes `AMC_SESSION_ID`, so a leftover shell-profile copy from a prior debug run cannot masquerade as a legitimate spawn-injected value (same canonical strip-and-reinject pattern as `ANTHROPIC_API_KEY`).
3. **Skill reads and appends.** The [`search-sessions` skill](/.claude/skills/search-sessions/SKILL.md) reads `$AMC_SESSION_ID` and appends `&excludeSessionId=$AMC_SESSION_ID` to every `/search/semantic` and `/search/fts` URL.
4. **Server filters.** `handleSearchFts` and `handleSearchSemantic` in [/src/main/services/cli/cli-server-search.ts](/src/main/services/cli/cli-server-search.ts) drop any result whose `sessionId` matches the param before responding (after the timeframe filter, with `total` recomputed for FTS).

When the env var isn't set (external Claude Code outside Omniscio), the skill falls back to two heuristics — drop sessions started in the last 10 minutes, drop sessions whose first message is a near-paraphrase of the user's query. The heuristics are best-effort and can mis-fire when the user legitimately wants to find an old session whose prompt resembled their current one; the env-var path has no such ambiguity. Full invariants and safe-change checklist: [.claude/memory/contracts/session-search-contract.md](/.claude/memory/contracts/session-search-contract.md).

### No Plain Speak overlay on a search answer

A Session Search reply **is** the result list, so it does not get the Plain Speak overlay. A plain-speak card would restate the answer already on screen and hide it behind a ⇄ toggle — noise on a chat whose whole output is "here are the sessions, with links".

The switch is the ordinary per-session overlay opt-out, stamped at **session birth** for anything created in the `__search__` virtual project (and, for the same reason, in `__ask_amc__` — see [ask-amc.md](ask-amc.md)). Stamping the row means it holds for every later spawn of that session, not only the first turn, and it also drops the "you forgot your card" nudge and the spoken-narration fragment.

The rule has one home — `OVERLAY_SUPPRESSED_VIRTUAL_PROJECT_PATHS` in [/src/shared/virtual-project-ids.ts](/src/shared/virtual-project-ids.ts) — and is applied by both session-minting paths (`session-create.ts` for Claude, the gated non-Claude leaf in `ipc/session/launch.ts`). Chats created **before** this shipped keep the cards already stored in their transcripts: stored message content is never rewritten.

### Archive & retention

Search sessions archive just like any other session — on desktop you middle-click the row (no X button is shown on desktop), on mobile you tap the X button (mobile-only) to remove it from the default sidebar list. Archived search sessions then appear in a collapsed **Archived (N)** row at the bottom of the Session Search project's sidebar. Clicking the row expands it so you can reopen a past search, and clicking again collapses it. The count updates live as you archive or reopen sessions.

Unlike regular projects — where archive history is kept indefinitely — archived search sessions are **retained for 30 days only**. After that, an automatic background task soft-deletes (`is_deleted = 1`) any search session whose `status = 'archived'` and whose `status_changed_at` timestamp (the moment of the last status transition, which the archive flow updates) is older than 30 days. This keeps the Archived drawer from growing unbounded for a feature that generates throwaway one-shot chats. Regular project archives are unaffected by this pruner — only rows belonging to the `__search__` virtual project are eligible.

Pruned rows remain physically present in the SQLite file (soft-deletes leave the row on disk; SQLite is not configured to auto-vacuum), but they are never exposed to the renderer because every sidebar and list query already filters `AND is_deleted = 0`. If a user ever needs manual recovery of a pruned search session within that file-retention window, the underlying row can still be found by querying the database directly; there is no in-app undo after pruning. The two layers of "delete" here are: `status = 'archived'` (reversible, visible in the Archived drawer, 30-day window) and `is_deleted = 1` (invisible to the UI, set by the background pruner).

The pruner logic lives in [/src/main/db/queries-sessions/archive.ts](/src/main/db/queries-sessions/archive.ts) (`pruneOldArchivedSearchSessions`) and is scheduled alongside the other periodic DB maintenance tasks in the main process.

## For agents

### How it works

The sentinel project id `SEARCH_PROJECT_ID = '__search__'` lives in [/src/shared/types.ts](/src/shared/types.ts) and is included in `VIRTUAL_FOLDER_PATHS`. On startup, `ensureSearchSessionsProject()` in [/src/main/services/claude-project.ts](/src/main/services/claude-project.ts) creates the `~/Claude/search-sessions/` directory and seeds a project row named "Session Search" if none exists (idempotent — skips if a row with that folder path exists, even soft-deleted). `resolveProjectWorkDir()` maps the sentinel back to the real directory path at spawn time. The seeded row has a random DB UUID; the sentinel is the row's `folder_path`, not its primary key.

**Auto-launch on navigation.** Unlike most virtual projects (Gmail, SMS, channels) that just render their own UI on click, Session Search and Ask Omniscio share an "auto-launch host" branch in `setActiveProject()` ([/src/renderer/src/stores/session-store.ts](/src/renderer/src/stores/session-store.ts)). The branch runs `pickAttentionSessionForProject()` first — if a session with status `needs_you`, `error`, or `stalled` exists in the project (excluding snoozed, scheduled-response, retry-state, and grace-id sessions), it activates that session and bumps `inputFocusToken` so the textarea focuses. Otherwise it calls `launchSession(projectId)`, which uses the backend's blank-session reuse logic — any existing blank row (no messages, no pasted images, not in the `excludeSessionIds` draft list) is recycled, otherwise a new row is created. `launchSession` bumps `inputFocusToken` on success, which the `useSessionPanel` auto-focus effect ([/src/renderer/src/features/sessions/useSessionPanel.ts](/src/renderer/src/features/sessions/useSessionPanel.ts)) keys off of. Cold-start (`isLoading: true` when the click arrives) defers the picker; the `fetchSessions` success branch re-fires it once sessions hydrate. Concurrent rapid clicks are gated by `autoLaunchInFlight`. The net effect: clicking either entry drops the user into a session every time — needs-you if one exists, otherwise a fresh blank.

At spawn time, [/src/main/process/process-manager.ts](/src/main/process/process-manager.ts) resolves the search row via `getProjectByFolderPath(SEARCH_PROJECT_ID)` and compares its `id` to `session.projectId` (both UUIDs). On match it calls `prepareSearchSessionsWorkDir()` from [/src/main/services/search-workdir-setup.ts](/src/main/services/search-workdir-setup.ts), which reads the bundled `search-sessions/SKILL.md`, strips its YAML frontmatter, and writes the body to `~/Claude/search-sessions/CLAUDE.md`. Claude CLI auto-loads `CLAUDE.md` from the workdir on startup, so the guidance arrives without any `--append-system-prompt` flag. The same writer also deletes any stale `.mcp.json` in the workdir, and the spawn passes `mempalaceEnabled=false` for search sessions — MemPalace is irrelevant here and its tool list would just add context noise.

**Why not `--append-system-prompt`?** It was the original mechanism, removed on 2026-04-23. On Windows, Omniscio spawns via `cmd.exe /c claude.cmd %*`, and `cmd.exe` truncates any argv entry at its first newline. The multi-line system prompt silently lost everything after its first line — Claude only knew it was "in Session Search" with no strategy or endpoint docs, and would hallucinate an MCP server. Moving the guidance into `CLAUDE.md` side-steps the cmd relay entirely and consolidates the source of truth with the external skill.

The `IPC.SEARCH_SEMANTIC` channel still exists (`'search:semantic'` in [/src/shared/ipc-channels/index.ts](/src/shared/ipc-channels/index.ts), schema `searchSemanticSchema` in [/src/shared/ipc-schemas.ts](/src/shared/ipc-schemas.ts), result type `SemanticResult` in [/src/shared/types.ts](/src/shared/types.ts)) but the renderer no longer consumes it from `SearchModal.tsx` — the previous Semantic tab UI was deleted in the 2026-04-26 redesign. The main-side proxy in [/src/main/ipc/search-handlers.ts](/src/main/ipc/search-handlers.ts) is still reachable for any non-renderer caller (e.g. tests) and reads `AMC_CLI_PORT` (falling back to `19519`) before issuing a 5-second-timeout fetch to `http://127.0.0.1:<port>/search/semantic`. The server-side endpoints the sidebar Claude chat hits are handled by [/src/main/services/cli/cli-server-search.ts](/src/main/services/cli/cli-server-search.ts) — `handleSearchSemantic` calls `semanticSearch()` from the embedding service, `handleSearchFts` calls `searchMessagesAsync()` (off the UI thread via the DB worker), and the two session-detail routes (`/session/<id>` and `/session/<id>/messages`) round out what Claude needs to verify a candidate.

## Related

- [global-search.md](global-search.md) — the Ctrl+K palette (FTS5 keyword search blended silently with semantic ranking, filters, deep-links)
- [semantic-search.md](semantic-search.md) — the **inline** semantic augmentation of keyword results inside Ctrl+K (different surface, same embedding model)
- [cli-control.md](cli-control.md) — the `127.0.0.1:19519` HTTP server that hosts the `/search/semantic`, `/search/fts`, and `/session/<id>` endpoints
- [deep-links.md](deep-links.md) — the `omniscio://session/<id>` URLs Claude renders in its replies
- [default-claude-project.md](default-claude-project.md) — the sibling `~/Claude` default project that hosts `search-sessions/` as a subdirectory
