mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 16:02:24 +00:00
* feat(ai-vault-search): define public contract and service seam * feat(ai-vault-search): add IPC runtime relay and web transports * fix(ai-vault-search): register search IPC at the core handler site ai-vault.ts was two lines over the 300-line max-lines limit; the search handlers belong with the other register*Handlers calls anyway. * fix(ai-vault-search): withhold degraded-root paths from relay status Status carried local filesystem paths over the relay while hits redact theirs. redactStatusForTransport applies the same policy at the same boundary: relay callers keep each root's reason and the array length as the count, so the type only makes root optional. * fix(ai-vault-search): close diagnostic path leak and remove test casts * feat(ai-vault-search): carry an execution host id and per-host outcomes on hits * feat(ai-vault-search): route desktop search by execution host scope, including runtimes * feat(preload): accept an execution host scope on session search * feat(web): answer only for the paired runtime on session search * docs(ai-vault-search): describe execution-host routing and the all-hosts merge * test(ai-vault-search): cover every host scope, the all-hosts merge and wire compat * fix(ai-vault-search): resume every host mid-page so a merged page never drops a hit * fix(ai-vault-search): decode the merged cursor with a schema instead of casts CI's type-aware audit refuses type assertions; a zod record validates the per-host entries and yields the typed map without one. * refactor(ai-vault-search): defer cross-host merged search
138 lines
8.4 KiB
Markdown
138 lines
8.4 KiB
Markdown
# 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:<target>`, or
|
||
`runtime:<environmentId>`. 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:<target>` asks that relay session and nothing else.
|
||
- `runtime:<environmentId>` 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.
|