Commit Graph
6 Commits
Author SHA1 Message Date
Brennan Benson d1aeba939f Native chat: remove unused journalCheckpoint and setConversationCommand (#30645)
* refactor(native-chat): remove three chat mechanisms nothing drives

- The adapter's optional startAnswered hook lost its only implementer in
  #27925; it was still forwarded through four layers and always read as
  answered. A start that never answered is read from the child's phase, as
  the exit already does.
- The saved lease's journalCheckpoint is only ever written as null and read
  only by validation. Records an older build saved with it still load: it
  joins the retired lease fields dropped at load, so the next write sheds it.
- The record store's setConversationCommand had no production caller. The
  tests that stood in for an older build with it now seed the saved row and
  reopen the store; three that only re-pinned another test are removed.

* test(native-chat): an older build's null journal checkpoint also loads

* Keep startAnswered cleanup in its owning PR

* Keep historical chat fixtures aligned with their readers
2026-10-20 17:12:41 -07:00
Brennan Benson 280ef3c6f3 fix(native-chat): accept sends through their own receipt (#26672)
* feat(native-chat): add a chat-owned command receipt store

Adds the agent_session_command_receipts table and an insert-if-absent
receipt write that runs inside a command's own journal transaction. A
receipt records the command's identity, canonical hash, status and a
pointer to the row holding its result; it dies with its chat record.
Nothing uses it yet: the existing operation ledger stays authoritative.

* fix(native-chat): key command receipts like today's ledger ids

Rows are keyed by caller and operation id; a send looks across callers and
other methods look up only their caller's row, matching the existing ledger.
Bad rows and newer table shapes read as unreadable instead of throwing.

* fix(native-chat): order receipt lookups by insertion and key by operation id

Global lookups now take rows in insertion order, so a read and an insert
agree on which receipt answers an id regardless of the host clock. The
primary key leads with the operation id, so its own index serves both
lookups.

* fix(native-chat): accept sends through their own receipt

Send (direct, queued, steer) and /compact no longer commit a separate
ledger reservation before the message is saved. Their command receipt is
inserted-if-absent in the same transaction as the submission or queued
draft; a repeated id replays from it, a changed request conflicts.
Operation ids are checked for syntax only, so age, clock skew, pruning
and the reservation's own storage can no longer refuse a send.

The ledger success row they already co-committed stays as a temporary
insert-if-absent write so families not yet switched keep seeing these
ids; it is never read on this path.

* fix(native-chat): answer a send's receipt one way, share /compact's id space with Send, wake delivery once

- After its commit, a send whose publication fails answers through the same receipt replay a retry takes, so one id gets one answer; the in-memory copy and journal reload are gone.
- /compact ids are global like Send's: a retry under a new connection replays instead of needing a second acknowledgment write.
- The receipt route calls its runner directly; the runner wakes delivery when a new message row commits.
- The compatibility ledger row expires from host time; each temporary site is labelled.
- Receipt read errors and post-commit failures are logged.

* test(native-chat): expect receipt answers where tests assumed the send ledger

Restart-continuation tests now model the case Send cannot vouch for (saved but
not loaded into memory) and keep it unconfirmed; a later throw after the message
is in the chat answers continued, as a resend would. Drop /compact's unreachable
legacy replay branch and fix stale comments.

* test: run the queued tests that open the journal database in the Node runtime project
2026-10-09 17:25:43 -07:00
Brennan Benson acea59c6d8 refactor(native-chat): remove the records-file and per-chat journal imports (#26038)
* refactor(native-chat): remove the records-file and per-chat journal imports

Every native chat user is on a build that already moved chat records and
history into the app-wide database, so the one-time copies are dead code:
the agent-sessions.json import and its owed-copy flag, the per-chat
journal.db import and the write-queue hold behind it, and the pre-SQLite
log.jsonl notice.

* refactor(native-chat): remove what only the deleted imports used

- JournalHostDatabase.unsyncedTransaction and the synchronous-pragma reset
  that undid it; JOURNAL_SYNCHRONOUS is now module-local. The stranded
  rollback test drives transaction() instead.
- deleteUnpublishedJournalRows and its SQL, plus its test.
- boundJournalStatusText.
- Stale comments naming the removed first-use copy (queued messages,
  send refusal example, JOURNAL_SYNCHRONOUS doc).
- Write-queue and readInOrder comments: a write also lags when issued
  behind one still waiting in line.
- Resolved-append ordering test binds the sink from inside a running read,
  so a resolver that read at handover now fails it.

* test(native-chat): run resolved-append in the SQLite runtime project
2026-10-06 23:03:32 -07:00
Brennan Benson 9905765e3b feat(native-chat): open structured chat's wire and stored records to registered agents, behind a negotiated capability (#25159)
* refactor(native-chat): keep the provider resume handle opaque to shared code

Shared structured-chat code parsed each provider's resume handle: Claude's
session id and branch leaf, Codex's thread id, through a 'claude' | 'codex'
union every new agent had to widen. The in-memory handle is now
{ transport, agent, nativeId, providerData? }: shared readers use nativeId,
lease and handle-chain checks compare transport and agent, and only the
Claude adapter reads its leaf (providerData).

Stored and wire forms are unchanged for Claude and Codex. One encoding
module writes their typed shapes and decodes both those and the neutral
shape a new transport uses, which an older build refuses as unreadable
rather than reading as Codex. Key and root strings, which fork seeds,
superseded creations and resume offers persist, stay byte-identical.

The journal's own handle type becomes the journal-row and attach-wire
encoding of the same handle, and the journal identity carries the
neutral handle (null before the provider proves one).

No user-visible change.

* fix(native-chat): derive journal-row provider handles from the journal identity

The journal row converter now takes the identity every caller already holds,
so a row's handle has one obvious constructor. Tests that wrote the in-memory
handle straight into journal rows now build it through that converter, and the
processless Claude fixture names a not-yet-proved handle as null.

* fix(native-chat): refuse a stored provider handle written in both forms

A typed Claude or Codex handle that also carries the neutral form's
transport, agent, native id or provider data named two identities; it
was read as Claude or Codex and the next write dropped the other one.
Such a row now stays unreadable and is set aside untouched.

* refactor(native-chat): route structured agents through registered definitions

The structured-chat shared layer named Claude and Codex in a closed router
type, in option reads, in frame classification, and in renderer label and
validation branches, so adding an agent meant editing shared types. Each
agent now has a definition (capabilities, resting option rules) owned by its
own module; the router is a registry of definition + adapter pairs built at
runtime setup, and shared code reads the definition instead of the name.

No behavior change for Claude or Codex; no wire or stored shape change.

* refactor(native-chat): name the structured agent list once in host types

The host types, conversation commands, provider-session ownership and the
runtime's create path spelled the structured agent list inline; they now use
the one shared type the record and wire already validate against.

* fix(native-chat): narrow the record before reading its agent's option rules

* refactor(native-chat): make the router's registrations the only agent definition lookup

The at-rest option reads looked definitions up in a second, separately populated map, so a
registered definition could route and declare capabilities while the static map decided which
options a chat at rest accepted. The router now answers `definition(agent)` from its
registrations, the at-rest readers take it through the host's adapter, and the built-in
definitions are listed only where the runtime composes the router.

* feat(native-chat): open the structured-chat wire and stored records to registered agents

A host's structured agents are the ones its runtime registered. Records, RPC
params, persisted tabs and the model catalog accept any registered agent instead
of naming Claude and Codex; each agent's definition declares the transport its
handles live in and the variable its account home pins. A new runtime
capability, agent-session.structured.registered-agents.v1, advertises that a
host accepts and lists its agents (agentSession.agents, with each agent's
capability record), and the host withholds any other agent's tabs and restart
offers from clients that do not advertise it.

* test(native-chat): cover registered agents on the wire, in storage and across versions

* refactor(native-chat): let the record store decide which agents' tabs exist

* test(native-chat): declare the pilot test agent's storage

* test(native-chat): read the old build's saved tabs through a parsed shape

* fix(native-chat): act on restart offers only for agents the calling client can show

A paired client too old to show an agent's chat was listed only the offers it could show, but
dismissing or continuing all reached every offer on the host, and a named continuation answered
with the host's whole remaining inventory. The client's audience now goes to the host with every
restart operation: only offers it sees are reserved, dismissed or returned. Without an audience
(this host's own process, or a client that shows every agent) nothing changes.

* test(native-chat): read an agent-registering baseline's storage on its own terms

The registered-agents downgrade test assumed its baseline release predates registered agents: it
expected the saved-tab parser to erase an unknown agent and called the record reader without the
agents list. Once a release with this change becomes the baseline, both break. The expectations now
follow what the baseline host advertises, and an agent-registering baseline is handed its own
Claude and Codex storage.

* refactor(native-chat): derive record-store admission from the runtime's agent registrations

Which agents a stored record may name and which agents the router drives came from two lists in
the runtime, so a newly registered agent could be routed while its records were set aside. One
list of registrations now holds each agent's definition and the factory for its adapter: the
store's admitted agents are derived from it before the store opens, and the adapters are built
from it once it has.

* fix(native-chat): hand the exit drain a promise for every registered agent

* fix(native-chat): let the adoption conflict check read any agent's ownership

Ownership rows name any registered agent since the stored records opened to them; the adoption
check compares by agent, so it takes the same open id. Only Claude and Codex still adopt.

* test(native-chat): use opaque handle in queued rejection fixture

* test(native-chat): share one Codex journal identity in the integration suite

Main grew the suite to the 800-line limit; the opaque-handle import pushed it
over. The two tests built the same identity inline.

* refactor(agent-session): name the handle's adapter state resumeCursor

Rename the neutral provider handle's providerData to resumeCursor before any
row persists the neutral form: it is an adapter-owned resume position (Claude's
transcript leaf), never identity. Claude/Codex stored and wire bytes are
unchanged; their typed shapes never carried the field.

State the stored-form contract (a handle's field set is closed; later per-link
data goes on the chain link, which every build preserves) and pin it with a
record round-trip test. Document that transport records the id space the
native id was minted in, which can differ from the agent's current transport.

* refactor(agent-session): one required agent registry; declarations admit what they claim

A StructuredAgentRegistry, built once from the {definition, adapter}
registrations, is now a required host dependency and the router routes with
it. The adapter interface loses its optional router-only capabilities?() and
definition?(); every reader (options at rest, thread goal, rewind, the
/compact handover) asks the registry. A live session still narrows rewind
through the adapter, and the host combines declared and narrowed in one
helper. The registry refuses a registration that declares compact, a thread
goal or rewind without the adapter method behind it.

/compact is admitted by the declared capability, so an agent that declares
compact:false gets the commandRefused fact instead of a thrown error.

Also: the cut-turn notice names the agent from the catalog, create-support
builds the account home through agentSessionAccountHome, and the turn
status text older clients read names the session's agent instead of
defaulting to Codex (Claude/Codex text unchanged).

* refactor(agent-session): the router applies the declared rewind itself

The router holds the registry, so its rewindSupport answers the owner's
declared rewind narrowed by the adapter, in one place no reader can bypass.
The host-side combining helper is gone; readers ask the adapter they hold.

* test(agent-session): register the agents the merged-in tests now need

The adoption replay test builds its host over this build's registered agents
instead of an adapter definition method, and the stored-form test passes the
stored agents the record guard now requires.

* chore(agent-session): the registry is the one lookup; drop the test-only empty capability record

* fix(agent-session): keep "dismiss all" restart offers dismissed for the desktop

The desktop renderer calls the restart methods as a paired runtime client
without the registered-agents capability, so it was handed a Claude/Codex
audience and its "dismiss all" took the scoped path: no dismissal fence and
no clearing of unwritten teardown witnesses, so a late teardown write could
bring a dismissed offer back.

The audience is now derived against this host's registry: a client that can
show every agent the host registered gets none, and dismisses exactly as
before (one fence for every offer). A client that truly cannot show some
agent still gets a final dismissal for what it sees: each cleared session is
fenced on its own (bounded, superseded by a dismiss-all fence) and only the
witnesses of agents it sees are dropped.

One predicate now answers which agents a client renders, for both tab
projection and restart offers; the Claude capability rule is folded in.

* fix(agent-session): a changed agent definition never hides that agent's chats

A record was readable only if every handle used the transport its agent's
definition declares today, and its account variable was declared by some
registered agent. A later build that drives the same agent over another
protocol, or under a renamed variable, would have set every existing chat of
that agent aside: no tab, no history, no error.

Readability now asks only that the record agree with itself: its agent is
registered, every handle shares one namespace owned by that agent, and the
account variable is a well-formed name. Whether this build can drive it (the
chain's transport is the one the agent speaks, and the account variable is
that agent's own, since it becomes the child's environment) is decided in
performAttach, the one admission every start of every agent passes through,
and refused there as hostUnsupported. A record pinning another agent's
variable is refused the same way rather than passing because some agent
declares it.

* refactor(agent-session): each agent's registration says where it runs and which account it pins

createSupport, create and the model catalog's account read still chose a
location rule and an account resolver by name (Claude, Codex, else none),
so a new agent would have needed a third branch beside the registration
list that already decides storage, routing and publication.

Each runtime registration now carries supportsLocation(location) and
resolveAccountHomePath({ launchEnv, location, purpose, workspacePath }),
and Claude's and Codex's rules move into their entries unchanged: a read
still syncs no home and starts no bridge, a Codex launch still trusts the
workspace first, and WSL locations resolve as before. The list exists
before the host is built, so none of these reads installs the host, and an
agent the list does not hold is answered no without opening the journal.

* test(agent-session): drop a duplicate registry key; keep fences out of the capsule state

A spread input already carries the registry, so the explicit key was
overwritten (TS2783). The session-fence helper now returns only the fence,
not the capsule state it was given.

* fix(agent-session): a scoped dismiss-all persists no per-session fence

The per-session dismissal fence added a forever-persisted capsule field
with an arbitrary cap that served no caller: only the local desktop calls
the restart methods, and on a host whose agents it all shows it gets no
audience and takes the unscoped, fenced dismissal. A scoped dismiss-all
(a caller that cannot show some registered agent) keeps that agent's
offers, clears only its audience's unwritten witnesses, and writes no
fence; a late write from this process is serialized behind it. The field
never left this unreleased branch.

* fix(agent-session): refuse an attach whose agent is not the session's own

The start check judges the record's agent while the router starts the adapter of params.agent, and the attach wire let the two differ. Admission now refuses agent !== provider as requestMalformed, so the agent checked is the agent started. Every host-built attach already sets them equal.

* fix(agent-session): offer to start a chat only when the start would accept it

The restart offer, a failure's retryable flag and the pre-send check asked only whether the adapter runs the chat's location, while the start also refuses a record this build cannot drive; such a chat was offered Resume and its Retry failed forever. One host predicate, hostCanStartRecord (location support and agentDrivesSession), now answers all four; adapterSupportsRecord stays the reader gate, and an undrivable chat's offer is kept, not retired.

* fix(agent-session): the model-catalog probe uses a record's account only if this build drives it

A record-scoped catalog read started the agent's lister under the record's account path whatever variable the record pinned. It now uses that path only when agentDrivesSession holds, and otherwise resolves the account as for a read with no record.

* refactor(agent-session): the record store admits agent ids; comments say where transport is checked

The store only ever asked whether an agent is registered, so its admission list is now the registered ids; the definition is the one home of an agent's transport and account variable. Comments that said the record store checks transport, and one that cited a nonexistent function, now point at agentDrivesSession and the attach admission. The isPersistedAgentSessionRecord(value, agents) call shape and the test fixture the cross-version probe imports are kept.

* docs(agent-session): the record store admits the registered agents' ids

* test(native-chat): dismiss-all through a remote client's audience keeps a newer Orca's offer

Uses an audience production sends (one that cannot show every agent), per review.

* chore(native-chat): keep the record store and recovery capsule under max-lines after the main merge

* test(native-chat): build the Stop-opened-turn test's identity and turn context the current way

The test (#25056) landed before the opaque provider handle (#24991), so main still built
the old {kind, threadId} handle; the turn context also needs this branch's agent registry.

* test(ratchet): require src/main/provider-process now that it has landed

* test(native-chat): keep main's opaque-handle import in the Stop-opened-turn test

Main's #25706 and this branch both added the import at different lines; the merge kept both.

* Keep saved chats readable without provider registration

* Keep stored-record compatibility checks independent of registration

* Keep saved providers in restart client audiences

* test(native-chat): type reveal fixtures without assertions

* test(wire): expose known agents in structured host fixture

* Supply startability dependency in the new Codex catalog fixture
2026-10-06 00:41:14 -07:00
Brennan Benson e817b0e237 refactor(native-chat): keep the provider resume handle opaque to shared code (#24991)
* refactor(native-chat): keep the provider resume handle opaque to shared code

Shared structured-chat code parsed each provider's resume handle: Claude's
session id and branch leaf, Codex's thread id, through a 'claude' | 'codex'
union every new agent had to widen. The in-memory handle is now
{ transport, agent, nativeId, providerData? }: shared readers use nativeId,
lease and handle-chain checks compare transport and agent, and only the
Claude adapter reads its leaf (providerData).

Stored and wire forms are unchanged for Claude and Codex. One encoding
module writes their typed shapes and decodes both those and the neutral
shape a new transport uses, which an older build refuses as unreadable
rather than reading as Codex. Key and root strings, which fork seeds,
superseded creations and resume offers persist, stay byte-identical.

The journal's own handle type becomes the journal-row and attach-wire
encoding of the same handle, and the journal identity carries the
neutral handle (null before the provider proves one).

No user-visible change.

* fix(native-chat): derive journal-row provider handles from the journal identity

The journal row converter now takes the identity every caller already holds,
so a row's handle has one obvious constructor. Tests that wrote the in-memory
handle straight into journal rows now build it through that converter, and the
processless Claude fixture names a not-yet-proved handle as null.

* fix(native-chat): refuse a stored provider handle written in both forms

A typed Claude or Codex handle that also carries the neutral form's
transport, agent, native id or provider data named two identities; it
was read as Claude or Codex and the next write dropped the other one.
Such a row now stays unreadable and is set aside untouched.

* test(native-chat): use opaque handle in queued rejection fixture

* test(native-chat): share one Codex journal identity in the integration suite

Main grew the suite to the 800-line limit; the opaque-handle import pushed it
over. The two tests built the same identity inline.

* refactor(agent-session): name the handle's adapter state resumeCursor

Rename the neutral provider handle's providerData to resumeCursor before any
row persists the neutral form: it is an adapter-owned resume position (Claude's
transcript leaf), never identity. Claude/Codex stored and wire bytes are
unchanged; their typed shapes never carried the field.

State the stored-form contract (a handle's field set is closed; later per-link
data goes on the chain link, which every build preserves) and pin it with a
record round-trip test. Document that transport records the id space the
native id was minted in, which can differ from the agent's current transport.
2026-10-05 14:48:12 -07:00
Brennan Benson 5cda0f4508 refactor(native-chat): keep agent-session records in the chat journal database (#24006)
* fix(native-chat): report a failed startup chat reconcile instead of failing app startup

At startup the chat host re-checks every saved chat's lease and writes the
result to agent-sessions.json. If that write failed (the file lock gave up,
the file could not be written, or the file was written by a newer Orca and
is read-only here), reconcileRestartLeases rejected, the startup IPC call
rejected, and the renderer fell into its degraded "Session restore failed.
Changes won't be saved until restart" mode.

The reconcile is bookkeeping: a lease left unreconciled grants no writer,
and every attach, send and read of a chat reconciles its own lease again.
So the startup reconcile now reports its failure through a new optional
host dependency, onStartupReconcileFailure, and resolves. The runtime
routes it to its onError sink under the scope
structured-agent-session-startup-reconcile, or logs it when no sink is
installed (the desktop installs none).

* fix(native-chat): read restored chats without waiting on lease bookkeeping

With native chat on and a chat tab open at quit, the renderer's startup
also awaits the chat tab restore (session.tabs.listAll). That restore
re-ran the lease reconcile before reading each chat and rethrew its store
failure, then recorded each restored tab as visible through a store
transaction that throws on a held lock or a read-only store. Either one
failed the restore, so startup still fell into "Session restore failed".

Reading a chat grants no writer, so the reconcile startup and the restore
run is now a reader's: createReaderReconcile never throws, answers whether
every lease is settled (recovery is resolved only then; the journal opens
either way), and reports each distinct failure once until a reconcile
settles. Attach and agent start keep the strict reconcile. The restore's
tab republish logs a failed visibility write and still publishes the tab,
since a client drops every unpublished chat tab; user-driven publishes
still refuse.

The host dependency is renamed onLeaseReconcileFailure (scope
structured-agent-session-lease-reconcile), since it now also reports for
reads.

* fix(native-chat): keep every record-store write off the startup chat read path

Round-2 review found two more writes on the startup chat restore that
could still fail it and put the app into "Session restore failed":
republishing a /clear replacement recorded its tab visibility strictly,
and resolving a chat's recovery rethrew its store error. The restore
also paid one lock wait per tab and per batch of chats while the lock
stayed held.

The restore now derives tabs from state it already holds:
- publishStructuredAgentSessionTab splits into the strict write and
  projectStructuredAgentSessionTab, which only updates the runtime's
  snapshot. The restore and /clear replacements only project: a saved
  tab index already lists every restored chat, and a /clear moves the
  tab in the same write that commits it. visibilityWriteMayFail is gone.
- Chats a legacy profile restores that the index does not list are
  recorded in one best-effort transaction (store.showSessionTabs), so a
  failure leaves the index absent to seed again rather than partial.
- The read restore's recovery resolution is caught and reported through
  onLeaseReconcileFailure, deduplicated with the reconcile's reports.
- Once lease bookkeeping fails in a restore pass, the rest of that pass
  skips it, so a held lock costs one wait for the startup reconcile and
  one for the restore, however many chats are open.

User actions (create, reveal, attach, send, the /clear commit) keep
their strict writes.

* test: open, seed and read the agent-session record store through one harness

Tests that open the durable agent-session record store, seed it, or read
back what it persisted now go through agent-session-record-store-test-harness.ts
instead of calling AgentSessionRecordStore.open or touching agent-sessions.json
themselves. A later change that moves the store into the chat database then
changes the harness instead of every test. No production code changes.

Tests whose subject is the JSON file itself (its .bak recovery, salvage,
schema versions, permissions, and what older builds read back) keep reading
and writing the file directly; the storage move rewrites or deletes them.

* fix(native-chat): start each restore pass from one lease check and stop its bookkeeping at the first failure

The restore now runs one reader lease check for the pass and lets each chat
re-check and resolve recovery only while the pass is still settled. The
first refusal or failed write clears it for the rest of the pass, and every
chat is still opened for reading. With another process holding the lock,
startup waits on it once in prepare and once in the restore, however many
chats are open; a legacy profile waits once more for its tab-index seed.

* docs(native-chat): correct restore comments and a test name to match the final design

* test: address the record-store harness by the host's state directory

The harness took the store's own folder, so each caller picked one
(join(root, 'store'), or 'agent-sessions' where a test read the store the
runtime owns). A later change that moves the store into the state
directory's journal database could not tell those apart, and would have
had to edit every caller again.

Every harness function now takes the state directory, the one the test's
journal database and recovery capsule already live in, and keeps the
store in the same subfolder the runtime uses. Callers pass that directory;
store-only tests pass their temp directory unchanged. Format tests that
share a directory with harness calls take the file path from
testAgentSessionStoreFilePath.

The folder name moves from a private constant in the runtime to
AGENT_SESSION_STORE_DIR_NAME beside the store's file name, so the harness
shares it without importing the runtime. Its value and every path built
from it are unchanged.

* refactor(native-chat): keep agent-session records in the chat journal database

The record store's records, operation ledger, retired claim keys and chat tab
index become tables in agent-session-journal.db (user_version 4). The version-4
migration copies agent-sessions.json in its own transaction and never writes,
renames or deletes that file or its .bak. Each store write is one journal
transaction over exactly the rows it changed, checked with the load rules; the
file lock, the external-change refresh and its hash, the .bak rotation, salvage
and the hot-path recovery fence are gone from the store.

* wip: importer tests

* test(native-chat): cover the records migration, the import, row writes and read-only records

* docs(native-chat): retire comments that describe the records file as the live store

* test(native-chat): drop the record-store harness's leftover file path and type the import fixture

* test(native-chat): let the host harness cleanup wait out a recovery-offer read's lock

* fix(native-chat): let Stop reach the agent when its ledger row cannot be written

Stop's operation-ledger row now shares the database with the chat history, so
damage, a full disk or a stranded transaction on that write refused the Stop
before the interrupt. A cancel plan now takes its decision from the committed
ledger in memory, runs without settling, and warns that the row was skipped.
Other mutations answer proven damage with the typed "Unable to load this chat."
refusal instead of the raw SQLite error.

* fix(native-chat): answer whether a profile holds chats from the database's rows

Every host install creates agent-session-journal.db, chats or not, and the
version probe created it too, so its mere existence made every profile that
ever installed the host wait on host install and reconcile at startup. The
check now opens the database read-only and looks for a record or tab row,
lets the records file answer while its import is still owed, and counts an
unreadable database as present. The version probe no longer creates the file.

* fix(native-chat): open a chat from history when its tab index cannot be written

Over records a newer Orca wrote, every write is refused, so opening a closed
chat from Agent Session History failed on the tab-visibility write and the chat
read as unreachable. Like closing a tab, opening one now reports a failed
restore-index write and still publishes the tab.

* fix(native-chat): keep the records import owed when the backup read fails transiently

A torn records file whose .bak could not be read (EACCES, EIO) was reported as
unusable, so the migration completed with nothing copied and never retried.
A non-ENOENT read failure of either copy now carries its cause, which the
importer classifies as a read that can clear.

* test(native-chat): pin that an unreadable records file never falls back to its backup

* fix(native-chat): restore imported chats' tabs when the records file had no tab index

A chat created while the import was owed recorded a tab index holding only
itself. When the file it later imported had no index, that index still read
as recorded, so the imported chats' tabs never came back. The import now
clears the recorded marker in that case, and restore falls back to the
profile's tabs.

* refactor(native-chat): drop the unused in-transaction store write

Nothing called it, and it bypassed the write queue and the read-only refusal.

* docs(native-chat): say that an unusable records file is left untouched but never re-imported

* refactor(native-chat): keep the provider handle chain check as main has it

The chain-validation refactor has no measured need in this change.

* docs(native-chat): retire lease-renewer comments that describe the records file as the live store

* fix(native-chat): keep a throwing failure sink from failing the startup chat read

The lease bookkeeping failure reporter called the host's failure sink
directly, so a sink that threw turned a reported, recoverable store failure
back into a rejected startup reconcile or read restore. The reporter now
catches a sink throw and logs both the original failure and the sink error
with console.warn.

* test(native-chat): wait for a replaced host's restart-offer writes before cleanup

A restart test replaces the host without tearing the old one down, so the old
host's fire-and-forget restart-offer withdrawal could still hold the recovery
capsule's lock directory when cleanup removed the test directory (ENOTEMPTY).
The harness now hands hosts a capsule that tracks running operations and waits
for them before removing the directory, replacing the rm retries.

* docs(native-chat): retire the abandon helper's note that the store re-creates its directory

* fix(native-chat): restore a chat opened while the import was owed beside the profile's chats

When the imported records file had no tab index, restore fell back to the
profile's saved tabs, which never list a Claude chat, and the seed then
rewrote the tab table without the chat opened while the import was owed.
The tab rows that chat left are now loaded as unrecorded, restore takes them
together with the profile's chats, and the seed keeps their tab ids.

* test(native-chat): pin that a create whose tab index write fails still opens the chat

* docs(native-chat): say why restore puts chats opened while the import was owed first

* test(native-chat): replace a ledger row rather than change it in place in the Send-now rerun test

The record store freezes published rows in tests, so setting a row's outcome
in place threw; the test now swaps in a changed copy, as its sibling cases do.
2026-09-30 14:45:32 -07:00