mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
* feat(cli): orca search over the agent session index `orca search <query>` calls PR 5's `aiVault.searchSessions` over the CLI's existing runtime RPC, against the host `--environment` / `--pairing-code` selects and no other. `orca search --index-status` calls `aiVault.searchStatus`. It is the proof the contract works with no panel. Every flag maps onto a contract field and nothing else: `--scope`, `--fresh`, `--limit`, `--cursor`, repeatable `--agent` and `--path`, `--since`, `--sort`, `--debug`, `--json`. No fan-out, no merged output, no `--host`. One command rather than a `search status` subcommand: the query is a bare positional, so `orca search status` could not be told apart from searching for the word "status". `--status` is unavailable because `orchestration task-list --status <state>` already owns the name as a valued flag. No new runtime capability. PR 5 decided an explicit `method_not_found` refusal maps to `unavailable/no-service`, so reusing `createSessionSearchClient` gives an old host a plain "this host runs no session search service" answer at exit 0 instead of a raw JSON-RPC error. `CommandSpec.repeatableFlags` scopes repeatability per command, because `--agent` must repeat for search and stay single-valued for `worktree create`. `help.ts` sat exactly at max-lines, so `skills-command-flag-help.ts` becomes `command-scoped-flag-help.ts` carrying both tables at the same call-site size. * refactor(cli): drop the search type assertions main's casting gate now rejects Main gained a `consistent-type-assertions: never` scan in the changed-code gate after this branch was cut, and it reported twelve assertions in the new files. The four in the argument parser were avoidable. `readEnum` now keeps the value `find` returns, which already carries the narrow type, and the agent filter goes through an `isAiVaultAgent` predicate over a `Set<string>` instead of widening the agent tuple. The test now narrows the printed envelope by shape and re-reads the printed result through `AiVaultSearchResponseSchema`, so the JSON assertions are checked rather than claimed, and the flag table is typed so its callback needs no cast. One assertion is left, for the structural fake client, with the SAFETY rationale AGENTS.md requires. * fix(cli): sanitize host strings and scope pre-command repeatable flags Route every host-supplied string the search formatter prints through the escape stripper, and resolve the repeatable-flag set from the command tokens ahead when a flag sits before the command. * refactor(cli): resolve repeatable flag rules once per command * fix(cli): clarify session search availability and SSH scope * feat(cli): hide orca search until the settings toggle ships `orca search` stays dispatchable but leaves every discovery surface: root help, group help, unknown-command suggestions, and `agent-context --json`. `buildAgentContext` did not filter hidden specs, so it also stops leaking the hidden `terminal stop`.
173 lines
11 KiB
Markdown
173 lines
11 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, one host's index generation, and an opaque persisted
|
||
index incarnation. Query, scope, filters, and sorting must remain the same; page
|
||
size may change. Writes that advance the generation can invalidate it, including
|
||
retention purges. Clearing or rebuilding the database invalidates it even when the
|
||
new generation counter matches. 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`.
|
||
- CLI: `orca search` calls both over the runtime RPC, against the host that
|
||
`--environment` / `--pairing-code` selects and no other. It reuses
|
||
`createSessionSearchClient`, so an old host's refusal reaches the caller as
|
||
`unavailable/no-service` rather than an error, and needs no new capability.
|
||
In an Orca SSH terminal, the forwarded CLI defaults to the controlling Orca
|
||
runtime's index. `--path` filters that index; it does not select the SSH host.
|
||
`--environment` / `--pairing-code` can explicitly select a paired runtime.
|
||
- 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 connects
|
||
these endpoints to the production service installed by PR 3b. Desktop indexing
|
||
runs in the scanner child; orcad and the SSH relay register their own in-process
|
||
services. Registration alone does not grant consent.
|
||
|
||
## Desktop index controls (PR8)
|
||
|
||
Settings → Agent Session History controls this desktop's persisted
|
||
`aiVaultSearch { enabled, historyDays }` policy. It stays local even when another
|
||
execution host is selected. Paired clients cannot grant consent or clear an index
|
||
through this surface; SSH relay registration remains disabled without a separate
|
||
host consent mechanism.
|
||
|
||
`aiVault.clearSearchIndex()` is a no-argument desktop-only preload operation over
|
||
`aiVault:clearSearchIndex`. It addresses the local scanner child, not the selected
|
||
remote host. The child's existing interactive request lane executes `searchClear`
|
||
through `SessionSearchInstance.clear()`: close the indexer and database handles,
|
||
remove the SQLite database and sidecars, then reconstruct only if consent remains
|
||
enabled. Errors propagate to the settings pane. This operation never deletes
|
||
original transcripts. There is no new runtime or relay method. Opaque cursors also
|
||
carry a persistent database identity: clearing creates a new identity, so a
|
||
pre-clear or legacy cursor returns `stale-cursor` even if the rebuilt numeric
|
||
generation happens to match. Reopening the same database preserves its identity.
|
||
|
||
Disabling closes the indexer and keeps the index copy; clearing deletes the copy.
|
||
Changing retention reuses the existing close-and-construct policy application.
|
||
The settings pane reads status only while enabled and visible, polls only an
|
||
observed indexing phase, and stops on completion or error. Opening the pane,
|
||
changing policy, or pressing Refresh obtains a new observation. The indexer's own
|
||
schedule does not depend on the pane.
|