# Agent session search contract `AiVaultSearchRequest`, `AiVaultSearchResponse`, `AiVaultSearchHit`, and `AiVaultSearchStatus` are defined in `src/shared/ai-vault-search-types.ts` and validated by `src/shared/ai-vault-search-contract.ts`. ## Search and pagination - Tool output beyond 3,072 characters per row is not indexed and not searchable; user and assistant text is indexed in full. - A page cursor outstanding during a retention purge is refused once as `stale-cursor`; the client re-issues page 1. - A phrase match across a chunk boundary of a long message is not supported. `aiVault.searchSessions(request)` accepts `query`, optional `scope` (`conversation` or `all`, default `all`), `freshness` (`indexed` or `wait-until-current`, default `indexed`), `limit`, opaque `cursor`, `filters`, and `debug` (default false). Conversation scope searches user and assistant text. Filters accept `agents`, `scopePaths`, ISO `since`, and `sort` (`relevance` or `newest`). Paths refer to the execution host and work for folders without Git. Legacy `tier` and `refresh` fields are accepted and discarded; they do not change the defaults. Limits use the engine's resolver: default 20, integers clamped to 1–100, fractional numbers use the default. Long queries reach the engine so it can report truncation rather than fail validation. Results contain `kind: 'results'`, `hits`, `page: { cursor, hasMore }`, `generation`, `truncated: { candidates, snippets, query, freshness }`, and `durationMs`. `snippets` is a count; the other truncation fields are booleans. `durationMs` measures the engine search, excluding any reconciliation wait. `debug: true` adds `debug: { route, repairedTerms?, plannerReport }`; the report contains `route`, optional `repairedTerms`, and `scope`. Diagnostics never appear at the top level. Status is never attached to search results. A cursor belongs to one query and one host's index generation. Query, scope, filters, and sorting must remain the same; page size may change. Writes that advance the generation can invalidate it, including retention purges. A refused cursor yields `{ kind: 'stale-cursor', generation, expectedGeneration? }` and the client discards it and issues page 1 without a cursor. Reusing that refused cursor continues to fail; there is no server-side cursor acknowledgement state. Malformed cursors and cursors for a different query yield `{ kind: 'malformed-cursor' }`. Generation checks also reject a first page if the index changes during retrieval. Generation is a fence, not a retained snapshot: a client cannot ask the host to recreate a previous generation. Pages are per host only. Ordering is local to that host's query. Clients must discard cursors when changing hosts. All-computers search, merged ordering, per-host aggregate outcomes, and merged cursors are deferred to a separate PR. That follow-up must define generation fencing, page-size changes, unavailable hosts, and bounded parallel retrieval before exposing a combined result list. ## Execution host routing Search and status address one execution host: `local`, `ssh:`, or `runtime:`. An omitted host means this desktop's local index. Invalid IDs and `all` are refused; neither can widen a request to other hosts. - `local` searches this machine's index over desktop IPC. - `ssh:` asks that relay session and nothing else. - `runtime:` asks that paired runtime over its RPC. A paired runtime answers for itself and never forwards through another desktop. Each hit may carry `executionHostId`. The desktop stamps remote answers with the host it addressed rather than trusting an ID returned by that host. Local answers and older hosts may omit attribution. ## Evidence and exposure Each hit carries agent, session ID, title, cwd, branch, updated time, message count, score, source, and evidence. Evidence contains snippet, role, and timestamp; it is null for operator-only matches that have no text evidence. Snippet matches use `[[` and `]]` markers. Source presence is `present`, `unverifiable`, or `missing`; the current engine emits the first two. Loss of contact does not prove a source missing. `redactForTransport(hit, transport)` is the exposure policy: | Transport | filePath / codexHome | resumeCommand | Status `degradedRoots[].root` | | ----------------------------------------- | ------------------------------------ | --------------------------------- | ----------------------------- | | Desktop IPC on the same machine | Included when known, under source | Included only for present sources | Included | | Runtime RPC on the same machine | Included when known, under source | Included only for present sources | Included | | Relay or paired runtime/web/mobile client | Withheld; source keeps presence only | Withheld | Withheld | `cwd`, titles, snippets, and other hit metadata remain visible to paired clients. Snippets cross the authenticated transport as indexed; this contract does not apply an observability redactor to transcript content. A missing Codex home is omitted. Resume commands reuse the command stored by the transcript reader, constructed by the sidebar's `buildAiVaultResumeCommand`; this layer does not construct commands or execute them. The runtime uses its authenticated `clientKind` context to distinguish paired clients from same-machine RPC, never a request-supplied locality flag. The receiving remote client also applies the same exposure function. ## Status, freshness, and availability `aiVault.searchStatus()` returns `enabled`, `phase` (`idle`, `indexing`, `current`, `degraded`, or `closed`), `filesIndexed`, `filesDue`, `filesFailed`, `degradedRoots` (`root` and `reason`), `lastReconcileAt`, `lastSweepCompletedAt`, and `generation`. Times are milliseconds since epoch or null. These are the indexer's observations; an indexed row is not a new filesystem verification. A degraded root's `root` and raw `reason` can both contain host filesystem paths. Desktop IPC and same-machine runtime RPC receive the full diagnostic; relay and paired clients receive only the fixed reason "Source root could not be verified." for each degraded root. The array length retains the count. `wait-until-current` calls `service.reconcile()` before searching. The adapter uses `indexer.reconcile({ full: false })`. After five seconds the endpoint searches anyway and sets `truncated.freshness: true` on results. It does not cancel the host's reconciliation. Completion before the deadline leaves the flag false; a reconciliation error before the deadline propagates. The indexer's bounded recent pass is not a promise that the entire historical corpus was swept. Search unavailability is a value: `{ kind: 'unavailable', reason: 'disabled' | 'not-ready' | 'no-service' }`. No registered service returns `no-service`. Status without a service has `enabled: false`, `phase: 'idle'`, zero counts and generation, empty degraded roots, and null timestamps. This is a sentinel for an absent service, not a claim of an empty, current index. A registered service may report disabled or not-ready. ## Boundaries and compatibility - Desktop: `aiVault:searchSessions` and `aiVault:searchStatus`, via preload. - Runtime and relay: `aiVault.searchSessions` and `aiVault.searchStatus`. - Desktop preload optionally accepts an execution host scope as a separate routing argument. It addresses exactly that host; missing connections never fall back to the local index. The web preload addresses its own paired runtime, answers `all` and any other host with `unavailable/no-service` rather than an error. Requests and responses are parsed where received from another process. Existing relay JSON-RPC request/response framing needs no new stream opcode. Following the existing relay method probe pattern, an old host's explicit `-32601` refusal (or runtime `method_not_found`) maps to `unavailable/no-service` on the client; status uses the absent-service sentinel above. Transport failures, authentication errors, and invalid payloads remain errors. Unknown request fields are stripped for wire compatibility. The process-local `setSessionSearchService(service | null)` registry is the only production seam in this PR. Tests use fake services and a real synthetic store. Nothing constructs an engine or indexer in production. PR 3b owns process lifecycle, consent/settings application, and registration of the production service.