Session Search (sidebar virtual project)
Session Search is a permanent sidebar entry: a chat with Claude where you describe a past session in plain English and Claude does the searching, then replies with clickable links straight into the session it found. It is backed by a local embedding index of session titles and first messages, so it can match by meaning rather than keywords. Ctrl+K remains the separate, instant keyword palette.
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 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
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).
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 for the full list.)
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/semanticfirst, fall back to/search/ftsif 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>.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).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.
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. 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) 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.
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.
One call for an inbox overseer. Triaging "what is waiting, who started it, and what did it last say" used to mean a call per session plus a call per transcript. Three opt-in parameters fold the whole picture into the same request: include=crew,branch,alerts (all takes every one), fullText=1 to drop the per-message clip, and changedSince=<ISO> to see only what moved since your last pass — the mark is echoed back so a five-minute sweep can page forward. Alongside the fields above, each session then also reports who really wrote its opening request (user / peer / system, so a person's own chats separate from a spawned worker fleet), its Plain Speak overlay parsed into sections and carried beside the final message rather than instead of it, any lettered question it asked, the app's own attention bucket (where an interrupted turn is distinguishable from a delivered one), and how long it has been running next to its cost and turns. As with everything else here, a caller that asks for none of it gets exactly what it got before.
curl -s "http://127.0.0.1:19519/sessions/digest?repo=<folder path>&statuses=needs_you,error,stalled&include=all&fullText=1&format=json" \
-H "Authorization: Bearer $(cat ~/.amc/cli-token)"
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 (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:
- Spawn-time env injection. When Omniscio spawns the Claude CLI for a search session (or any other session),
process-manager.ts:spawnLocalStreamJsonsetsspawnEnv.AMC_SESSION_ID = session.sessionId. The env var rides in the child process environment alongsideCLAUDE_CODE_OAUTH_TOKENand Omniscio's other per-spawn stamps. - Ambient strip.
getStrippedExact()in /src/main/process/clean-spawn-env.ts includesAMC_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 asANTHROPIC_API_KEY). - Skill reads and appends. The
search-sessionsskill reads$AMC_SESSION_IDand appends&excludeSessionId=$AMC_SESSION_IDto every/search/semanticand/search/ftsURL. - Server filters.
handleSearchFtsandhandleSearchSemanticin /src/main/services/cli/cli-server-search.ts drop any result whosesessionIdmatches the param before responding (after the timeframe filter, withtotalrecomputed 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.
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). 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 — 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 (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 and is included in VIRTUAL_FOLDER_PATHS. On startup, ensureSearchSessionsProject() in /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). 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) 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 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, 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, schema searchSemanticSchema in /src/shared/ipc-schemas.ts, result type SemanticResult in /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 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 — 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 — the Ctrl+K palette (FTS5 keyword search blended silently with semantic ranking, filters, deep-links)
- semantic-search.md — the inline semantic augmentation of keyword results inside Ctrl+K (different surface, same embedding model)
- cli-control.md — the
127.0.0.1:19519HTTP server that hosts the/search/semantic,/search/fts, and/session/<id>endpoints - deep-links.md — the
omniscio://session/<id>URLs Claude renders in its replies - default-claude-project.md — the sibling
~/Claudedefault project that hostssearch-sessions/as a subdirectory
Last verified 2026-10-02