The scan issue for an OpenCode database on a \\wsl.localhost share read as an
error with an instruction the user cannot follow. It now surfaces on the Agent
Session Search settings page under a computer row, where it belongs as a known
limitation, so the WSL branch now reads 'OpenCode sessions inside WSL can't be
searched from Windows yet.'
Copy only: the issue keeps kind 'scope' and its path, every other branch is
untouched, and discovery, the WSL gate, and the busy-timeout behavior are
unchanged.
The row above the list is now just that button. It appears when a paired
server is reachable, new enough and off, acts on exactly those plus this
computer, and disappears when there is nothing left to do. What it offers is
read off the rows each render, so it cannot disagree with them.
Deletes the standing auto-enable consent with it: the persisted flag, the
code that armed and cleared it, the per-host memory of which switches the
user had touched, and the line that promised future computers would turn
themselves on. A preference that acts on hosts the user never sees is worse
than a button they press when they mean it.
Each switch and Turn on all now act on the click. The dialogs restated the
row they sat under and stood between the user and a preference they can
reverse with the same control. Clearing search data keeps its dialog: that
one destroys something.
With only this computer, the count, the Turn on all button and the two
subheads all restate the single switch under them. Show them once a paired
Orca server exists, which is the first point at which they say anything.
(cherry picked from commit 2c7ee206bd)
Renames the pane, splits the list into this computer and paired Orca
servers, and puts a count of what is on above it with a Turn on all that
skips offline and too-old hosts and keeps going past a host that refuses.
Consenting once persists a standing consent so a server that later becomes
reachable turns on without another dialog; turning one off by hand drops it.
Rows past the sixth fold away, ordered by what the user can act on. Status
sentences now say how much is searchable instead of Ready, and an off
computer says so with its switch alone.
(cherry picked from commit f4cfac3ee2)
showAiVaultSearch opens the sidebar on the vault tab and sets one flag. The
panel takes the flag, widens its scope to every computer, focuses the search
box and clears the flag, so a later remount stays where the user left it.
(cherry picked from commit 2b66e20fcd)
The status contract gains an optional messagesIndexed, read from the store
beside the file-state counts and cached the same way, so a settings row can
say what is searchable rather than how many files were opened. Optional on
the wire: a paired server that predates the field degrades to a session count.
(cherry picked from commit 366118be0f)
Text parts alone left OpenCode behind every file-based provider: a command
someone ran, what it printed, and the model's reasoning were all unsearchable.
Widens the capture query to text, reasoning and tool parts. Reasoning folds
into the turn's own words, the way the shared block list already treats a
thinking block. Each tool part becomes one `tool` message carrying the call
line and what came back, built with the same `toolCallText` every file
provider uses; OpenCode's `filePath` is renamed to the `file_path` spelling
that list knows, so a call is findable by its file argument.
Adds a decoded-size ceiling beside the existing part ceiling. It is the bound
a non-streaming source needs and a streaming one does not: a JSONL provider
publishes each message as it reads it, while this one holds a whole session
before posting it across the worker boundary. Neither ceiling truncates; both
fail the read so it is retried and surfaces.
OpenCode has two storage shapes, so asking only that some OpenCode session
published messages was satisfied by the legacy JSON fixture while every
SQLite session in the vault stayed silent. Assert per discovered source and
keep the agent-coverage check beside it.
OpenCode sessions live in one SQLite database read on a worker thread, and
the worker only ever answered with the newest few messages for the panel
preview. The parser therefore published nothing over the transcript channel,
so the search index wrote a placeholder row for every OpenCode candidate and
no OpenCode message was ever searchable.
Adds a `capture` request to the worker protocol that returns the session and
every text part of every user/assistant turn from one open of the database.
The agent parser asks for it whenever a sink is listening, so OpenCode joins
the whole-document sources on the same path as Grok, Cursor and Gemini. The
placeholder path (`parserPublishesMessages`, `noteUnreachableParser`) is gone;
an OpenCode read that fails now fails like any other file.
Bumps the index schema so existing indexes drop their placeholder rows, and
adds `sessionsByAgent` to the index status, which is the count that made this
bug visible.
Say search, not index or transcript; lead with what the user gets and
where it shows up; one plain privacy sentence; count sessions, not files;
drop the mechanics that change no decision (stop hint, SSH note, source
roots jargon).
A phrase hit was highlighted with the OR expression over the stop-word
stripped terms, so an exact sentence rendered as scattered bold words with
its stop words plain. The snippet now uses the expression the route
matched by: one run for a phrase, every typed word for AND, the terms for
OR.
Codex 0.153+ writes paginated rollouts whose completed agent messages carry
content blocks typed `Text`. The transcript reader matched block types
case-sensitively, so every assistant turn from those sessions was dropped
before it reached the search index while user turns and tool output were
kept. Match case-insensitively and bump the index schema so existing
indexes are rebuilt with the replies present.
An exact sentence pasted out of a transcript was not returned. The route
ladder only ran the phrase and AND rungs for a literal-looking query, so
prose fell straight to OR, where the sentence's common words filled the
candidate limit with recent sessions and the old session holding the
sentence never reached ranking.
The planner now carries a `phrase` candidate: the query's tokens in order
with stop words kept, which is what the sentence is actually indexed as.
The ladder runs phrase then AND over those tokens for every query of two
or more tokens. A one-token query still takes the rung only when it
looked literal. `incomplete` is reported by the rung that answered rather
than accumulated across every rung tried.
A pass stops reading at its wall-clock deadline and records nothing about
the candidates it never opened, which is correct: being owed a read is a
fact about the row, not an entry in a queue. But a candidate the opening
sweep never reached has no row at all, so the store's `due` count cannot
see it. The sweep still reported `completed`, the indexer stamped
`lastSweepCompletedAt`, and `status()` answered `current` with a backlog
of thousands: "Up to date - 130 files indexed", then 630, then more.
The read loop now counts what it decided was owed and did not read and
hands the number back as `left`; the pass propagates it; the indexer
holds the last pass's count, adds it to `filesDue`, reports `indexing`
while it is non-zero or a sweep is owed, and no longer stamps a sweep
the deadline cut short as complete.
Delete index cleared the index while search was on, so the host closed, removed and immediately reconstructed it and everything reindexed. Turn local search off first, then clear, so the rebuild only happens when the user switches search back on.
Agent Session History now lists every computer that can hold an index --
this computer first, then each paired Orca server -- as one row with an
icon, a name, a single status line and its own switch. Indexing consent is
stated once above the list, and each row carries the switch for the host it
names, so turning search on for a server no longer means finding that
server's own settings.
Server rows poll aiVault.searchStatus on the same 2s/10s cadence as the
local one while the pane is visible, and report what the host actually
answered: Off, a sweep in progress, or an up-to-date count. A server that is
not connected stays listed but dimmed, with its last known switch position
and no claim about its index. A host that refuses the set call with
host-too-old flips to an update prompt that links to Remote Servers.
The old "Enable session history search" switch and the separate index-status
row are gone; their status copy moved to session-history-status-copy.ts and
their polling to use-session-search-status.ts, so every row shares one
message builder and one poll. Advanced > Delete index copy is unchanged and
still local-only, and a paired web client still sees this computer alone.
window.api.aiVault.setSearchEnabled is declared and bridged here but
implemented by the parallel backend PR.
Adds `aiVault.setSearchEnabled` so a desktop can turn a paired Orca server's
transcript index on or off and have the server apply it without a restart.
The runtime method refuses any caller without a `pairedDeviceId` with a
`forbidden`-class error, writes the whole resolved policy through the runtime
store so retention rides along untouched, then reaches the index through a
host-supplied hook: `applySessionSearchSettingsChange` on the desktop, the
in-process instance's new `apply` on orcad. The relay is unchanged.
Wire compatibility is Rule 1 shaped: a new optional method. A server that
predates it answers method-not-found, which the desktop IPC handler maps to an
error whose message is exactly `host-too-old`. Old clients never call it. The
method is deliberately absent from the mobile allowlist, and `aiVaultSearch`
stays out of the paired settings projection.
The panel's "All computers" scope produced no request: the hook parsed the
scope into a single host id and stopped when that was null, so the panel
answered "Choose one computer to search its sessions." The desktop already
merges every enumerated host behind `aiVault:searchSessions`, so pass the
scope straight through and stamp each hit with the host it came back on.
Hosts the merge could not search are named under the results header with a
short reason, since a silent partial answer reads as "no such session".
The `all` scope on `aiVault:searchSessions` now fans out from the desktop
to every host the session list enumerates and merges the pages into one.
Legs run in parallel: the local index through the search service, SSH and
runtime hosts through the existing remote search client.
Two fixed orders, because relevance scores from independent indexes are
not comparable. `newest` asks every leg for recency and k-way merges on
`updatedAt`, nulls last, ties broken on execution host id. `relevance`
rotates hosts in host-id order by their own rank.
The merged cursor is an opaque base64url payload holding each host's
cursor, how many of its current page were already emitted, and the
generation that offset counts into, plus the page size and sort the
cursor belongs to. A host whose index moved is fenced to `stale` and
stops contributing; the rest keep paging. Per-host outcomes ride back on
one new optional `hosts` field on the results response.
`aiVault:searchStatus` with `all` stays refused, and neither the runtime
RPC nor the CLI gains the scope, so a fan-out is never two hops.
`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`.
* feat(ai-vault-search): persist agent-session search consent and retention
Two booleans and nothing else: `enabled` and `historyDays`, off by default
because building the index reads every transcript on the machine. No `paused` --
the PR 3 indexer is immutable, so every change is close-and-construct.
The settings IPC normalizes a write like every other field and hands the change
to the index; there is no UI for it until PR 8.
* feat(ai-vault-search): hold one indexer and engine pair per host
The object that owns a host's live index and the three recipes that change it.
The indexer is immutable, so a settings change is close-and-construct, disabling
is close with no replacement, and clearing is close, remove the database,
construct. The new instance's first sweep purges a narrowed window and admits a
widened one, so neither needs a code path.
The database sits beside the scanner's parse cache, one file per host. A runtime
with no node:sqlite can hold no index at all, which the Node 18 floor on orcad
and the relay makes a real case rather than a hypothetical one.
* feat(ai-vault): let the scanner child own the session search index
The transcript reader runs in that child, so the index consumer has to as well:
one read serves both the session list and the index. Three request operations
(search, status, reconcile) and one fire-and-forget settings message carry
everything a parent needs; main never opens the database file.
The init frame becomes a factory because it is read at every spawn, so a
respawned child sees current consent rather than the first frame's. A child
holding a running index is never idle from the parent's side, so idle retirement
is suppressed while the index is on -- retiring it would stop the reconcile loop
until some later scan happened to respawn one.
Both files this lands in were already at the max-lines ceiling, so three
collaborators move to where they belong rather than being disabled around: the
invalidation deadline into the class that owns invalidations, call cancellation
and the start requeue into the call-state module, and orcad's flag parsing into
its own file.
* feat(ai-vault-search): register a search service on every host that answers
Without a registered service a host answers no-service, which means "this host
does not have the feature" rather than "the index is off". All three hosts now
answer the second thing.
The desktop forwards to the scanner child. orcad and the SSH relay daemon have
no such child -- orcad ships only the watcher and daemon entries, and the relay's
AI Vault sidecar runs the remote scanner, which publishes nothing to the
transcript channel -- so on those two the index lives in the process that would
drive its reads, gated on a runtime that has node:sqlite at all.
The relay registers with consent off and no way to turn it on: nothing carries a
setting to a remote host yet. That is the honest state, and it is still worth
registering, because it is what tells a client the difference between off and
too old.
* test(ai-vault-search): price a warm pass over five thousand transcripts
The number the reconcile interval will be revisited against, measured rather
than argued: a warm sweep stats every file under every root, a warm cycle stats
the newest N per agent, and neither reads what the index already holds. It does
not tune the interval.
* fix(ai-vault-search): answer the casting gate without assertions
main's new type-assertion rule reaches every file this branch touches. All nine
sites drop the cast rather than carry a SAFETY: rationale: the operation guard
narrows with `in`, the sqlite probe narrows the builtin it loads, the child test
keeps the discriminated reply instead of widening it, and the settings resolver
takes `unknown` -- which is what it really reads, since a persisted profile can
hold a value no version of this code wrote.
* fix(ai-vault-search): let a refreshed scan root reach the live index
The parent re-resolves scan roots before every policy push, precisely so a
WSL distro or extra Codex home that appeared since the child spawned enters
the window. The child forwarded only the settings to a live instance and used
the roots solely in its `??=` initializer, so those roots were dropped for the
child's lifetime.
The indexer stays immutable: a structurally different root set closes the pair
and constructs a new one, the same way a changed databasePath already does.
Compare via `sameSessionSearchRoots` rather than a plain JSON compare, because
nothing fixes the key order two producers write; lists are sorted too, since
the indexer walks every root and a re-enumeration that reorders is not a
change. An unchanged set still never restarts a running index.
The orcad and relay in-process hosts resolve roots once at install and never
re-apply, so they have no such seam.
* fix(ai-vault): restart the scanner child the index is holding
Three review items.
The hold keeps a child alive for the index, but only a queued call ever
started one: `pump()` skipped a hold with an empty queue, so an idle indexing
child that crashed, or an `ensureChild()` that failed at start, left indexing
stopped until an unrelated request happened to arrive. `pump()` now starts the
child the hold requires, which is also the restart callback the fault policy
already schedules, so the existing delay and circuit bound the retry exactly as
they bound a queued call's start. `updateSessionSearch` goes through the same
seam instead of its own `ensureChild` call.
A search registers no AbortController, so a cancel sent for a search id was
added to the `cancelled` set and never consumed. Nothing can reach that today
-- no caller passes a signal and the child answers in milliseconds -- so this
is only a leak of ids: consume it when the search settles.
The orcad argument doc claimed a `--`-prefixed value stays a flag. The parser
takes the next token regardless, and orcad-launch-contract.test.ts pins that,
so the doc is what was wrong. Behaviour is unchanged.
* fix(ai-vault): recover search indexing and refresh scan roots
* fix(ai-vault): defer search refresh policy reads
* fix(session-search): stabilize paging and host enablement
* fix(session-search): refresh host roots within full sweeps
* docs(session-search): clarify initial root fallback
* fix(native-chat): stop a bounded tail read from moving the chat cursor past unapplied rows
A structured chat pane could latch "Working for N" forever after the agent had
finished, showing the send arrow rather than Stop, while the sidebar and
`worktree ps` correctly read idle.
The client replica has one position (`state.cursor`) and one body. Two
operations keep those consistent: replace (both from one host snapshot) and
append (rows contiguous with the cursor). The `tail-page` branch was a third
thing: it took the cursor from the journal head, the items from a bounded page
(200 items, byte-capped), then merged retained client submissions over the
page's. Under continuous journal writes the client is always slightly behind,
so the branch ran on every window focus and on every pane re-activation. When
more than a page of rows had landed since a send, that send's user item fell
off the page, its submission was not carried, the retained `pending` survived,
and the cursor jumped past the dispatch-acceptance row. Nothing re-sends it: a
batch carries only touched items and that submission is never touched again.
Delete the third operation rather than guard it. A live subscription is now the
only thing that moves the cursor, and `subscribe({ cursor })` already replays
exactly the missed rows.
- remove the window `focus` listener and the owner/transport `refresh` contract
- skip warm hydration: a retained owner subscribes at its applied cursor
- cold hydration keeps its history read, applied as the existing `snapshot`
(replace) event rather than `tail-page`
- delete the `tail-page` action and its reducer branch
- delete `resumeCursor` and `shouldAdvanceStructuredResumeCursor`; two cursors
with two advancement rules were how position and body drifted apart
`older-page`/`loadOlder`, the unattached-refusal grace, generation guards and
the coalescer are unchanged. No host, wire or schema change.
Also fixes a second cost of the same branch: focus during a busy turn discarded
paged-in older items, shrinking the transcript to one bounded page mid-turn.
* fix(native-chat): preserve unavailable mixed-version session fences