Commit Graph
151 Commits
Author SHA1 Message Date
OrcaWinandOrcaWin fdcf887bf0 chore(lint): fix suppressed lint violations instead of ignoring them (#33294)
* chore(lint): remove disable directives that suppress nothing

Each was reported unused by the oxlint config that owns its rule (root,
casting, react-doctor, anti-slop, native-plugins), including 17 file-level
react-doctor blanket disables whose rule no longer fires. An unused
react-hooks disable was masking a react/purity finding in
useAgentBucketCounts (compiler rules skip functions with hook
suppressions); suppress that one explicitly where the code explains it.

* test(tab-bar): use typed tab/file fixtures instead of @ts-expect-error partials

* chore(lint): fix misc suppressed lint violations instead of disabling

- consistent-type-imports: the "vi.importActual requires inline import()"
  rationale was wrong; use typed vi.mock(import(...), importOriginal) or a
  type-only namespace import.
- no-control-regex: source-control commit/push failure summaries, shell PATH
  hydration, and ipynb ANSI parsing reuse the shared ansi-escape-sequences
  stripper and control-character pattern instead of private copies.
- no-explicit-any / no-useless-spread / no-this-alias: typed `this` in
  bridge test mocks, drop blanket file-level disables, drop a this-alias.

* refactor(main): replace avoidable type assertions with checked narrowing

17 production cast suppressions in main/wsl-guest removed: untrusted IPC
and RPC payloads (Jira issue update, WSL guest plugin install, handshake
type) now narrow through isRecord/type guards and build typed results;
redundant casts dropped; hand-rolled object checks and errno casts use the
existing isRecord / hasErrorCode helpers.

* refactor(renderer,mobile): replace avoidable type assertions with checked types

13 production cast suppressions removed: RPC failures read through the
`ok` discriminant instead of casting to RpcFailure; react-native-web host
refs narrowed with instanceof HTMLElement; redundant reply/diff casts
become checked annotations; the TUI agent param uses z.custom(isTuiAgent)
like its sibling schemas; the agent-status slice cast that hid missing
actions is gone so the creator's return type checks the whole slice.

* chore(lint): drop dead tiptap wrapper; scope anti-slop mocking rule to test support

- safe-react-node-view-renderer patched handleSelectionUpdate, which
  @tiptap/react 3.31.3 no longer has, so it always returned early. Use the
  stock ReactNodeViewRenderer.
- The anti-slop test override only matched *.test.{ts,tsx}, forcing
  file-level no-module-mocking disables in test harnesses and fixtures.
  Match *-test-harness, *-test-fixture(s), .test-fixture and *.test.mjs,
  then drop those disables and the now-redundant per-file exemptions.

* test: replace repeated cast suppressions with typed fixtures (main/shared/e2e)

Net -119 cast directives. Typed fixtures replace recurring casts:
unusedSshConnection/unusedOrcadHostTransport placeholders, a typed
readPersistedState harness read (exposed a test treating optional
httpProxyUrl as always set), real Response objects in fetch spies, an
updater main-window fixture, a CDP webContents adapter, one shared
releaseModuleExport for cross-version tests, and requireOverlayDir in
place of `as string` on possibly-undefined overlay paths.

* fix(runtime): validate loaded device rows and host scope instead of casting

Two cast suppressions claimed checks that did not happen:
- device-registry-file-read: "normalizeLoadedDeviceRow re-validates every
  field" was false; deviceId, token, name and timestamps were never
  checked. Rows without an identity are now dropped and the file is set
  aside through the existing corrupt-file path (registry reads as
  doubtful, so no revocation verdicts); a non-string token is stripped
  so the row stays listed but cannot authenticate.
- client-ui: "only optionality differs" was false; any string reached
  persisted workspaceHostScope. Narrow it with isWorkspaceHostScope and
  ignore unknown scopes (wire schema unchanged for mixed versions). The
  remaining cast now states what it actually covers.

* test(runtime,ipc): drop redundant cast suppressions; typed runtime store fixture

-98 cast directives across runtime/ipc tests: 61 casts that typecheck
without the assertion; a typed vi.fn for scoped invalidation; a
partialRuntimeStore fixture in place of `store as never` for
OrcaRuntimeService; typed worker terminal/authority builders in the
incarnation-fallback suite (the old stub lacked runtimeId, ptyId,
worktreeId and launchTokenHash) plus removal of stacked duplicate
directives there.

* chore(lint): typed vi.mock imports and explicit listener snapshot in shared-bucket tests

Remaining files from the misc-rule pass that overlapped other buckets:
codex-accounts mocks use the typed vi.mock(import(...)) form instead of
a consistent-type-imports disable; the daemon late-exit fixture copies
its listener set with Array.from instead of a no-useless-spread disable.

* refactor(github,mobile): narrow routing settings type; drop remaining avoidable casts

- work-item-routing cast a one-field object to AppState['settings'] under
  a claim that readers only consume activeRuntimeEnvironmentId. Return the
  existing RuntimeFocusSettings pick instead and narrow the cache-key
  chain's parameters, so the compiler enforces that claim.
- mobile file preview reads refusals through the RpcResponse `ok`
  discriminant rather than a cast.
- terminal document host-notify accepts `unknown` errors, removing the
  message-bridge cast.
- native chat readability drops a redundant repo-list cast.

* test(renderer): replace repeated cast suppressions with typed fixtures

-140 cast directives (and ~150 unsuppressed casts of the same shapes):
- mockPanePtyArgs owns the connectPanePty fixture casts, so a wrong
  argument order or fixture is now a type error; drop binding casts for
  members PanePtyBinding already declares.
- Seed AppState tab rows with the typed makeTerminalTab; this exposed a
  split direction 'row' that is not a TerminalPaneSplitDirection.
- Partial GlobalSettings use getDefaultSettings overrides; Jira-only
  stores use createTestStore; runtime environment rows/statuses come from
  a typed paired-runtime-environment fixture.

* chore(lint): drop no-module-mocking disable now covered by the test-fixture glob

* test(e2e): fix all 221 grandfathered e2e type errors; baseline to zero

The e2e typecheck baseline grandfathered 221 errors in 119 files. About a
third were tests drifted from the current API, several of which would
fail or mislead at runtime:
- terminal-shortcuts called a nonexistent xterm buffer method;
- the daemon-lifecycle fake lacked terminateOwnedTree, hidden by importing
  its type from a module that does not export it;
- arguments in the wrong position (listProcesses(hostId, opts), a dropped
  pty.write inputKind, an extra toWebTerminalSurfaceTabId argument);
- removed enum values ('drawer', 'idle', 'editor',
  'session-insecure-origin') and a launch_source the telemetry schema
  rejects;
- missing required fields (turnScope, childWorkOpen, providerIdentity,
  reviewLookupOutcome, runtimeServiceAccount behind an `as RelayConfig`);
- dead reads (tab.customTitle, bucket.targets, changedPixels).
Also lifts variables that narrowed to `never` after poll-callback
assignment, and adds a typed xterm render-dimensions accessor. Includes
the cross-version suites' shared releaseModuleExport adoption.

* chore(typecheck): gate the now-clean e2e typecheck in local lint

The e2e baseline is empty, so the ratchet now rejects any tests/ type
error. CI already runs it via run-ratchets; add it to pnpm lint's
explicit ratchet list and replace the stale 'not yet clean' note.

* test(mobile): typecheck every mobile test; baseline 105 files -> 0

mobile/tests-typecheck-baseline.txt grandfathered 105 test files (531
errors) whose types CI never checked. Contract changes had landed without
these tests following, and type pins in them silently checked nothing.

- Root cause: salvagedOptional (src/shared/zod-salvage.ts) cast away
  zod's optional marker, so every reply type built on it claimed
  salvaged fields were always present. Return a real ZodOptional;
  runtime is unchanged (the outer optional short-circuits undefined).
- Production now takes the port it uses: RpcOperationSender instead of
  RpcClient for send-only callers, a narrow fetch signature, and only the
  request fields push-receive reads.
- findAllByHostType/findByHostType replace `'Text' as never` lookups;
  stubRpcClient gives tests a typed RpcClient.
- Real test bugs: settings-host-client-lifecycle "relay" hosts were
  direct hosts (relayHostId no longer exists); two toEqualTypeOf pins
  could never hold; a recovery hook ran with an undefined scope key;
  a create-PR test cast to a never-imported type; "no URL" assertions
  passed on null; plus stale required props, args and enum values.

* test(e2e): parse workerStart result instead of casting it

* test(runtime): typed partial runtime stores with production-shaped fixture data

Route every partial store passed to OrcaRuntimeService through
partialRuntimeStore with typed row builders, removing 60 cast directives
that hid fixture data production never produces:
- paired-client-navigation-test-harness keyed setWorktreeMeta writes by
  `[object Object]` (it took an id where the store takes a target), so
  reads never saw them;
- invalid worktree lineage origin/capture and incomplete workspace lineage;
- getWorkspaceSession returning null where the store returns the default
  session; getGitHubCache returning undefined;
- orcad migration states missing migrationId/manifest/receipt, with a
  toEqual pinned to that incomplete shape;
- partial repos, metadata, folders, project groups and notification rows.

* refactor(renderer): resolve suppressed React hook lint instead of disabling it

109 of 187 react-hooks/react-doctor suppressions fixed:
- ~95 exhaustive-deps disables only hid stable refs and setters passed
  through controller objects; list them (identity never changes, so
  nothing re-runs), plus a few real stale closures (tabId/worktreeId).
- Effects keyed on one value that read the latest props now use
  useEffectEvent (WebConnect, skill freshness, Bitbucket dialog seeding,
  Landing preflight, automation catalog, structured-session recovery,
  file explorer).
- The conditional layout-owner hook is selected once at module load from
  the build constant instead of a rules-of-hooks disable that also hid
  the whole controller from compiler and react-doctor rules.
- A hook disable was masking a react/purity Date.now() in the palette's
  recent-tabs memo; sample it when the snapshot is captured.
- Three identical debounced-search effects share
  useTaskSearchDebouncedApply; layout snapshot merge helpers move out to
  stay under max-lines.
Remaining suppressions are mount-once resources, invalidation epochs,
deliberately keyed fields, and test harnesses that mock the dispatcher.

* chore(ratchet): exempt test harnesses and fixtures from the focus-read ratchet

The ratchet skipped *.test-fixture.ts but scanned *-test-harness,
*-test-fixture(s) and *.test-support files. Those seed settings with
getDefaultSettings (which the ratchet discovers as a focus reader) and
never route host work, so 17 baseline entries were false positives, and
replacing a test's `settings: {...} as never` with typed defaults tripped
it. Match the repo's test-support names and prune (which also drops one
stale entry), and rename the frame-ordering harness to *-test-harness like
its siblings. A blanket "-harness" match would also exempt
e2e-terminal-pty-harness, which production imports, so the names stay
explicit.

* test: merge duplicate shared/constants imports

* revert(file-explorer): restore original tree load effects pending SSH e2e investigation

* test(runtime): drop RuntimeStore cast now that the fixture is typed

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-24 19:37:19 -07:00
Neil db8fc6d0cc refactor: group agent-session conversation and message identity (#33324) 2026-10-24 18:43:40 -07:00
Brennan Benson f04b831a61 Record Stop immediately and settle subagents from exit proof (#28051)
* Observe Claude readiness through the host status

* Keep optional readback behind host readiness

* Recheck launch permission after host readiness

* Preserve the awaited child during permission revalidation

* Await Codex protocol open under host ownership

* Await ACP protocol open in the host

* Derive ACP opening from its protocol session facts

* Exercise unanswered ACP options with an explicit silent route

* Await Pi open and fail signed-out construction

* Read Pi catalog facts from awaited opening

* Keep the Pi fixture exit observer

* Record failed owned opening before the first message

* Abort a matching provider open when its child exits

* Cover provider exit during an unanswered host open

* Cover explicit failure facts on a host-stopped child

* Capture message choices per accepted send and preserve picker state

* Apply accepted Claude message choices at prompt handover

* Apply captured message choices at provider dispatch

* Bind native chat choices to accepted message dispatch

* Reset Codex turn choices for each captured message

* Resolve Codex message defaults from native facts

* Align message choice and Pi default fixtures with provider dispatch

* Align option fixtures with saved message choices

* Match Codex message choices to the active turn origin

* fix: let pending commands reach handover refusal

* Abort pre-prompt message choice writes with host-owned wait

* Verify message choices through provider input boundaries

* Keep legacy mobile test hosts explicit about message choices

* Align Stop close causes and request-note fixtures

* test: align ACP Stop note fixtures with settled behavior

* Verify close intent reaches the native adapter

* Record the Stop request before waiting for interruption

* Keep Stop cleanup independent of request-note persistence

* Keep startup waits cancellable and preserve operation order

* Derive Codex startup exits from host phase

* Open Codex protocol in background-task test fixtures

* Keep ACP startup ownership until handshake completes

* Run ACP opening catalog fixtures with Node SQLite

* Verify saved ACP picks without live option writes

* Keep ACP catalog bookkeeping from failing an options read

* Restore signed-out restart and fail Pi startup faults

* Run Pi opening catalog fixtures with Node SQLite

* Preserve message choices across cold clients and kept cards

* Remove the unused provider option restore ledger

* Clarify launch choices and legacy option hydration

* Remove remaining restore-ledger forwarding and assertions

* Remove unused ACP and Pi restore bookkeeping

* Report late Codex exits and keep card Cancel notes scoped

* Remove the Pi close wrapper left by unused cause plumbing

* Update Stop tests for the simplified close contract and Node journals

* Update remaining close and interrupt assertions after cause removal

* Keep live choices and delivery order while agents start

* Drop retired startup revision from Codex opening

* Keep live-option cancellation until message choices and clarify catalog reads

* Hold ACP option replies in live-write cancellation regressions

* Remove retired Pi startup revision and match saved-pick behavior

* Check running choices and retained rewind messages after delivery integration

* Use observed root exit in Codex close reports

* Apply startup picks and let parked messages release operations [skip ci]

* Remove unused Codex opening report field [skip ci]

* Keep ACP restoration facts local to the provider [skip ci]

* Apply Pi startup picks after readiness and remove unused open facts [skip ci]

* Cover goal changes during the held Codex message wait [skip ci]

* Save a waiting startup pick after Stop ends its opening [skip ci]

* Save choices when startup ends or a replacement is still opening

* Check saved choices separately from Claude startup reports

* Release scripted Codex startup before awaiting queued picks

* Check the saved picker selection in per-message choice fixtures

* Exercise current choices and eager client capabilities in runtime fixtures

* Keep accepted Stop requests in runtime settlement assertions

* Reuse runtime fixture reads and type the fake connection directly

* Type the Codex child-proof fixture connection directly

* Align startup fixtures and preserve observed Claude exits

* Honor queued send authorization in Claude runtime fixtures

* Open Codex protocol sessions before Stop fixture dispatch

* Wait for the opened Codex child before runtime notifications

* Keep repeated permission picks usable and align Codex fixtures

* Validate pending picks against the live option state

* Order startup picks before later messages and retain flow regressions

* Keep reviewed lifecycle contracts while pruning duplicate tests

* Save picks after a proven exit without requiring a child

* Type the startup reply fixture against the Claude control contract

* Keep Codex startup flow contracts and remove overlapping adapter tests

* Restore the committed dependency lockfile after test tooling

* Keep startup recovery flows and remove duplicate Pi handshake cases

* Cover both Pi startup choices in the delivery flow

* Match the Pi prompt entry recorded by the scripted wire

* Honor the before-input callback in the startup reply fixture

* Keep message choice flows and remove overlapping selector tests

* Retain Stop flows and remove repeated reducer and adapter cases

* Keep Stop note writes independent of full session context

* Keep startup option menus independent and show the saved choice

* Exercise startup picks across real menus and the runtime

* Match startup picker fixture to saved message choices

* Keep saved effort visible when picking a model during startup

* Include the running-turn scope in repeated Stop fixture

* Use the current attach contract in import fixtures

* Keep the Codex journal translator within its line limit

* Match ACP startup fixtures to saved picks and late Stop behavior

* Check startup effort after saved picker choices are acknowledged

* Complete Stop target types and keep the effort regression concise

* Use requested picker values in the effort regression acknowledgement

* Exercise Stop receipt replay across a resumed provider child

* Require owned provider spawn and one host-awaited open (from #28055)

* Restore the live checks the startup cleanup's deleted tests pinned

One assertion each, in the surviving suites: a stale-generation exit is
ignored, a failed create lets its sink go, a readback before open is saved,
an idle stop that fails is retried, a no-pick Pi start teaches its default,
an older client's pre-spawn failure and packed answer, an undrivable chat
stays readable, the resting slash menu stays on its own host, the main agent
reads apart from subagents, restore publishes status, naming runs off the
host's status feed, and a throwing logger never reaches the operation.

Also corrects the reliability gate note that claimed the send-idempotency
suite replaced the refusal-retry tests.

* Read a rewind in doubt outside the chat's serialize and apply it in the commit

The started settle and the attach's ready-child path read the provider's
history first, then apply what it proves in a short commit that re-checks
the rewind it read.

* Accept messages while the agent starts: run the start outside the chat's serialize

The chat's serialize was both where a send was admitted and where its agent
started, so every slow step of a start (closing-child join, lease
reconcile, recovery probes, resolvers, history read, spawn, sink drain,
provider dispatch) held every later send.

A start now registers in one short step before its first wait: its abort
controller (which Stop, close and quit already reach outside the lock),
its startup clock (the 10-minute ceiling now counts from here) and quit's
drain. Every wait after runs outside the lock, raced with that signal, and
every write is a commit that first re-checks the start is still the
session's. A start that loses the check is late: its spawn is closed
through the post-acquisition failure path and nothing is handed to it.

- The delivery loop's prepare returns a start handle; the loop awaits the
  start outside, then re-prepares. Handover writes the pending row and
  begins the dispatch abort handle in one commit, calls the provider
  outside after re-checking the child, and resolves in a second commit.
- Ready operations claim a start instead of running one in their step.
- A client attach runs the same steps under one serialize, as before.
- An exit reported between spawn and publish is held for the publish
  commit, which ends the child it indexes.
- An unproven close still fails only what was queued when its start was
  asked for.

* Test that a send is accepted at every wait an agent start makes

One whole-flow test holds the start at each wait in turn (closing-child
join, reconcile probe, recovery re-probe, resolver, spawn, sink drain,
open, started recovery, pending row, provider call): a second send is
accepted at once, a Stop is recorded without waiting and nothing reaches
the provider, and what was sent goes to the published child once, in
order. Also quit before spawn, concurrent sends, a lease left recovering,
and an idle sweep racing a send.

* Bound each startup step on its own and drop the startup silence rule

A start no longer ends after 60 s without output: output is not protocol
progress. Each protocol step now has its own bound where its handshake
lives, and one 10-minute ceiling from registration stays as the outer bound.

- Codex: initialize 15 s and thread start/resume on the connection's 30 s
  default again, instead of the startup ceiling.
- ACP: initialize 60 s, authenticate 120 s, session new/load/resume/fork
  300 s; turns stay unbounded and option writes keep their caller's bound.
- Claude: the initialize answer has a 60 s bound; past it the open fails
  with the ordinary startup-failure words and the host ends the child.
- The onOutput plumbing that only fed the silence rule is removed.

* Count Pi ready once it reports its state

Pi's start no longer waits for its model listing and slash commands.
The open returns after get_state and the unknown-provider check; the
listing then arrives as an options report (with the catalog listing the
start resolved) and the commands as a later publish. An empty model list
no longer fails the start as signed out: a signed-out Pi runs the unknown
provider, which still fails it, and a prompt it cannot run is refused
with the sign-in words by the existing prompt path.

get_state keeps the connection's 30 s request bound instead of the
startup ceiling.

* Test that model and effort picked while a chat is created ride its first message

A slow host's create reply arrives after the user picked a model and effort. The picker's view
layers the launch's held picks over its seed and the host's saved selection, and every send
carries what the picker shows, so the first message carries the picks whether the host's status
names its selection or not, and while the last pick is still on its way to the host.

* Start a phone's new empty chat on its first message

The phone's new chat is an agent.launch with no prompt, which always created the chat with its
agent started, while desktop chats start on the first message. The phone now advertises the
create-without-start capability, and a launch with no prompt from a caller advertising it creates
the chat at rest, keyed in the ledger by its create intent as a client's create is. Launches with
a prompt, the CLI (which advertises nothing), orchestration workers and ensure still start at once;
an older host ignores the capability and stays eager.

* Take main's lockfile; the branch changes no dependencies

* Restore main's exact text in files the merge only reformatted

The pre-commit formatter rewrote 37 of main's files staged by the merge, though this branch never changed them, pushing two past the line limit. Committed without the hook because the hook would reformat main's text again; CI lints the result.

* Fit the startup tests to the owned-spawn, host-awaited-open contract

Test doubles gain the mandatory open and an acquisition generation, the
removed started event gives way to the rig's proveStart, and two literal
types are tightened for CI's typecheck. A Stop during a held start writes
no host note on this branch: the taken-back send draws its own
stopped-before-start row, which the startup-attempt test now asserts.

* Fit the remaining CI checks to the startup limits and current releases

- Run agent-launch-at-rest.test.ts in the SQLite Node runtime project.
- Pin the send-options cross-version case to v1.4.228, the first published release with agent-session.send-options.v1; v1.4.226 was never pushed.
- Load the baseline's isStructuredTab from src/shared (moved in v1.4.228), falling back to the renderer path for older baselines.
- End the never-proven start with a short startup ceiling: the silence limit it relied on is gone, and the ceiling counts from registration.

* Read the option stamp from the chat's one host-execution selection

* fix: preserve startup authority and accepted agent choices

* Retain dispatch authority and reconcile accepted options

* fix(native-chat): preserve accepted choices and cancellation authority

* Restore established client selection flow

* Preserve provider output observation and typed startup fixtures

* fix: preserve initial Claude launch choices and effort readback

* test: complete Claude catalog session fixture

* Preserve incoming main files after merge formatting

* Preserve compact handover through startup merge

* Type instant startup fixtures before their open is supplied

* Keep browser sign-in under its existing expiry owner

* Finish the main merge: retired-model display, start rewind cut, new tests

A model the Claude CLI retired is reported again, so the picker shows what the child runs and the
next send moves the chat off it. The start reads a rewind in doubt against the handle it answered
with, the record its commit then applies to. Main's tests follow the start's open: the admission
fixture holds a real turn, and a sign-in close during setup fails the open.

* Type the start's rewind record as the store returns it

* Type the Sleep test's held open as the provider open it replaces

* Sleep counts a chat stopped once the start it aborted leaves it at rest

A start runs outside the chat's lane, so Sleep's step can run before the start it aborted has
settled; it then re-reads the chat once that start is done, instead of reporting it unverified.

* Keep Sleep's start-abort bookkeeping beside the rest of Sleep's stop

* Keep the listed background Stop lifetime cases

The two #29905 cases lived in the turns test this branch removed. They pin that a listed Stop never ends a helper lifetime it did not capture: a same-id restart during acceptance, or a refused helper that appears mid Stop-all, which must not close the agent.

* Reconcile test registries and queued provider work accessor

* Update CI scope fixtures after removed connection test

* Bound native chat Stop protocol steps and omit empty startup feedback

* Supply current session fence in empty-start Stop test

* Align goal cancellation test with accepted-message feedback

* test(native-chat): cover distinct repeated Stop through Codex adapter

* fix(native-chat): save dispatch proof before Stop closes the child

* ci: retrigger checks for Stop dispatch repair

* Keep accepted Stop effects through dispatch settlement

* Capture earlier Stop evidence before writing request feedback

* Preserve named Stop runtime effects across turn advancement

* Combine projection imports in named Stop regression
2026-10-23 22:10:55 -07:00
Brennan Benson c84817e8f9 fix(native-chat): preserve accepted command outcomes across agent startup (#31605)
* fix(native-chat): a goal set while the agent rests clears the composer

Setting a goal starts the agent when none is running, which moves the
session's runtime fence before the host answers. The composer dropped
that successful answer as stale, so the objective and Goal chip stayed.
Keep the reply for goal changes as for the other writes that may start
the agent.

* fix(native-chat): settle each command independently of runtime changes

* fix(native-chat): scope rewind timeout feedback to its issuing pane

* test: use checked native chat command fixtures

* test: complete native chat presentation fixtures

* fix(native-chat): admit Stop before capability discovery

* test(native-chat): consolidate overlapping startup coverage

* test(native-chat): preserve IME composition on goal acceptance

* fix(native-chat): capture each action before async preparation

* fix(native-chat): preserve shared Stop failure reporting

* test(native-chat): cover already-sent queue notices after disposal
2026-10-23 17:11:05 -07:00
Brennan Benson 47484b13d6 fix(native-chat): keep queued messages in the chat's own record, so a refused one can't be skipped (#30418)
* fix(native-chat): a refused queued message still blocks the queue when its card write fails

Every reader of a queued card goes through one derived view of the journal's dispatch facts,
so a card whose message was refused reads as returned even when its card-table update and heal
both failed. One decision and one writer store the derived state for every action.

* refactor(native-chat): keep each queued message in the chat's journal, not a second table

A queued message lived in its own SQL table, and a hook that may be skipped copied
each refusal from the journal into it. When that write failed, a refused card
vanished: the cards behind it sent, and its Send and Delete did nothing.

Each queued message is now a `queue` row in the chat's journal beside the
submission that hands it off and the dispatch row that answers it. The reducer
folds the card from those rows, so a refusal returns the card and holds the
cards behind it with nothing written, and replay reads the same. Every action
writes its own rows in one transaction. Cards an earlier build stored are read
through the same classification until they move: each action moves its own
card, and a bulk import runs once per open. A new epoch restates every card
still owed an answer in the transaction that deletes the old rows.

The settlement hook and its savepoint, the open-time heal, the drain's heal and
the derived-card view are gone. The published card list is unchanged.

* test(native-chat): adapt the queue tests to cards kept in the journal

Cards are written through the journal, so tests queue them with their body
alone and hand them off under their real fingerprint. Tests of the removed
settlement hook, heal and SQL store are deleted; the text-update spec now
covers the edit decision alone, and a test of an earlier build's stored holds
seeds the legacy table and reads the imported cards. Rolling over an epoch now
keeps a card whose hand-off was never handed over, so that test checks the
retried id never sends rather than that nothing sends.

* test(native-chat): a refused card holds the queue through rewind, a new epoch and a restart

One whole-flow test through the host: the refusal writes no card row, a rewind
that retires the refused hand-off and a rolled-over epoch keep the card
returned, a restart and Resume send nothing behind it, its own Send refused
again still holds the queue, and its Delete releases the next card in order.

* test(native-chat): an earlier build's queued cards move into the journal once, each read the same

One mixed-import test through the host. The first open's import fails and
moves nothing, yet every card reads as it will; Delete and Send each move only
their own card. The next open moves the rest in one transaction, writing a
refusal only where its hand-off is gone, nothing for answered cards, and leaving
an unreadable row on disk. A later command turn spends a moved command card, a
third open writes nothing, an old Delete replays from its receipt, and a chat
with only answered legacy rows clears them in a delete-only transaction.

* test(native-chat): adapt typechecked tests to cards kept in the journal

The row-kind table lists a queue row, the clear and legacy-import tests
match the current signatures, and the downgrade test keeps only the
leftover-send case: a queued card is now a newer-version journal row an
older build keeps read-only, and users don't downgrade.

* refactor(native-chat): drop the journal row writer's unused transaction hook

No caller passes one since the queue's savepoint bookkeeping left.

* test(native-chat): downgrade probes write rows at the version older builds read

The highest version this build reads is now 4, the queue row's. A newer
build's row in these probes is written at version 3 so the older build
meets its kind, not a newer version, and the Stop-event probe hands the
older build the rows as stored, since a read raises each row's version.

* test(native-chat): unit tests follow the queue row's version 4

The legacy-import test opens real SQLite, so it runs in the Node runtime
project. The newest version this build reads is now 4, and a read raises a
row's version to it, so the turn-lifecycle test no longer pins the
in-memory version (the written one stays pinned by journalRowSchemaVersion).

* fix(native-chat): one batch echoing two refused queued messages spends both

The per-row delivery check returned only the first waiting card an echo
proved delivered, so when one settlement carried echoes for two cards the
second stayed waiting and was sent again. It now returns every card the
row proves delivered, and the fold withdraws each; replay folds the same.

* fix(native-chat): a rewind moves an earlier build's cards into the journal first

While the open's import had failed, an earlier build's card that read as
refused through its hand-off lost that reading when an in-place rewind
retired the hand-off: it disappeared and the card behind it sent. The
rewind now adopts every legacy card ahead of its tombstones and deletes
their rows in its own transaction, as an epoch replacement already does,
so the fold latches each card's reading before its hand-off is gone.

* fix(native-chat): keep an earlier build's card whose hand-off is still unconfirmed

The import dropped a legacy card handed off as a send whose delivery was
unknown, though the fold keeps such a card linked and a later refusal
returns it. The import now keeps every card whose hand-off a later row can
still answer, through the same check the reducer folds dispatch rows by.

* fix(native-chat): one delivery echo withdraws at most one queued card

One provider echo proves one delivery, yet it withdrew every waiting card
with the same text whose hand-off was refused, so a second identical card
was lost. The fold now pairs each echo with one card: on its own row the
earliest-positioned match no other echo of that row took, and for a card
that waits again the earliest applied echo no card has taken. Paired echoes
live in the fold's state, so live and replay pair the same; readers of
cards not yet imported work on a copy.

* fix(native-chat): a queued message handed to the agent is sent again only on proof it never got it

Pressing Stop after Orca handed a queued or steered message to the agent,
before the agent confirmed it, put the card back in the queue, and Resume
or the next send sent it again. A check matching the agent's echo of the
text was the only guard against a double send, and it kept trading a
resend for a lost card. Whether the card waited again was decided by the
rejection's category, not by proof the agent never got the message.

A refused hand-off now waits again only where Orca proves the agent never
received it: the host still held it, or whoever settled it recorded that.
A failure still returns the card and holds the cards behind it. Anything
else may have reached the agent, so the card is spent and its message
reads as sent, derived on every fold from the stored rejection: with the
turn it was answered into, as that turn's unconfirmed message where it was
handed over; withdrawn before any turn opened, as a person's own send a
Stop took back. The reopen mark ignores a recovered doubt, and the restart
reconciler no longer judges a queued hand-off unsent from what history
lacks. The echo text matching and its pairing are deleted.

A close or quit while the agent starts settles sends with the unrun hold.
The host hands nothing to a child before its start answers
(structured-agent-session-delivery-loop.ts: a handover needs phase
ready), so such a send went to an earlier child and may have run: it now
reads as sent instead of waiting again.

* fix(native-chat): Claude's report that it removed a queued message unrun lets the card wait again

Claude says when it removed a send without running it: the interrupt
receipt's cancelled list, or a command_lifecycle cancelled, discarded or
refused frame for a command that never started. Every one settles through
rejectClaudeDispatchWaiters, which now marks the settlement unrun. For a
queued card's hand-off the host records that as proof the agent never got
it, so the card waits again under the queue's pause. A direct send keeps
the plain refusal: there the proof would owe it as stopped history.

* fix(native-chat): an earlier build's card stopped after hand-over follows the same rule on upgrade

Earlier builds put a card back to waiting after any refusal without a
verdict and cleared its link, so a card the agent may already have would
be sent again after the upgrade. The card's own hand-offs in the journal
decide now: unchanged and last sent as one that may have reached the
agent, it is spent on that hand-off and its message reads as sent; edited
or sent again since, it is held for its own Send under a host-only marker
that publishes as a plain hold; with no hand-off in doubt, including one
that never left Orca, it waits as before. The import writes a card spent
on a hand-off in doubt, so its message shows.

* fix(native-chat): read a queued row's hand-off before the lookup that uses it

The lookup's callback lost the narrowing of the row's change, which the
mobile test typecheck rejects.

* test(native-chat): a card steered into a turn a Stop ends stays sent in the Stop tests

Three tests pinned the old rule, that a card handed into a running turn
comes back to waiting when a Stop ends it. Under the new rule nothing
proves the agent never got it, so the card is spent: with the turn named,
its message reads as that turn's unconfirmed one; without, as a send the
Stop took back. Neither is ever sent again.

* fix(native-chat): a queued message stopped before Orca handed it to the agent waits again

A Stop that aborts a hand-off after its row is written but before the agent call now records
the withdrawal as never reaching the agent, for queued hand-offs only, so plan D's rule returns
the card to waiting under the Stop's pause instead of spending it. Direct sends keep the plain
withdrawal (the flag would owe them as stopped history). The agent's own refusal while aborted
is kept rather than overwritten, and an abort thrown during the call stays in doubt.

* fix(native-chat): a queued message stopped while the agent prepares it waits again

The abort signal reaches only an adapter's preparation (its options, the previous turn opening),
never its write, so the signal's own reason thrown back is the adapter saying nothing was
written. The handover now reads that, for queued hand-offs only, as never reaching the agent,
so the card waits again under the Stop's pause. Claude no longer wraps that abort as an option
refusal, which had returned a stopped card as a refused one blocking the queue. A real refusal
keeps its cause and its barrier; any other throw stays in doubt and is never resent.

* test(native-chat): the usage-limit Continue backlog case writes its card through the journal queue

Main's #30396 wrote its backlog card with the stored fingerprint and host-instance stamp the
journal queue derives instead; the card the Continue must yield to is the same waiting card.

* test(claude): the real-CLI queued Stop expects the unrun mark Claude's cancelled-unrun receipt carries

* fix(native-chat): retain spent legacy messages when import fails
2026-10-23 00:53:44 -07:00
Neil b342d29cc1 test: drop obsolete streamparser dependency (#31835) 2026-10-21 22:19:46 -07:00
99d18f7a2f Browser, chat and simulator tabs main-first: the window asks main for each change and draws only main's update (dark, layout-owner build only) (#31516)
* The window shows each layout records frame record by record, without reading its session back (dark, layout-owner build only)

A records frame no longer re-serializes the window's whole session and runs the
Loader. Panes' terminal bindings and live titles apply alone, to the tabs the
frame names; any other record builds only its own workspace's slices, with the
view and facts read from the store. Only a workspace the store does not show
from frames yet (startup, a reopened stream, a first visit, one sent whole)
takes the full build.

Tests install an apply check that, after every flush, compares the store with
the full build for the same cache and pre-flush store; the shared layout-apply
transport now sends main's records frames and fails a flush that differs.

* A frame of only tab rows shows without reading the window's session back or running the Loader (test, layout-owner build only)

* The account-restart test's fake binding runtime carries per-pane binding generations, so it passes with the switch on (test only)

* The account-restart test types its store writes, so its five old e2e type errors are gone (test only)

* A workspace build reads the editor selection and groups through the save's own writers, as the full build does (dark, layout-owner build only)

* A tab released from the window's hold shows the binding main wrote meanwhile (test, layout-owner build only)

* The frame-apply check still checks a flush's other workspaces when it builds one in full, and the shared transport keeps a test's own check (test only)

* The frame-apply check is proven to catch a mismatch beside a full build, and the shared transport restores only the check it installed (test only)

* wip(d3a-2): a windowless host lists a file main opens on session.tabs at once

* wip(d3a-2): the record key's fields are declared where the key is made

* wip(d3a-2): main publishes an editor tab's view, and a window rebuilds the open file it lacks from it

Main's in-memory editor view now carries the file facts an open file needs; it rides the tab row
(PublishedTab.editorView) and a frame build rebuilds a diff, conflict, preview or check-details tab.
Inert with the switch off: views are set only by layout.apply (switch builds) and read only in frame-sync.

* wip(d3a-2): a switch-on window sends editor opens, closes and promotions to main and draws the tabs from its frames

Layout-owner build only; the switch-off path is unchanged. An open proposes the tab id and sends
openEditorTab with the edit file's record or the tab's view (a reopen also its index); the open
file is written at once and the tab is selected once its frame draws it. Callers that act on the
tab (focus request, link reveal, group focus) wait for that frame through layout-owner-pending-tabs.
A close sends closeTabs; the tab goes with the frame and the file's content after it, and a
refusal keeps both. A promotion sends promotePreviewTab and the frame clears the flag. Deleted:
the drawn editor senders, their before/after captures, the editor drawn-tab holds and the
window-record keep rule (a transient tab is kept by its frame view).

* wip(d3a-2): a switch-on close keeps its selection and a reopen, a link names its group, and main stamps an open's time

Layout-owner build only. After main's frame drops a closed editor tab, the window selects the
group's previous tab and its surface through activateTab and focusGroup, after closeFile's
content cleanup rather than before it; a reopen sent while the close was in flight keeps the
file. openFileForLink's proposed view names the group the open asked for. The window's
openEditorTab sends no createdAt: main stamps the tab's time (ledger L186). Preview promotion
writes no tab itself: the window ends a preview only where a paired server owns the layout.

* wip(d3a-2): editor tests assert switch-on tabs through main's command and frame (test only)

Each store and hook test that assumed an immediate editor-tab draw now runs with main's model
through the T1 transport in apply mode and its frames (tabs-drawn-from-main.test-fixture), and
asserts the tab, its selection and its content after main's frame. The fixture fills the frame
slices a partial editor-and-tabs store lacks.

* wip(d3a-2): main takes a restored move as a command, renames a view tab's path, and keeps one tab per file per group

Layout-owner build only (the switch gates every change; shipped builds unchanged). moveTabToWorkspace no
longer skips a tab "the window's save got there first" (L160): window saves no longer carry the move, so
a repeat is tab_not_found; tab_exists and invalid_params stay. A move or rename may carry a non-plain
tab's new view (its file under the new path or owner), which replaces editorViews[tab] (L180). Main
refuses a moveTab or moveTabToWorkspace that would put a second tab for a file into a group (tab_exists),
and its loader folds a saved session's duplicate into the first, as the window's restore does
(dedupeEditorTabsWithinGroups): the window's frame dedupe hid the second, which no close could reach.

* wip(d3a-2): a switch-on restored move and disk rename are main's commands, and the frame that shows them rekeys the content

Layout-owner build only; the switch-off path is unchanged. A restored editor tab re-owned by its
file's workspace (L157, L158) sends moveTabToWorkspace (a re-own in place sends renameEditorFiles)
under the ids and group the window proposes, with each view tab's new view; a disk rename (L180)
sends renameEditorFiles per workspace and draws nothing first. The window's content rekey (open
file records, drafts, buffers, the editorBuffers echo gate, pendingLiveDiskVerification and
pendingSelfMoveEcho) waits in layout-owner-content-rekeys and the frame flush that first shows main's
change applies it, in the same store update as the frame (a flush now writes one patch). A change no
tab shows is rekeyed on main's answer; a refusal drops it and changes nothing. The move coordinator
and the restored-owner loader wait for that outcome; a refused rename rolls the disk move back.
A rename picks a runtime-owned or floating file's owned id, as main's frames restore it.

Deleted: recordRestoredEditorMoveInLayoutOwner, recordRenamedEditorFilesInLayoutOwner, the
reparent's before/after capture and the drawn-tab holds for moves and renames. Tests that assumed
the window moves or renames first now run through main's T1 transport in apply mode and its frames.

* wip(d3a-2): the static gate's casts and layout-writer ratchet hold for the switch-on move, rename and gap fixes

Layout-owner build only. The content rekey's pure store change stays in rekey-open-files-action
(its writer count drops 4 -> 2, pruned); a twin tab main routes to a paired server ends its
preview through patchTab, the existing tab writer, instead of a new one; a frame-only editor tab
takes its label through the frame build's tab-label facts, read from its view or file record; the
moved file's activation names the moved tab, writing no surface field. Test casts are typed or carry
a SAFETY note.

* wip(d3a-2): the switch-on rename takes the window's pure rekey as an argument, so the two modules do not import each other

Layout-owner build only; no behaviour change. Clears the static gate's no-cycle finding.

* wip(d3b): a switch-on window sends browser, chat and simulator opens, closes and page changes to main and draws them from its frames

Main places the chats its host lists (twin h), a local cancel keeps no tombstone (L155), and the
listing no longer writes chat tab structure where frames draw it (L153).

Co-Authored-By: Claude <noreply@anthropic.com>

* wip(d3b): a switch-on browser, chat or simulator close waits for main's frame, lands an emptied workspace as today, and its tests read main's frames

* wip(d3b): the stale-listing and cross-version chat tests check a switch-on local cancel through main, with a presence precondition and per-test resets

* wip(d3b): main places a tab whose group it lacks in the workspace's first group, not beside the window's anchor (L150)

* wip(d3b): tests for main's chat tab commands from a host's chat changes

* wip(d3b): main's windowless chat tab and chat fork tests apply a switch-on host's chat tab through main's layout

* wip(d3b): the switch-on close and open keep their layout writes in the files the writer ratchet counts them in, and a drawn browser close carries its position instead of a rebuilt layout

* Editor tabs main-first: the window sends each editor action to main and draws editor tabs only from main's frames (dark, layout-owner build only)

With ORCA_LAYOUT_OWNER_IS_MAIN on, editor open (preview replace, open to the side), reopen at
its position, close, close-all, promote, the resolved conflict placeholder, a restored-editor
move and a disk rename are layout commands the window sends before it draws anything. Window
content (open file records, drafts, buffers, the autosave gate) is written at once for an open,
and rekeyed in the same store update as the frame for a move or rename. Main publishes each
non-edit tab's view, keeps views in memory across applies and partition changes, stamps the
tab's time, folds same-file duplicates, and republishes a windowless host's session.tabs after
its own file open. The switch-off path is unchanged.

* wip(d3b): a switch-on close main answers workspace_not_found drops the tab's content as tab_not_found does, and placement reads a missing active-group map safely

* wip(d3b): switch-on browser and chat tests open and close through main's command and draw from its frame (test only)

* wip(d3b): a switch-on chat listing writes no tab structure of a workspace main's frames draw; a host chat title changes only the drawn tab's label

* wip(d3b): main's phone rows show a diff tab and a markdown preview as the window's do, with the preview's source named by the window's open-file id

* wip(d3b): the cleanup close's page-read guard counts its named linear passes, so a per-entry rescan still fails switch-on (test only)

* wip(d3b): the static gate's compat entry, focus-read ratchet and formatting hold for the switch-on test conversions and phone rows

* test(d3a-2): the buffer view index follows a rename main answers

* wip(d3b): placement reads the active-group map the real store always holds; the browser test store seeds it

* wip(d3b): a switch-on background browser open leaves the worktree's remembered browser tab unset, as with the switch off

* wip(d3b): a switch-on chat open selects the chat in its group without moving the focused group, as the window's own open does; its test counts the chats main draws

* wip(d3b): the converted simulator, launch-cancel, cleanup-close and chat-listing tests assert what the window's draws asserted, through main's command and frame (test only)

* wip(d3b): main fences a chat's create on its close even when the close fails for want of the chat host, so a cancelled launch never shows

* wip(d3b): a frame keeps the window's hidden chat launch slot after its neighbour, so a resumed chat keeps its saved place through main's frames

* wip(d3b): a declined chat's terminal is placed after the chat and pinned as it is in one main move, and shown before main closes the chat

* wip(d3b): a switch-on chat close the host refuses asks main to open the chat again, as the window's re-read shows it with the switch off

* wip(d3b): a paired server's workspace, its row naming the server, opens and closes browser and chat tabs without main's layout (test only)

* wip(d3b): a refused chat close comes back only while this machine's host still lists the chat, and a close main answers workspace_not_found removes the window-only tab

* wip(d3b): the twin chat close test names what each build stops: no host off, main's host on (test only)

* wip(d3b): the refused-close reopen takes only a chat agent it can launch

* wip(d3b): a frame's browser selection keeps an unchosen workspace's unset through the restore projection's own rule, not a second write

* wip(d3b): the simulator and paired-server tests type their mock stores without assertions (test only)

* wip(d3b): the switch-off declined-chat placement runs as before, with no wait; only the switch-on move waits for main

* chore(d3b): drop the frame-binding helper the main merge left without a caller

* chore(d3b): drop the import the deleted helper used

* wip(d3b): a chat this machine's window creates is placed only by the window's own open, so a reserved chat slot keeps its place (switch-on)

* wip(d3b): main reads a chat tab change against its layout when its queue reaches it, through the one rule the switch-off save uses, so a close right after an open closes it

* wip(d3b): a declined chat's terminal moves to its index among the tabs main draws, not past a hidden launch slot, so it still replaces the chat (switch-on)

* wip(d3b): main's phone-row comments say which rows the window's own row wins, and a test shows a window-listed diff keeps its dirty state (comments and test only)

* wip(d3b): a switch-on chat open returns only the tab's id and chat, the fields main's frame keeps; no caller reads more

* chore(d3b): the unit-routing census registers the browser suites' drawn-from-main store fixture as a support bridge (test only)

* chore(d3b): the chat tab rule's comment wraps and names its two callers (comment only)

* wip(d3b): main's chat tab queue asks a worktree's host only once it has a saved session, and one failed change no longer stops the ones after it (switch-on)

* wip(d3b): a chat tab batch whose changes cannot be read is logged, and the batches after it still land (switch-on)

* test(d3b): a CLI-chat resume's tab is opened once, by the window through main, after the tab before its hidden slot (test only)

* test(d3b): an SSH worktree has no host to run a chat on, so none of the chat open, close or fix paths reach it (test only)

* chore(d3b): the content tab placement reads folder workspaces through main's workspace-record selector, as the new ratchet requires

* fix(merge): put back main's f47f632 files that the merge took from a later main by mistake (no D3b change)

---------

Co-authored-by: E2E Test <e2e@test.local>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-21 22:32:07 -04:00
Brennan Benson d085427628 Keep model choices coherent across Claude updates (#30107)
* Use one model catalog per execution scope

* Accept a missing session record in host scope

* Merge duplicate launch-resolution import

* Fix catalog typecheck errors

* Align catalog tests with the one-list design

* Type the integration fixture's send without a cast

* Give the discovery test complete runtime dependencies

* Fix model catalog completion and scoped configured display

* Preserve model catalog authority across released clients

* Fix mobile first-frame catalog fixture narrowing

* Keep startup catalog persistence with one completion owner

* fix: preserve chat selections across CLI model updates

A newer CLI can omit a model that an existing chat still runs. Keep that chat's selected model and effort instead of replacing them from the available list.

Discover one model list per host, account, launch command and folder, refreshing after two minutes. Keep current values and the running model's exact effort choices with its chat. Show only the known effort when Claude supplies no matching menu.

Preserve older paired-client delivery and configured launch arguments. Make Codex use independent discovery and retain the established ACP and Pi paths.

Tracked in STA-10947.

* fix: keep the Pi options read within its file limit after main's growth

Tracked in STA-10947.

* fix: import the option snapshot's shared module once

Tracked in STA-10947.

* fix: keep Grok and Pi model menus and fast controls across restarts

Restore the established ACP and Pi catalog route: newer listed models and qualified startup defaults stay visible, and genuine retained effort menus survive later reads. Keep exact-child Fast and Speed controls. Update the phone option contracts and the remaining stale expectations to the selection rules.

Tracked in STA-10947.

* fix: share each workspace's model list and keep live Grok and Pi menus

New chats read their workspace's own model list through the one shared request for that workspace, so a second chat in the same folder starts on the model and effort the agent actually runs there, and a slow first answer can no longer overwrite a newer list. A running Grok, OpenCode or Pi chat keeps its live model menu when an older shared list arrives later. Update the tests that still expected the replaced behavior.

Tracked in STA-10947.

* fix: keep separate agent and chat model lists for Grok, OpenCode and Pi

A brand-new Claude chat now offers the selected model's effort levels before the first message, as it did before. Grok, OpenCode, omp and Pi again keep the list read from the installed agent apart from the list a running chat reports, and each newer list replaces only its own source. So a chat shows the model the agent is actually running with that model's own effort menu, and an effort change can no longer act on an older model's menu. Saved model lists on WSL are readable again without starting the agent.

Tracked in STA-10947.

* fix: keep desktop catalog storage out of the phone's type graph and show Grok's late model answer

The phone app's type check no longer pulls in desktop-only catalog storage. The model-list code no longer reaches the saved-catalog file through a type import. When a Grok model change times out and Grok later confirms the model, the chat shows the model Grok actually runs.

Tracked in STA-10947.

* Remove unrelated merge formatter and package manager churn

* Clarify independent catalog ownership and refresh contracts

* Declare picker demand callback as an effect dependency

* Complete mobile catalog surface and catalog merge types

* Complete catalog contract fixtures for current host types

* fix: preserve catalog source contracts and account fallback

* test: inject independent Codex discovery transport

* test: remove accidental client fixture assertion

* test: qualify the saved account fallback read

* fix(chat): preserve catalog evidence and current child controls

* Fix catalog CI types and mobile option readiness

* Reconcile session controls after inventory changes

* Fix catalog import and deferred test resolver types

* Keep model capability observations separate from picker views

* Use installed model choices for chat options

* Fix model catalog test fixture contracts

* Restore model option compatibility and stable settlement

* Return a catalog reply in provisional launch fixture

* Compose unreported live model defaults in the host

* Preserve explicit replacement model on recovery send

* fix(chat): use stable scoped catalogs on local desktop

* test(chat): preserve generic catalog runner type

* fix: scope catalogs to configured environment and keep saved reads passive

* fix: satisfy catalog regression and controller CI checks
2026-10-21 16:42:27 -07:00
Kelvin Amoaba 22154de422 feat(mobile): tab strip shows terminal and chat agent state, reveals the next tab, renames on double-tap (#31372) 2026-10-21 18:27:30 +00:00
Brennan Benson 5c666d2974 fix(native-chat): retry a failed chat name, show the failure on the tab's hover card, and add Regenerate name (#30646)
* fix(native-chat): name a chat from its first message at once; the generated name replaces it

A native chat's one stored name was written only by a best-effort, one-shot
generation, so any failure left every surface on the 'Codex Chat' placeholder.
The first prompt now names the chat when its first turn starts, and the
generated name replaces it only while the field still holds that seed.

* fix(native-chat): the chat names setting says a chat shows its first message until it is named

* fix(native-chat): retry a failed first chat name twice before keeping the first message

A failed name generation was final for the chat's lifetime. The first name now
gets two retries, after 2 and 4 seconds, and stops early once the
first-message name was replaced.

* fix(native-chat): show a failed chat name on the tab hover card, with Regenerate name

A chat whose name generation failed after every retry gave no sign of it, and
nothing could run it again. The host now keeps the last failure in memory and
publishes it with the chat's status while the chat still has the name it failed
to replace; the tab hover card shows it with the error dialog, and a new
agentSession.regenerateName (behind a capability) makes one more try.

* fix: derive chat naming progress from host jobs

* fix(native-chat): merge duplicate import CI caught

* fix(native-chat): declare agentSession.regenerateName in the cross-version surface manifest

* fix(native-chat): stub naming-failure hook in SortableTab rename shortcut test
2026-10-20 18:34:16 -07:00
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 6c7a6ddd54 fix(native-chat): save every chat reply and tool output whole; bound only the copy sent to clients (#29982)
* fix(native-chat): save every chat reply and tool output whole; bound only the copy sent to clients

Orca cut Codex replies, every agent's reasoning, plans, prompts and all tool
output to a 16 KB head when it saved them, and threw the rest away. The
limit existed so a reconnecting client is not replayed a huge row, but it
was applied at save time, so every transport limit became permanent data
loss.

The journal now keeps what the agent said. One outbound form, applied to
every history page, live batch and catch-up, sends a client the same 16 KB
head of each tool output and diff it always got, and marks the item
`shortened`. A new capability-gated `agentSession.item` RPC returns the
whole item, and the desktop and phone show "Show full output" / "Show full
message" when the copy was cut. Items past half a page are shortened and
marked instead of replaced with "too large to show".

Two bounds stay, both named: a 4 MiB ceiling per text (head and tail),
applied where every provider's rows enter the write queue so one write can
never stall a chat, and a preview on Grok/OpenCode/OMP/Pi calls while they
run, whose closing row is built from the whole value the translator held.

* test(native-chat): an over-budget history item is shortened and marked, not replaced

* fix(native-chat): format, keep the sink and projection under the line limit, update method count and tests

* fix(chat): preserve complete writes and keep detail current

* test(chat): align final tail reservation with captured content

* Prototype source-batch journal ownership for Claude

* Pin the resolved iterator promise in ownership experiment

* Revert "Pin the resolved iterator promise in ownership experiment"

This reverts commit bdb49ab738.

* Revert "Prototype source-batch journal ownership for Claude"

This reverts commit e5ed387f0c.

* Preserve large received chat content while pacing input

* Verify received chat writes wait for journal persistence

* Close test journal databases before removing temporary roots

* Exercise item detail in the cross-version chat wire manifest

* Pin complete saved output and fitted client publication

* Exercise Pi exit retention with real journal write pressure

* Settle terminal web test unmount before DOM teardown

* Reuse existing readers and contract support in session tests

* Reuse measured chat item sizes in history pages

* fix(chat): derive page form type from reader contract

* fix(chat): open phone user messages while loading full text

* test(native-chat): reuse imperative context provider construction

* test(native-chat): format full-content disclosure fixture

* ci: preserve unit shard diagnostics after a timeout

* ci: retain unit shard output when tests time out

* ci: expose unit shard watchdog and output stages

* Revert "ci: expose unit shard watchdog and output stages"

This reverts commit 0b4991b144.

* Revert "ci: retain unit shard output when tests time out"

This reverts commit 87eec4c04f.

* Revert "ci: preserve unit shard diagnostics after a timeout"

This reverts commit 1888c656bb.

* test: verify complete Claude tool output without large failure diffs

* Simplify reply extraction in cross-version session test

* Restore full message recovery for grouped phone thoughts

* Target recovery button in grouped thought fixture

* test(native-chat): count agentSession.item alongside main's new structured method

* fix(chat): reuse strict session subscription schema

* test(chat): open Codex before checking received content
2026-10-20 11:13:48 -07:00
NeilandClaude 11b925d5a1 fix(ssh): a legacy relay terminal stays with the relay that runs it when another relay lists the same id (#30870)
* fix(ssh): a legacy relay pane stays with the relay that runs it when another lists the same id

Co-authored-by: Claude <noreply@anthropic.com>

* fix(ssh): keep an exit reported before a pane's owner is known, and re-list a contested pane

Co-authored-by: Claude <noreply@anthropic.com>

* fix(ssh): a legacy relay id's owner survives reconnects, is adopted only once every relay answered, and a contested id is never served

Co-authored-by: Claude <noreply@anthropic.com>

* fix(ssh): pin a legacy relay id's owner only on a listing that accounts for every relay

Co-authored-by: Claude <noreply@anthropic.com>

* test(ssh): the cross-version bridge test passes the target's owner ledger

Co-authored-by: Claude <noreply@anthropic.com>

* fix(ssh): an epoch-scoped relay id keeps its sole lister as owner on an incomplete listing

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 19:13:05 -07:00
Brennan Benson 53191250e8 Accept messages while any agent starts, and bound each startup step (#28046)
* Finish structured agent readiness before delivering held work

* Publish the Codex process before protocol startup

* Publish ACP agent processes before session startup

* Publish Pi at spawn and settle signed-out starts once

* Adapt permission fixtures to startup lifecycle

* Keep model discovery for chats before their first start

* Use one host readiness promise for provider operations

* Await provider opening in the execution host

* Await Claude initialization in host-owned open

* Observe Claude readiness through the host status

* Keep optional readback behind host readiness

* Recheck launch permission after host readiness

* Preserve the awaited child during permission revalidation

* Await Codex protocol open under host ownership

* Await ACP protocol open in the host

* Derive ACP opening from its protocol session facts

* Exercise unanswered ACP options with an explicit silent route

* Await Pi open and fail signed-out construction

* Read Pi catalog facts from awaited opening

* Keep the Pi fixture exit observer

* Record failed owned opening before the first message

* Abort a matching provider open when its child exits

* Cover provider exit during an unanswered host open

* Cover explicit failure facts on a host-stopped child

* Capture message choices per accepted send and preserve picker state

* Apply accepted Claude message choices at prompt handover

* Apply captured message choices at provider dispatch

* Bind native chat choices to accepted message dispatch

* Reset Codex turn choices for each captured message

* Resolve Codex message defaults from native facts

* Align message choice and Pi default fixtures with provider dispatch

* Align option fixtures with saved message choices

* Match Codex message choices to the active turn origin

* fix: let pending commands reach handover refusal

* Abort pre-prompt message choice writes with host-owned wait

* Verify message choices through provider input boundaries

* Keep legacy mobile test hosts explicit about message choices

* Keep startup waits cancellable and preserve operation order

* Derive Codex startup exits from host phase

* Open Codex protocol in background-task test fixtures

* Keep ACP startup ownership until handshake completes

* Run ACP opening catalog fixtures with Node SQLite

* Verify saved ACP picks without live option writes

* Keep ACP catalog bookkeeping from failing an options read

* Restore signed-out restart and fail Pi startup faults

* Run Pi opening catalog fixtures with Node SQLite

* Preserve message choices across cold clients and kept cards

* Remove the unused provider option restore ledger

* Clarify launch choices and legacy option hydration

* Remove remaining restore-ledger forwarding and assertions

* Remove unused ACP and Pi restore bookkeeping

* Keep live choices and delivery order while agents start

* Drop retired startup revision from Codex opening

* Keep live-option cancellation until message choices and clarify catalog reads

* Hold ACP option replies in live-write cancellation regressions

* Remove retired Pi startup revision and match saved-pick behavior

* Check running choices and retained rewind messages after delivery integration

* Apply startup picks and let parked messages release operations [skip ci]

* Remove unused Codex opening report field [skip ci]

* Keep ACP restoration facts local to the provider [skip ci]

* Apply Pi startup picks after readiness and remove unused open facts [skip ci]

* Cover goal changes during the held Codex message wait [skip ci]

* Save a waiting startup pick after Stop ends its opening [skip ci]

* Save choices when startup ends or a replacement is still opening

* Check saved choices separately from Claude startup reports

* Release scripted Codex startup before awaiting queued picks

* Check the saved picker selection in per-message choice fixtures

* Exercise current choices and eager client capabilities in runtime fixtures

* Reuse runtime fixture reads and type the fake connection directly

* Align startup fixtures and preserve observed Claude exits

* Honor queued send authorization in Claude runtime fixtures

* Open Codex protocol sessions before Stop fixture dispatch

* Wait for the opened Codex child before runtime notifications

* Keep repeated permission picks usable and align Codex fixtures

* Validate pending picks against the live option state

* Order startup picks before later messages and retain flow regressions

* Keep reviewed lifecycle contracts while pruning duplicate tests

* Save picks after a proven exit without requiring a child

* Type the startup reply fixture against the Claude control contract

* Keep Codex startup flow contracts and remove overlapping adapter tests

* Restore the committed dependency lockfile after test tooling

* Keep startup recovery flows and remove duplicate Pi handshake cases

* Cover both Pi startup choices in the delivery flow

* Match the Pi prompt entry recorded by the scripted wire

* Honor the before-input callback in the startup reply fixture

* Keep message choice flows and remove overlapping selector tests

* Keep startup option menus independent and show the saved choice

* Exercise startup picks across real menus and the runtime

* Match startup picker fixture to saved message choices

* Keep saved effort visible when picking a model during startup

* Include the running-turn scope in repeated Stop fixture

* Use the current attach contract in import fixtures

* Keep the Codex journal translator within its line limit

* Match ACP startup fixtures to saved picks and late Stop behavior

* Check startup effort after saved picker choices are acknowledged

* Complete Stop target types and keep the effort regression concise

* Use requested picker values in the effort regression acknowledgement

* Require owned provider spawn and one host-awaited open (from #28055)

* Restore the live checks the startup cleanup's deleted tests pinned

One assertion each, in the surviving suites: a stale-generation exit is
ignored, a failed create lets its sink go, a readback before open is saved,
an idle stop that fails is retried, a no-pick Pi start teaches its default,
an older client's pre-spawn failure and packed answer, an undrivable chat
stays readable, the resting slash menu stays on its own host, the main agent
reads apart from subagents, restore publishes status, naming runs off the
host's status feed, and a throwing logger never reaches the operation.

Also corrects the reliability gate note that claimed the send-idempotency
suite replaced the refusal-retry tests.

* Read a rewind in doubt outside the chat's serialize and apply it in the commit

The started settle and the attach's ready-child path read the provider's
history first, then apply what it proves in a short commit that re-checks
the rewind it read.

* Accept messages while the agent starts: run the start outside the chat's serialize

The chat's serialize was both where a send was admitted and where its agent
started, so every slow step of a start (closing-child join, lease
reconcile, recovery probes, resolvers, history read, spawn, sink drain,
provider dispatch) held every later send.

A start now registers in one short step before its first wait: its abort
controller (which Stop, close and quit already reach outside the lock),
its startup clock (the 10-minute ceiling now counts from here) and quit's
drain. Every wait after runs outside the lock, raced with that signal, and
every write is a commit that first re-checks the start is still the
session's. A start that loses the check is late: its spawn is closed
through the post-acquisition failure path and nothing is handed to it.

- The delivery loop's prepare returns a start handle; the loop awaits the
  start outside, then re-prepares. Handover writes the pending row and
  begins the dispatch abort handle in one commit, calls the provider
  outside after re-checking the child, and resolves in a second commit.
- Ready operations claim a start instead of running one in their step.
- A client attach runs the same steps under one serialize, as before.
- An exit reported between spawn and publish is held for the publish
  commit, which ends the child it indexes.
- An unproven close still fails only what was queued when its start was
  asked for.

* Test that a send is accepted at every wait an agent start makes

One whole-flow test holds the start at each wait in turn (closing-child
join, reconcile probe, recovery re-probe, resolver, spawn, sink drain,
open, started recovery, pending row, provider call): a second send is
accepted at once, a Stop is recorded without waiting and nothing reaches
the provider, and what was sent goes to the published child once, in
order. Also quit before spawn, concurrent sends, a lease left recovering,
and an idle sweep racing a send.

* Bound each startup step on its own and drop the startup silence rule

A start no longer ends after 60 s without output: output is not protocol
progress. Each protocol step now has its own bound where its handshake
lives, and one 10-minute ceiling from registration stays as the outer bound.

- Codex: initialize 15 s and thread start/resume on the connection's 30 s
  default again, instead of the startup ceiling.
- ACP: initialize 60 s, authenticate 120 s, session new/load/resume/fork
  300 s; turns stay unbounded and option writes keep their caller's bound.
- Claude: the initialize answer has a 60 s bound; past it the open fails
  with the ordinary startup-failure words and the host ends the child.
- The onOutput plumbing that only fed the silence rule is removed.

* Count Pi ready once it reports its state

Pi's start no longer waits for its model listing and slash commands.
The open returns after get_state and the unknown-provider check; the
listing then arrives as an options report (with the catalog listing the
start resolved) and the commands as a later publish. An empty model list
no longer fails the start as signed out: a signed-out Pi runs the unknown
provider, which still fails it, and a prompt it cannot run is refused
with the sign-in words by the existing prompt path.

get_state keeps the connection's 30 s request bound instead of the
startup ceiling.

* Test that model and effort picked while a chat is created ride its first message

A slow host's create reply arrives after the user picked a model and effort. The picker's view
layers the launch's held picks over its seed and the host's saved selection, and every send
carries what the picker shows, so the first message carries the picks whether the host's status
names its selection or not, and while the last pick is still on its way to the host.

* Start a phone's new empty chat on its first message

The phone's new chat is an agent.launch with no prompt, which always created the chat with its
agent started, while desktop chats start on the first message. The phone now advertises the
create-without-start capability, and a launch with no prompt from a caller advertising it creates
the chat at rest, keyed in the ledger by its create intent as a client's create is. Launches with
a prompt, the CLI (which advertises nothing), orchestration workers and ensure still start at once;
an older host ignores the capability and stays eager.

* Take main's lockfile; the branch changes no dependencies

* Restore main's exact text in files the merge only reformatted

The pre-commit formatter rewrote 37 of main's files staged by the merge, though this branch never changed them, pushing two past the line limit. Committed without the hook because the hook would reformat main's text again; CI lints the result.

* Fit the startup tests to the owned-spawn, host-awaited-open contract

Test doubles gain the mandatory open and an acquisition generation, the
removed started event gives way to the rig's proveStart, and two literal
types are tightened for CI's typecheck. A Stop during a held start writes
no host note on this branch: the taken-back send draws its own
stopped-before-start row, which the startup-attempt test now asserts.

* Fit the remaining CI checks to the startup limits and current releases

- Run agent-launch-at-rest.test.ts in the SQLite Node runtime project.
- Pin the send-options cross-version case to v1.4.228, the first published release with agent-session.send-options.v1; v1.4.226 was never pushed.
- Load the baseline's isStructuredTab from src/shared (moved in v1.4.228), falling back to the renderer path for older baselines.
- End the never-proven start with a short startup ceiling: the silence limit it relied on is gone, and the ceiling counts from registration.

* Read the option stamp from the chat's one host-execution selection

* fix: preserve startup authority and accepted agent choices

* Retain dispatch authority and reconcile accepted options

* fix(native-chat): preserve accepted choices and cancellation authority

* Restore established client selection flow

* Preserve provider output observation and typed startup fixtures

* fix: preserve initial Claude launch choices and effort readback

* test: complete Claude catalog session fixture

* Preserve incoming main files after merge formatting

* Preserve compact handover through startup merge

* Type instant startup fixtures before their open is supplied

* Keep browser sign-in under its existing expiry owner

* Finish the main merge: retired-model display, start rewind cut, new tests

A model the Claude CLI retired is reported again, so the picker shows what the child runs and the
next send moves the chat off it. The start reads a rewind in doubt against the handle it answered
with, the record its commit then applies to. Main's tests follow the start's open: the admission
fixture holds a real turn, and a sign-in close during setup fails the open.

* Type the start's rewind record as the store returns it

* Type the Sleep test's held open as the provider open it replaces

* Sleep counts a chat stopped once the start it aborted leaves it at rest

A start runs outside the chat's lane, so Sleep's step can run before the start it aborted has
settled; it then re-reads the chat once that start is done, instead of reporting it unverified.

* Keep Sleep's start-abort bookkeeping beside the rest of Sleep's stop
2026-10-19 10:53:46 -07:00
Brennan Benson 7dcb7d4439 Sleep stops a workspace's native chat agents (#28445)
* fix(sidebar): Sleep stops a workspace's native chat agents

Sleeping a workspace stopped terminals and browsers but never its structured (native)
chats, so their claude/codex process kept running, and Sleep was disabled for a
workspace whose only content was a chat.

Sleep now stops each chat's agent through a new agentSession.sleep (the host's close,
keeping the tab), routed to the host recorded on the chat's tab; an older host without
the capability fails Sleep with a clear message. The Sleep menu counts a chat.

* Fix native chat Sleep ownership and delayed teardown

* Stop native chat sessions when sleeping their workspace

* Fix sleep teardown wake and host-qualified receipt races

* Verify mobile Sleep sends the selected desktop host

* Keep worktree Sleep RPC within method file limit

* Use sleep intent as attempt identity

* Gate workspace sleep on structured client support

* fix(native-chat): preserve sleep stop receipts and send timing

* fix(native-chat): keep superseded workspace sleeps quiet

* fix(native-chat): ignore superseded failed sleep stops

* Fix sleep lifetime test resolver types

* Accept workspace Sleep for already ended chats

* Sleep native chats through the shared no-toast sleep core

Main split runSleepWorktrees into sleepWorktreesReportingFailures (no toast) and
a toasting wrapper. Move the native chat stop, host routing and typed failures
into the shared core so the WSL move's Sleep request also stops chats, and
report every workspace not verifiably slept (an unconfirmed chat stop included)
in failedWorktreeIds. Fold one ACP adapter field onto a line so main's file plus
this PR's ownership check stays within the file length limit.

* Import isStructuredTab from its shared module

* fix(native-chat): leave chat rows to their host through Sleep

Sleep's worktree status drop deleted the chat's row, which the host feed then
rewrote, so the outcome depended on which write landed last. Open chat tabs
own their rows (same rule as terminal-tab close), and a chat only makes a
workspace sleepable while its host holds the agent's process.

* fix(native-chat): judge the Sleep receipt by facts, not lane occupancy

A running chat's stop queues the provider's own requested-close end report
onto the chat's lane, so the lane-occupancy check read every proven stop as
superseded and dropped its receipt. Later work now counts only once it
exists: a moved fence, a child, delivery, child work, a pending submission,
a pending create or attach, or an open dispatch.

* test(native-chat): report a fake close's end only for an acquired child

* Preserve canonical workspace identity when sleeping

* Propagate optional sleep host through file command contract

* Move the deliberate-sleep PTY tests to main's spec file

Main grouped the PTY connection suites (#29193) and moved this file to a .spec.ts registered from pty-connection-spawn-and-startup.test.ts. Taking main's copy here keeps the following main merge free of a modify/delete conflict; this PR's test changes are reapplied after the merge.

* Reapply Sleep's teardown waits to the moved PTY spec; keep acquisition under its line limit

The deliberate-sleep spec moved in #29193; this carries the PR's changes into it: a pane mounted after wake waits for issued teardown, an unqualified pane waits for every same-id host's teardown, and a queued startup waits too.

The merge left claude-structured-session-acquisition.ts over 300 lines. resolveClaudeAcquisitionError, which already builds the exit-unproven error, now decides whether to release the acquisition.

* test: keep the Sleep receipt checks on the closed handle and kept record

* fix(sidebar): a single-workspace Sleep failure says workspace, not workspaces

* fix(sidebar): Sleep stops a workspace's chats on every host that has a copy

* test: drop redundant casts from the Sleep flow test state

* fix(sidebar): stop a workspace's chats after its terminals, leaving main's teardown owner check as is

* fix: admit workspace chat stops before teardown waits

* fix: retain browser teardown caller sequencing

* fix: retire failed acquisitions after observed root exit
2026-10-19 00:33:01 -07:00
Brennan Benson 503d919e64 feat(native-chat): + menu shows chat, disabled chat and CLI rows (#29074)
* feat(native-chat): choose which agents open as a chat

Settings → Chat gets a Providers list. Each agent the host can open as a chat
has a switch; off opens that agent in a terminal instead. Only choices the user
makes are saved. Unchosen: Claude and Codex on, others on when their CLI is
installed on this computer.

The renderer's route and the host's launch-mode decision both read one rule, so
the tab menu, shortcuts, CLI, orchestration and relayed phone launches agree.

* fix: keep chat provider settings with the other chat settings types

* test: route input carries this computer's detected agents

* feat(native-chat): the + menu shows chat, disabled chat and CLI rows

With Chat UI on, each agent whose chat provider is on gets a chat row, disabled
with the reason where the workspace's machine can't run that chat (not
installed, SSH, WSL, not on this host). Every other installed agent gets a row
marked (CLI) that opens its terminal. With Chat UI off, or on an older server
that opens no chats, the menu is unchanged.

* fix: new tab menu rows use a launch variant instead of restyling the menu item

* fix: name every route blocker the menu rows map

* fix: menu row subscriptions render on the server too

* feat(native-chat): open a chat agent's CLI from beside its + menu row

A chat row whose CLI is installed on the workspace's machine gets a › beside
it. Its side menu offers '<Agent> (CLI)', which opens that agent's terminal for
this launch only; nothing is saved and the row itself still opens a chat.

* fix: keep new tab agent surfaces authoritative

* fix: handle unknown host support reasons

* fix(chat): resolve provider choices from their owning computer

* fix: include chat launch feedback translations

* test(chat): use checked native inventory fixtures

* chore: retrigger PR checks

* test: pin pending chat row behavior

* chore: retrigger PR checks after cancellation

* Fix launch preparation lifetime and execution host startup

* fix: keep agent menu choices bound to their launch

* Simplify agent menu support reads and preserve explicit surfaces

* Keep focused menu behavior coverage within its budget

* Keep menu host fixtures typed as execution hosts

* Assert notes payload on the current launch action

* Combine agent session launch type imports

* fix(chat): keep unchosen providers on without discovery

* fix(chat): use workspace installation for menu defaults

* test(chat): type provider routing fixtures against store contracts

* fix(tab-bar): keep + menu chat rows clickable while the host answers

Each menu open discarded the host's answers, so every open showed the chat
rows disabled with 'Checking chat support' until the host replied. Answers
now outlive the menu and refresh in the background, rows use this client's
facts until the host answers, and a slow host no longer reads as a refusal.

* test(chat): align shared launch fixture with private settings reader

* fix(tab-bar): keep known + menu refusals and never treat a silent host as one

Closing the menu dropped the workspace's agent list, which changed which agents
the menu asks about and threw away the host's earlier answers, so a reopen showed
a refused chat as clickable. A support read that could not answer was stored as
"No chat on this host", disabling chats the client knows are supported.

The menu now keeps the stored agent list while closed (only fetching waits for
it to open) and only a real host answer relabels a row. The workspace's
detection target is resolved where main already resolves it (QuickLaunchButton
and the tab-bar runtime model) and passed in, so no new focus read is added.
Tab-bar test mocks now stub the QuickLaunchButton exports the menu imports.

* fix: sync runtime English catalog for agent menu

* test(chat): give the structured-session RPC fixture the private settings reader

The host now reads saved chat choices through requireStore().getSettings(); the
shared RPC fixture only had getClientSettings, so the launch-mode reader saw no
settings and create support refused Claude. The fixture now follows each test's
client settings.

* test(chat): pin create support's options argument and kept decline reason

* fix(native-chat): + menu chat rows wait disabled for the host and share its answer across tab groups

A chat the host refuses showed clickable for up to the 10s host read, then flipped to disabled,
once per tab group. Rows now stay quietly disabled until their host answers (or the read times
out), and every tab group on the same workspace host reads the same answers.

* fix(native-chat): + menu rows also wait before the host's chat list loads, and on every reopen of a host that could not answer

Asking the host no longer waits for its chat list, so Claude and Codex no longer show clickable,
then disabled, while it loads. A read that times out releases rows for that open only, so the
next open waits again instead of flipping on a late refusal. Agents not installed skip the read.

* fix(native-chat): typed + menu search puts a launchable row ahead of a disabled chat

In a WSL workspace, typing an agent's name and pressing Enter highlighted its disabled chat row,
so nothing launched where main opened the CLI. Search now ranks rows that can launch first.

* fix: preserve provider choice and cached native inventory

* feat(native-chat): hosts publish each workspace's chat availability as one stream

A new negotiated stream, agentSession.subscribeAvailability, advertised as
agent-session.structured.availability.v1. The host keeps one in-memory resource per
workspace, shared by every reader, that asks the existing createSupport check for every
registered agent in that workspace and publishes only complete generations. It answers
again when a launch setting changes, after an explicit agent refresh, and (for answers
that could not be checked) when a workspace appears; the last reader leaving retires it.
A check that could not answer is unknown, never a refusal. agentSession.agents stays
static and unchanged.

* feat(native-chat): + menus read a publishing host's workspace availability

The renderer holds the workspace's availability stream while its tab bar is mounted
(not while a menu is open), shared by every tab group, with runtime and contact fences.
It exposes a readiness model, { ready: false } | { ready: true; byAgent }, from
useHostStructuredAgentAvailability and the menu hook. On a host advertising the
capability the menu reads its answers from that model and makes no per-agent
createSupport reads; an older host keeps the per-agent reads, answer cache and deadline
unchanged. Row building and presentation are unchanged.

* fix(native-chat): workspace availability checks only what the shown workspace needs

Only the shown worktree (or an open menu) holds its host's availability stream, and only with Chat
UI on. Display checks no longer force a fresh login shell for an agent that isn't installed, so
Claude and Codex answer without waiting on it. A change to the Windows runtime default or a project's
WSL choice re-checks, and a workspace still on its first check answers again when one appears.

* fix: keep main's bytes for the create-support file the merge reformatted

The pre-commit formatter rewrapped main's unformatted lines during the merge and pushed the file past its line limit.

* test: type the + menu rows hook mocks without assertions

* test: match the availability stream's types in three tests

* test(cross-version): declare the workspace availability stream in the agent-session surface

* feat(native-chat): + menu holds a placeholder until its agents are known, and learns them while the workspace is shown

* feat(native-chat): + menu reads one agent health snapshot per machine

The + menu no longer checks chat availability per workspace. Each Orca
runtime keeps one agent health snapshot: every agent's location rule on
this machine plus a typed observation of its installed release, probed
in the server's own home. It is taken on first use and replaced only on
a launch setting change, an explicit agent Refresh (after a fresh shell
capture), or a restart, and kept without readers.

- The version probe reports supported, unsupported or inconclusive; a
  launch still refuses an inconclusive run, while the snapshot reads it
  as unknown rather than a refusal.
- agentSession.subscribeAvailability takes no params and publishes that
  snapshot; a replay that closes the transport releases its reader.
- The renderer keeps the snapshot per host, runtime and pairing, through
  lost contact and across workspace switches on the same host.
- The menu hook drops the per-open createSupport display reads, their
  deadline and answer cache; older hosts show client facts and the
  click's admission, unchanged, states any refusal.

* fix: a failed shell recapture still publishes the agent health snapshot

* fix(native-chat): capture fresh shell facts for every health snapshot

* fix(tab-menu): share settled agent actions across menu surfaces

* fix(tab-menu): preserve loading row line height

* feat(native-chat): the whole chat row opens its CLI submenu

Hovering a chat row in the + menu, or pressing ArrowRight on it, now opens
the "<Agent> (CLI)" submenu to the right instead of needing a separate
chevron button. Clicking the row, or Enter/Space, still starts the chat
and closes the menu; hosts pass closeMenu since a submenu trigger never
closes the menu on its own.

* fix(tab-menu): line the CLI submenu up with its chat row

* feat(native-chat): a chat row's terminal button opens its CLI

Clicking a chat row in the + menu starts a chat. Hovering the row (or
pressing ArrowRight on it) reveals a terminal button at its right edge that
opens the agent in a terminal instead. This replaces the submenu, whose
chevron made the row look like it only opened a submenu. The button's click
bubbles into the row, so the menu closes as for any item and the hosts no
longer need a closeMenu callback.

* fix(native-chat): the chat row's terminal button handles drags, arrows and screen readers

A press on + dragged onto a chat row's terminal button and released there
now opens the CLI: the row reads whether the pointer or focus is on its
button instead of the click target, which a drag-release never sets.
Arrow, Home and End keys pressed on the focused button move between rows
as they do on the row. The row is announced as "Start a <Agent> chat"
instead of including the button's label.

* fix(native-chat): chat rows read "Start a chat with <Agent>"

"Start a OpenCode chat" was ungrammatical for agents whose name starts with a vowel.
2026-10-19 00:27:17 -07:00
Neil 9905da80cc refactor(runtime): import runtime helpers from their owning modules, not re-export shims (#30373)
Removes the re-export lines in 22 src/main/runtime modules (rpc schemas, skill surface, worker
identity, terminal viewport, teardown, git, e2ee crypto and more); importers and test mocks name
the real modules.
2026-10-18 09:28:51 -07:00
Neil 658953dc23 refactor(ssh): group system-ssh transport, ssh config and remote exec into ssh/{system-transport,config,remote-exec}/ (#30355)
git mv plus import, vi.mock and path-literal rewrites; one e2e route regex follows the moved
dynamic-forward process.
2026-10-18 07:26:10 -07:00
Neil 2d1ec53533 refactor(ssh): fence the legacy relay in ssh/legacy-relay/ and drop the relay name from live deploy primitives (#30292)
Moves legacy-relay-bridge/ plus the relay-only census and endpoint files into ssh/legacy-relay/,
so the 2027-01-15 retirement deletes one folder. Live install, exec and framing modules that only
carried the relay name become remote-*. git mv plus import, path-literal, routing and workflow
path-filter rewrites only; no remote file name or path changes.
2026-10-18 03:17:41 -07:00
Neil 2c12c1edf1 refactor(runtime): move runtime/relay to runtime/mobile-cloud-relay (#30274)
The folder is the cloud relay that reaches phones; under its old name it read as the legacy SSH
relay. git mv plus import and path-literal rewrites only.
2026-10-18 01:35:40 -07:00
Neil c359d09f22 chore(deps): remove unused desktop and mobile declarations (#30182)
* chore(deps): remove unused desktop and mobile declarations

* fix(deps): retain parser used by historical-release tests
2026-10-17 22:34:51 -07:00
Brennan Benson 216e3e42a6 fix(native-chat): a Codex chat is Working only while Codex holds the send (#29922)
* fix(native-chat): a Codex chat is Working only while Codex holds the send

Working read 'a send still pending delivery', so any echo or idle signal
that never arrived (a hook-blocked prompt, an answer naming no turn, idle
before a timed-out send was written) kept the chat Working, Stop up, the
queue frozen and /compact refused until the agent exited.

Codex now reports which sends it still holds: unanswered, answered into a
turn that has not ended, or answered into none while a status read after
that answer says the thread runs. Replies and statuses are ordered as they
are read, so a stale status cannot release a newer send. The host reads
that set (plus the short window while it hands a send over) for Working,
publishes a send the child stopped holding as in doubt on every page and
frame so older clients agree, and republishes when the set changes with no
row. The Codex idle latch and its release are removed.

* fix(native-chat): name the handover journal type; reverse without mutating

* test(native-chat): a lost Codex answer while idle is no longer the chat's work; run the held-sends test with the database runtime

* fix(native-chat): bound work updates and own command handovers

* refactor(native-chat): reuse session delivery plumbing

* test(native-chat): allow separate ownership restatements

* test(wire): resolve the release tab predicate after relocation

* test(native-chat): reuse existing promise and empty-batch primitives
2026-10-17 13:14:15 -07:00
Brennan Benson 035588e914 test(cross-version): read isStructuredTab from the shared module the newest release defines it in (#30000) 2026-10-17 00:11:43 -07:00
Brennan Benson 58f1f7de1e fix(mobile): edit queued messages in place (#28646)
* fix(mobile): edit queued messages in place

* fix(mobile): use the platform input font size for queued edits

* fix(mobile): handle missing host support while probing queued edits

* Keep phone queued edits owned through save settlement

* Keep phone queued edits local to mounted chat

* fix(mobile): retire queued editor when its card disappears

* fix(mobile): retain queued edits while reconnecting

* test(mobile): await reconnect timer inside act callback

* fix(mobile): publish queued edit transport after commit

* test(mobile): await queued Edit inside act callbacks

* fix(mobile): hand a stale queued Save to the composer and reopen on the newer text

* fix(mobile): close an unchanged queued Save locally and keep stale typing in its own chat

* fix(mobile): explain disconnected queued edit refusal

* fix(mobile): combine queued session contract imports
2026-10-17 00:06:07 -07:00
Neil 76fc1e5acd fix(web): pair and stay paired from plain-http LAN and Tailscale pages again (#29984)
Partially reverts #27540 and the plain-http part of #27552, which broke web
pairing on upgrade for 1.4.227 users who open Orca at a LAN or Tailscale address:

- The host no longer refuses a first pairing from a plain-http page on another
  machine (insecure_origin, ORCA_ALLOW_INSECURE_WEB_PAIRING).
- Such a page keeps its pairing code in local storage and signs in with it, as
  in 1.4.227, so reloads, new tabs and bookmarks stay paired; no notice.
- Session-only browser keys (pairing.device-key-ephemeral.v1, the idle sweep)
  stay for browsers without Ed25519.
- A web client whose credentials are refused shows the host's reason instead
  of an empty workspace and "Session restore failed".
2026-10-16 22:50:27 -07:00
Brennan Benson dd3bbf680e Catch up desktop chats from the newest page (#29125)
* fix(native-chat): retire acknowledged send echoes after catch-up

* test: follow phone lint rules in reconnect regression

* test: format reconnect message expectation

* Keep current chat requests independent of loaded history

* Read current chat requests in bounded cursor pieces

* Bound desktop catch-up after independent request support

* Fix current request subscription static checks

* Match generated request schema import ordering

* fix(native-chat): correct current request type boundaries

* Accept readonly client capability fixtures

* Exercise current request reads in wire compatibility inventory

* Align current request fixtures with provider commit and RPC surface

* test(native-chat): include current request browser capability

* Keep the mobile chat session hook within its line budget

* Format mobile current request readers

* fix(native-chat): reopen once for a current-request version whose read keeps failing

A deterministic read failure reopened the subscription, whose first frame re-read the same version and reset the backoff, so the pane resubscribed every 750 ms forever.

* test(native-chat): type the current-request snapshot fixture

* test(native-chat): wait for the hydrated open before driving the subscription
2026-10-16 17:46:54 -07:00
Brennan Benson 8c1ade9e44 Keep pending chat decisions available outside loaded history (#29121)
* fix(native-chat): retire acknowledged send echoes after catch-up

* test: follow phone lint rules in reconnect regression

* test: format reconnect message expectation

* Keep current chat requests independent of loaded history

* Read current chat requests in bounded cursor pieces

* Fix current request subscription static checks

* Match generated request schema import ordering

* fix(native-chat): correct current request type boundaries

* Exercise current request reads in wire compatibility inventory

* Align current request fixtures with provider commit and RPC surface

* test(native-chat): include current request browser capability

* Keep the mobile chat session hook within its line budget

* Format mobile current request readers

* fix(native-chat): reopen once for a current-request version whose read keeps failing

A deterministic read failure reopened the subscription, whose first frame re-read the same version and reset the backoff, so the pane resubscribed every 750 ms forever.

* test(native-chat): type the current-request snapshot fixture

* test(native-chat): wait for the hydrated open before driving the subscription
2026-10-16 16:01:58 -07:00
Brennan Benson b3048a2b9c Wait for agent readiness without blocking native chat (#27925)
* Finish structured agent readiness before delivering held work

* Adapt permission fixtures to startup lifecycle

* Keep model discovery for chats before their first start

* Use one host readiness promise for provider operations

* Await provider opening in the execution host

* Await Claude initialization in host-owned open

* Observe Claude readiness through the host status

* Keep optional readback behind host readiness

* Recheck launch permission after host readiness

* Preserve the awaited child during permission revalidation

* Record failed owned opening before the first message

* Abort a matching provider open when its child exits

* Cover provider exit during an unanswered host open

* Cover explicit failure facts on a host-stopped child

* Keep startup waits cancellable and preserve operation order

* Keep live choices and delivery order while agents start

* Apply startup picks and let parked messages release operations [skip ci]

* Save a waiting startup pick after Stop ends its opening [skip ci]

* Save choices when startup ends or a replacement is still opening

* Check saved choices separately from Claude startup reports

* Align startup fixtures and preserve observed Claude exits

* Order startup picks before later messages and retain flow regressions

* Keep reviewed lifecycle contracts while pruning duplicate tests

* Save picks after a proven exit without requiring a child

* Type the startup reply fixture against the Claude control contract

* Honor the before-input callback in the startup reply fixture

* Keep startup option menus independent and show the saved choice

* Exercise startup picks across real menus and the runtime

* Keep saved effort visible when picking a model during startup

* Include the running-turn scope in repeated Stop fixture

* Use the current attach contract in import fixtures

* Align carried-message fixture with startup event contract

* Keep launch-only permission picks usable while prior writes wait

* ci: rerun on main with the locale fix

* Preserve goal retries and chat launch selections

* Give launch regression its request identity

* test(native-chat): type the provisional-launch hook's session prop
2026-10-16 12:44:35 -07:00
NeilandClaude 7306273d89 fix(ssh): switching back to a quiet remote terminal no longer refetches its snapshot (#29253)
* fix(ssh): switching back to a quiet remote terminal no longer refetches its snapshot

Co-Authored-By: Claude <noreply@anthropic.com>

* test(cross-version-wire): drop notice negotiates only between current peers

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-15 19:39:59 -07:00
Jinwoo Hong dfafde6a7f test(cross-version): pin the ephemeral-key and source-fence downgrade tests to v1.4.224 (#28803)
v1.4.225 ships both capabilities, so the newest release no longer models an
old host. Pin each to the last release without its capability, as the other
downgrade tests in the suite already do.
2026-10-14 14:39:55 -04:00
OrcaWinandm4air 822bd9f972 fix(ssh): keep 1.4.224's running terminals working after the upgrade (legacy relay bridge) (#27787)
* wip: legacy relay bridge client

* Revert "fix(ssh): end an older Orca's relay terminals from the connect refusal (STA-10166) (#27748)"

This reverts commit d4a55f583e.

* feat(ssh): bridge terminals an older relay still runs into their old tabs

* test(ssh): cover the legacy relay bridge client, target bridge, transport and gate

* test(ssh): type the bridge test fakes without assertions

* test(e2e): a 1.4.224 relay's live terminal survives the upgrade on a Docker SSH host

* ci(e2e): route legacy relay bridge changes to its Docker spec

* fix(ssh): bridge delta moves too, and never read a non-exhaustive relay listing as exit

* fix(ssh): keep retrying a bridge for panes that mount before connect; end panes proven gone

* fix(ssh): bridge 1.4.224's detached leases through export preflight; publish every leased pane before the first look

* test(e2e): build the released relay out of process so Playwright never loads ESM build tooling

* fix(renderer): drop the recovery hook main removed from PtyTransport

* test(e2e): show the converted worktree before reading the bridged tab

* test(e2e): reconnect and wait for the converted host before opening the bridged tab

* fix(renderer): match bridged leases to tabs mirrored from the managed server

* test(e2e): find the mirrored relay-era tab under any worktree key

* test(e2e): open the converted worktree before looking for its mirrored tab

* test(e2e): a retired lease is terminated or pruned

* fix(ssh): back off attaches a listed terminal refuses; one pane per bridged shell

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-12 02:24:12 -07:00
Neil bb2d3090ab Share historical test source and add manual cache cleanup (#27743)
* Share immutable historical source across local cross-version test caches

* Add preview-first cleanup of old historical test caches

* Include historical test cache cleanup in worktree space maintenance

* Report and retain read-only Electron installs during cleanup

* Protect read-only installs under root and isolate cleanup CLI fixtures
2026-10-11 21:11:37 -07:00
OrcaWinandm4air 0663e27b35 feat(web): the browser signs in with a non-extractable device key, or pairs for this session only (#27552)
* feat(web): the browser signs in with a non-extractable device key, or pairs for this session only

- On a secure page with WebCrypto Ed25519, the browser registers a non-extractable key
  (stored in IndexedDB, create-exclusive across tabs) over a v2 token socket, then
  reconnects with key auth and replaces its stored pairing code with a placeholder.
  Requests on a keyed socket carry no token.
- On a plain-http page on another machine, or without Ed25519, existing stored pairings
  keep working unchanged (with a warning); new pairings stay in memory and, when the host
  supports it, register a session-only key.
- Only a sealed device_revoked naming this browser's key deletes it; a downgraded host,
  a bad proof or host storage trouble keep it.
- The shared handshake core gains an async device-key credential (v2 only).

* fix(web): mark web sockets, bind keys to their pairing, survive a broken key store

- Every web socket carries ?orca-client=web so hosts gate only the web bundle.
- A key record belongs to one pairing revision: a re-pair uses its fresh code.
- A code that never migrated connects without reading IndexedDB; a migrated
  pairing whose key store fails retries instead of going dead.
- A tab that finds the key already active forgets its code, and saving a stale
  copy of the same pairing never puts the code back over a placeholder.

* fix(web): a stale tab never deletes or overwrites a newer pairing

A key record from a newer pairing revision is left alone (the tab stays on its
code), and saving an older revision never replaces a newer stored pairing.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 15:17:00 -07:00
OrcaWinandm4air fee083fa47 feat(runtime): session-only browser keys and opt-in for pairing from plain-http pages (host) (#27540)
* feat(runtime): session-only browser keys and an opt-in for pairing from plain-http pages (host)

- pairing.registerDeviceKey accepts ephemeral: true (advertised as
  pairing.device-key-ephemeral.v1). Such keyed rows are dropped when the host
  restarts and after 12 hours with no live socket; a sweep removes them and cuts
  any socket still bound to them, so later key auth gets device_revoked.
- The WebSocket upgrade Origin is recorded per socket. A first pairing (a
  never-used pairing code) from an http:// page on another machine is refused
  with a sealed insecure_origin error unless the operator sets
  ORCA_ALLOW_INSECURE_WEB_PAIRING=1. Already-paired browsers, https and loopback
  pages, and native clients are unchanged.
- Registry file reading moved to device-registry-file-read.ts to keep the
  registry under the size cap.

* fix(runtime): gate first pairings only for the marked web client, never phones

React Native sends an http Origin on every LAN WebSocket, so the Origin alone
refused first-time QR pairing from released phones. The web bundle now marks
its upgrade with ?orca-client=web; only a marked socket from an http page on
another machine is gated. Released hosts ignore the query string.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 14:40:36 -07:00
OrcaWinandm4air f9794d8efc refactor(remote): one paired E2EE handshake core for desktop, CLI, web and mobile clients (#27523)
* refactor(remote): one paired E2EE handshake core for desktop, CLI, web and mobile clients

Every paired client now builds its hello, checks the host's ready, seals its
auth frame and opens host frames through src/shared/paired-e2ee-client-handshake.
Platforms inject their own NaCl/base64 primitives. The v2 client session and its
pure-JS key schedule (@noble/hashes 1.8.0, the version mobile pins) move to
src/shared, and the per-host v2 latch policy is shared with an injected store.

No wire change: frames match the captured legacy and v2 fixtures byte for byte,
and a cross-version test drives the core against the released host and the
released client against this host. The auth step is now async so a device-key
signer can slot in later.

* fix(remote): keep @noble/hashes out of the CLI's unbundled runtime

The packaged CLI loads src/shared output from disk, so a bare @noble/hashes
import failed the packaged-runtime check. The v2 key schedule is now a
primitive: Node clients use the node:crypto schedule (moved to src/shared so
the CLI can reach it), while the browser and phone keep the pure-JS one.

* fix(mobile): re-check the v2 latch before sending a legacy auth frame

The auth step is async, so another socket can latch the host to v2 while a
legacy auth frame is being sealed. Refuse it at send time, as before.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 12:14:51 -07:00
OrcaWinandm4air 6ffbdcd875 feat(runtime): device-key authentication and per-installation key migration (host) (#27493)
* feat(runtime): device-key authentication and per-installation key migration (host)

* fix(runtime): corrupt registry is never a revocation verdict; keyed sockets dispatch without a token; forks keep their Relay binding

* fix(runtime): keep an unparseable device registry aside; an unlisted key is never 'revoked' while it exists

* fix(runtime): a set-aside corrupt registry keeps verdicts doubtful even when the registry file is missing

* test(runtime): registerDeviceKey reaches the bound socket through the dispatcher for runtime and mobile rows

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 07:51:33 -07:00
OrcaWinandm4air 7f30a7aa35 fix(mobile): direct connections use v2 framing and never fall back once v2 is seen (#27473)
* fix(mobile): direct connections use v2 framing and never fall back once v2 is seen

* test: drop casts in direct v2 tests

* fix(mobile): count v2 handshake failures against the auth retry budget; latch only after host auth

* fix(mobile): pick direct framing at open, recheck the latch before legacy auth, and keep old hosts off the auth budget

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 06:41:39 -07:00
e9279b613e refactor(ssh): drop the legacy-relay bridge and the relay branch of the terminal gate (#27471)
The terminal gate's relay lister always resolved to null in production (no SSH PTY
provider is ever registered), so its relay and previous-relay branches, the legacy
relay route/router/routing, the previous-relay census and the once-connected
re-check could never run. The gate now takes only the host census; census-only
verdicts are pinned by a characterization test written before the change.

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-11 06:12:22 -07:00
OrcaWinandm4air 2b4b60a87e refactor(relay): delete the SSH relay server, its build assets and tests (#27215)
* wip: delete the SSH relay server and its build, assets and tests

* wip: drop main/relay plugin conformance; WSL reader lookup test

* fix: drop the emptied relay bench; prune baselines

* test: keep the native halves of tests that also covered the relay

* chore(ci): drop shard timings for the deleted relay server tests

* fix: keep the directory listing budget that darwin terminal names now import

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-10 22:36:42 -07:00
OrcaWinandm4air fff0bb0d06 feat(runtime): the host refuses a request meant for another runtime (#27224)
A CLI launched from an Orca terminal now sends, with each request, the
runtime it verified before sending (expectedRuntimeSource: the launching
profile's installation id plus the verified runtime id). The host checks
it before parsing params or running any handler and refuses a mismatch
with runtime_source_mismatch, so a runtime that took over the endpoint
between the CLI's check and its send cannot apply the request.

Optional field: an old CLI sends none and an old host ignores it (the
CLI's pre-send probe stays the fence there). Hosts advertise
runtime-source-fence.v1.

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-10 03:50:26 -07:00
OrcaWinandm4air 31b2b17d9b refactor(wsl): move the WSL guest bundle sources out of src/relay (#27174)
* refactor(wsl): move the WSL guest bundle sources out of src/relay

The WSL hook relay and WSL browser network relay are built from src/relay but are
not part of the SSH relay. Move them, and the 46 relay modules the hook relay
bundles, to src/wsl-guest so the SSH relay can be deleted on its own. Pure
renames; the built bundles are byte-identical.

Adds a boundary test so nothing outside src/relay imports it except relay tests.

* fix(ci): exempt renamed files' existing lint debt in the changed-lines gate

* test(relay): keep the cast suppression on the wrapped line

* test(relay): only parse files that mention relay in the import boundary check

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-10 02:46:27 -07:00
OrcaWinandm4air 180e8c186f feat(ports): host-scoped port scan and stop on the server (#27188)
* feat(ports): host-scoped port scan and stop on the server

Add workspacePorts.scanHost/killHost, addressed by workspace and resolved on the
server to the workspace's own execution host (this machine or one of its SSH
hosts). Stop is bound to the host its row was scanned on and is refused on SSH
hosts, where ownership cannot be proven. Advertised as
workspace-ports.host-scoped.v1.

* fix(ports): only this host's workspaces authorize Stop; a failed relay scan is not empty

Port ownership probes now come from host routing instead of the repo row's
connectionId, so an SSH repo that carries only executionHostId (or a runtime-
hosted repo) can no longer make a local listener look workspace-owned.

The relay's ports.detect now fails when it could not read the socket table
(unreadable /proc/net/tcp, or neither Windows scanner answering) instead of
answering an empty list, so a host scan reports unavailable rather than none.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-10 01:27:49 -07:00
OrcaWinandm4air cbeb703490 feat(hosts): servers publish a host descriptor (#27193)
* feat(hosts): servers publish a host descriptor

A paired server now reports a stable installation id, created once per
profile, plus an optional machine binding (HMAC keyed by that id over
the machine id and profile path) in status.get and in runtime pairing
offers. Optional fields only: older clients ignore them and older hosts
omit them. Clients start using it in the follow-up.

* fix(hosts): serialize installation id creation so concurrent starts never fork it

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-10 01:26:26 -07:00
OrcaWinandm4air b961442814 test(wire): give the preflight downgrade journey a non-Windows old host (#27177)
#27164 made an old host without a known non-Windows platform ask for an update
instead of answering its default list. The cross-version journey mocked the
capability probe but not the status read, so no request ever reached the wire.

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-10 00:32:53 -07:00
d3e3b173e6 fix(agents): one host-routed agent-detection entry point (#27054)
* fix(agents): one host-routed agent-detection entry point

preflight.detectAgents / refreshAgents take an optional { worktreeId } and the
host resolves that workspace's project runtime (WSL included) or SSH target.
Gated by preflight.workspace-scoped.v1; old peers keep the host default.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(agents): route detection by the worktree's own host record

A repo id shared by local and SSH rows made the first row pick the detection
host. Resolve the worktree from the host's list and apply the shared
execution-host routing rule; ambiguous rows refuse. Dashboard falls back to the
host-default list until a workspace's own list loads.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(agents): refuse an ambiguous workspace instead of probing the host

Only selector_not_found keeps the host default; selector_ambiguous now fails
closed. Adds runtime-level coverage of the lookup.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(agents): refuse an ambiguously owned folder workspace

Co-Authored-By: Claude <noreply@anthropic.com>

* test(agents): quick-launch runtime target carries the workspace

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-09 18:39:13 -07:00
Neil 1912c90141 Extract subprocess handling into @orca/process-host (#26963)
Extract subprocess execution and process contracts into @orca/process-host with explicit public entry points, independent checks, and one Node 18 compatible CommonJS runtime. Migrate application consumers and integrate package builds, dependency selection, import enforcement, and runtime resource staging.

Preserve daemon startup ownership and profile isolation, and qualify development rebuild recovery and concurrent output publication. Remove unnecessary aliases, test forwarding exports, and a duplicate output scan.

Validation: full typechecks, changed-code quality, package and artifact checks, native Windows builds/types plus 306 runtime tests, and independent native reviews.

Matched full E2E controls on original main and unchanged reviewed head both pass all 17 applicable tests (nine skips). Earlier intermittent E2E failures are documented in the PR; no assertions were weakened. The final combined-tree audit preserves both branches with no import-boundary violations.
2026-10-09 18:32:28 -07:00
Brennan Benson 59631b8b26 fix(native-chat): edit a queued message in place, even while a question is open (#26886)
* feat(native-chat): edit a queued message in place on the host

A queued card's Edit used to copy its text into the composer and delete the
card, so the message lost its place, an agent-mail card lost its sender, and
while a question or approval replaced the composer the text vanished.

The execution host now edits the card itself:
- agentSession.queuedMessageUpdate replaces only the card's text, as a
  compare-and-set on the body fingerprint the client last saw. It keeps the
  id, position, sender, kind, send mode and attachments; a command card is not
  editable. A resent Save answers from the card as it stands (unchanged or
  changed), so a lost answer never overwrites newer text.
- agentSession.queuedMessageEditHold acquires, renews and releases an
  in-memory edit lease per caller and edit. While one is live, automatic
  delivery stops at the card (earlier cards still send, later ones wait); an
  explicit Send still overrides. Leases expire after two minutes on their own,
  re-derive validity from the card on every read, and die with the
  conversation. Release uses cleanup admission.
- The published card carries an optional editHeld marker, never `paused`.
- Both methods sit behind the new agent-session.queued-message-edit.v1
  capability, independent of the dark mid-turn queue capability, and are on
  the mobile allowlist for the phone follow-up.

* feat(native-chat): edit a queued card inline on desktop

Edit message now opens an editor inside the queued card instead of moving the
text into the composer. The card keeps its place and sender, and the editor
stays usable while a question or approval holds the composer's slot (it does
not lose focus to the prompt card).

- Enter or Cmd/Ctrl+Enter saves, Shift+Enter is a newline, Escape cancels;
  IME composition keys never save or cancel. Save and Cancel replace the
  card's own actions while it is edited; one editor per pane.
- The editor holds the host's edit lease, renewing every 30 seconds and taking
  it again after a lapse. Lease failures are only logged: they never block
  typing, Save or Cancel.
- If the card is sent or removed elsewhere mid-edit, a clean edit just
  closes; changed text goes into the chat box after any existing draft, with
  one toast. A Save that lands and then sends counts as success.
- Against a host without agent-session.queued-message-edit.v1 the card offers
  no Edit (no copy-and-delete fallback); an empty overflow menu is hidden.

* fix(native-chat): name every queued-edit notice in its switch

* fix(native-chat): keep queued-edit typing safe and stop disabling Send elsewhere

- Save with an unreadable answer (dropped for a moved runtime fence, or lost)
  now reads the published card: saved text shown -> close; card gone -> the
  sent-as-saved / chat-box path; otherwise keep the editor quietly.
- Send and Steer stay enabled on a card edited on another device; the
  published editHeld marker is gone (no reader left). Automatic delivery
  still stops at an edited card on the host.
- Save answering changed moves the typing to the chat box and reopens the
  editor on the newer text. Notices say when the chat box is hidden by a
  question or approval.
- Closing the pane or switching chats with unsaved typing appends it to that
  chat's box quietly.
- Lease renewal no longer publishes, so an open editor is not idle-sweep
  activity.
- The consume compare-and-set also matches the body fingerprint.
- Saving unchanged text keeps the original blocks.

* fix(native-chat): keep Agent in the Japanese queued-edit notices, as the catalog does

* fix(native-chat): close a queued edit its own Save already landed, wake the queue for a pruned lease, keep typing across a reload

A Save whose answer was lost now closes once the card shows the saved text,
and a card that went out with exactly the typed text closes quietly instead
of copying it to the chat box. A renew that prunes another editor's lapsed
lease wakes the queue in place of the timer it cancels. A reload, which
unmounts nothing, hands unsaved typing to the chat box like closing the pane.

* refactor(native-chat): move the queued-edit notices to their own module

* fix(native-chat): unref the edit-lease timer in a way the phone's typecheck accepts

The lease module reaches the mobile test program through journal-store, where
setTimeout answers a number, so `timer.unref?.()` failed mobile's tests
typecheck ratchet.

* test(cross-version-wire): call the queued-edit methods in both skew directions

The manifest must name every agentSession method; the two edit methods
reach their host methods on a new build and are only called by clients
on a host advertising agent-session.queued-message-edit.v1.

* test(rpc): count the two queued-edit methods on the structured surface

* fix(native-chat): lay out the inline queued-message editor as a header, field and Save footer

While a card is edited, its row shows a pencil, "Editing message", the sender and an X that
cancels; the field at full width; and a footer with the key hint and Save. The card's queue icon
and its status line are hidden until editing ends.

* fix(native-chat): show the Enter key on the queued-message editor's Save button

The editor footer's muted key hint is gone. Save now carries the same inline
key badge as the new-workspace composer's Create button, extracted into a
shared ButtonKeyHint so both buttons render one badge.
2026-10-09 17:31:28 -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 a825a84a0f test(search): keep Kiro out of older hosts' expected history-search agents (#27002)
#26695 added Kiro to the history-search agent catalog and updated the
unit tests, but the cross-version wire tests still built their expected
older-host agent list as 'everything except the agents added later',
which now included Kiro. Older hosts never accepted Kiro, and the
client correctly withholds it, so those 7 tests failed on main.
2026-10-09 16:56:37 -07:00
Brennan Benson 573ff0bca3 Save a new chat's first message during create (#26332)
* fix(native-chat): the host owns the send queue; the window keeps no saved outbox

The desktop kept each structured chat's unsent messages in localStorage and
sent them in order, so one message whose fate was unknown froze every later
send, Retry dropped it silently, failed sends could not be discarded, and an
offline chat could send hours later. The host already records every message
and owns the queue; the window now only sends.

- One in-memory sender for composer, launch prompts and messages sent from
  outside the chat. One send in flight per chat; a transport failure resends
  the same id for up to 30 s; a refusal that proves nothing was recorded puts
  the text back in the composer with the reason; a send that went out and was
  never answered shows an in-doubt line with Send again and holds nothing up.
- The host's "unknown" rows get the same in-doubt line and Send again, from
  the journal, in every window.
- A message an older build left in localStorage is never sent: the host's
  conversation outline decides what goes back to the composer, and the copy is
  deleted once that is saved.

* fix(native-chat): send through the structured chat RPC wrapper, with its timeouts

The sender called the runtime RPC directly, skipping the per-method timeouts
every other structured chat call gets. Only a remote host's request skips the
compatibility check the sender already ran.

* fix(native-chat): an unconfirmed send goes back to the composer, with no new row line

The common pattern draws nothing extra on a message whose delivery is in
doubt once its turn is over, and puts a failed send's text back in the
composer with the reason. So a send nobody answered in time, or one a host
answers in a way that proves nothing, goes back to the composer worded as
unconfirmed, and a host "unknown" row shows nothing extra. Removes the in-doubt
phase, Send again, and its strings.

A host's made-up row for an id its journal lost now reads as unconfirmed, not
as recorded, so that message comes back instead of vanishing.

* test(native-chat): pin that nothing resends a send given back as unconfirmed

* fix(native-chat): sends survive a tab close, never resend after a Stop, and keep remote images

- A normal tab close lets sends on their way settle; what the host never took
  goes back to the conversation's draft. Only a cancelled launch or a worktree
  purge drops them.
- After any Stop, a send already on its way is never sent again under its id:
  a doubtful answer, or a resend that was due, hands its text back worded as
  unconfirmed.
- A returned image keeps the SSH connection it lives on; the connection never
  goes to the host.
- An older build's saved message the host recorded and then rejected is left
  to the host's own row, never handed back.

* fix(native-chat): a Stop or a tab close never resends a send already out, and a kept card is the card's

A Stop that landed while a same-id resend was being readied (its timer
fired, its request not out yet) let that resend go out after the Stop.
The sender now tracks whether a request is awaiting its answer: a Stop
hands back every send between attempts as unconfirmed, lets one whose
request is out settle from its answer, and never issues a request after
it. A normal tab close withdraws the same way instead of doing nothing,
so nothing more goes out and nothing is dropped.

A send the host rejected but kept as a card (keptAsQueuedMessageId) is
the card's, from its reply or the journal, and never goes back to the
composer.

Adds the freeze tests: a send whose fate is unknown holds later sends
no longer than its deadline, and one the host can neither confirm nor
deny releases the next at once.

* fix(native-chat): read a resend's turned-away call or reused id as unproven

Ports the send-answer proof contract. A call the host turned away before
running it (method_not_found, invalid_argument, unauthorized) proves only
that this request wrote nothing, so it reads as never sent on a first
attempt only; on a resend an earlier attempt may have landed, and it goes
again under the same id. A resent id the host says was already used for
other content (messageIdReused) proves nothing either, like an expired or
conflicting id.

Pins the rest of the contract: any row the host returns is its own, a
thrown error is no answer whatever its code, and an older build's
refused, held or outlived-Stop copy is handed back once and never sent.

* refactor(native-chat): type the structured composer's send with its attachment type

Keeps NativeChatStructuredSession.tsx within the file length limit.

* refactor(native-chat): drop the kept-card guards the sender never needed

A kept send's row is a rejection that was never a Stop's, so the sender
already reads it as recorded, from its reply or the journal. The test
that pins it stays; the two extra checks only covered a kept row that is
also a withdrawal, which the host never writes.

* test(native-chat): route launch tests' sends through the client wrapper the sender calls

The sender sends through callStructuredAgentSession, but these launch
tests replaced that module with a factory that answered nothing (and still
named a probe that no longer exists), so every launch prompt resent until
its 30 s deadline and the tests timed out. Each factory now hands sends to
the runtime RPC mock the tests already answer, and expectations of a local
send no longer ask for the remote-only compatibility option.

* fix(native-chat): hand a message back without importing the composer's attachment hook

The worktree purge reaches the launch prompt, which hands text back, and
the attachment hook's imports reach the store. A test that builds the
real store behind a mocked one then waited on itself and hung. Handing
back now writes images to the draft store directly, as the hook's helper
did, and a test pins that each returned image keeps its SSH connection.

* fix(native-chat): one send per chat, with no line of sends behind it

A chat with a send out took further messages into an in-memory line and
sent them one by one. A message typed behind one in doubt then hit its
own 30 s deadline and came back as not sent without ever going out.

Now, as the common pattern does, a chat takes one send at a time: while
it is out, Send is disabled and Enter leaves the text in the box. The
sender refuses a second send instead of lining it up, so the 30 s
deadline always runs from the send itself. A Stop or a tab close stops
the one send: settled from its answer if its request is out, handed back
as unconfirmed between attempts, or silently if it never went out.

Notes sent from outside the chat while its send is out stay with their
sender (not ready); a launch prompt that meets the person's own first
message waits in the composer instead of being lost.

* fix(native-chat): give a Stop-withdrawn note back to its chat once its notes were cleared

Notes sent from outside a chat clear once their message is recorded. A
message the host recorded as pending and a Stop then withdrew came back
only to its sender, which had already let go of it, so the text was
lost. The chat's draft now takes it, as an earlier build's outbox did.

* test(native-chat): type the composer-actions probe without a cast

* chore(native-chat): drop outbox wording left in comments and an empty locale group

* fix(native-chat): pace failed checks, keep the host's reason, and hold the chat for its launch prompt

- A send whose checks failed before its request went out (an unreachable
  or incompatible host, an unreadable history) was tried again at once,
  about a thousand times a second for 30 s. Attempts are now paced by
  the attempts made, whether or not their request went out.
- A host that refuses every resend by throwing (native chat turned off,
  a journal that won't open) gave back only "couldn't confirm". The
  host's reason now comes first, still without claiming not sent.
- A launch's prompt now holds the chat's one send from the click, drawn
  as sending, so a message typed while the chat starts can't overtake
  it; it goes out once the chat exists, and a cancelled launch frees it.
- Notes whose send nobody could confirm say so instead of "did not
  accept", and notes launched into a new chat let go of their text once
  that chat's composer holds it.
- An open chat keeps drawing a recorded send until its row arrives, so a
  reply that beats the history no longer makes the message flicker out.

* fix(native-chat): hold a chat's sends until it has started, and send a failed chat's message with its restart

A message sent to a chat still starting went out at once to a host that
had no record of the chat yet, read for a fence it could not get, and
came back as not sent. A message sent to a chat whose start failed
restarted it but no longer went with the restart.

While a chat starts, Send stays off and Enter leaves the text in the box,
as with a send already out. A text message sent to a chat whose start
failed restarts it and goes as the restart's first message, through the
same staged-prompt path a launch prompt takes: it holds the chat's one
send slot, drawn as sending, until the chat exists, and comes back to
the composer if the restart fails again. Notes wait while a chat starts
and ride a failed chat's restart, keeping their text until it is sent.

* chore(native-chat): test the queue request, rejection words and card hand-offs; drop an unused clear

- Pin which sends ask the host to queue, the moved rejection wording,
  and that a queue send whose card was handed off and then refused or
  withdrawn, or whose replay names a withdrawn card, is never handed
  back.
- The send-at-most-once gate now says what the desktop promises after a
  reload: it never sends the id again, and hands the text back.
- The legacy read names when it goes, and drops the notice clear nothing
  called.

* fix(native-chat): keep a sent launch prompt's entry, and give back at once what can't go out

- Cleaning up a launch prompt released its send slot even after the
  prompt had gone out, which deleted the entry the sender keeps once the
  host records it. A launch prompt recorded as pending and then
  withdrawn by a Stop was lost from both the chat and the box, and an
  open chat dropped the new chat's first message before its row arrived.
  The slot now gives back only a reservation that was never sent.
- A send stopped before its request went out by something trying again
  won't clear (this client and the server can't talk, or the host
  refused the history read) kept Send off for 30 s and then said Orca
  couldn't reach the agent. It now comes back at once with its own
  cause: not sent, since nothing went out, or unconfirmed if an earlier
  attempt did. Transport errors keep the paced retries.

* fix(native-chat): notes keep their own text through a new agent's launch

Notes sent to a New agent whose start failed came back to the notes and
also sat in the new chat's composer, so sending both delivered the text
twice. The notes keep their text until it goes out (they hold it from
the click), so the launch now says so and no composer gets a copy, on a
failed start or a refused prompt alike. The tests that asserted the copy
in the composer pinned the old double ownership and now assert the
notes are its only owner.

Notes that rode a failed chat's restart and ended unconfirmed now say
Orca couldn't confirm them, as notes sent directly do, instead of that
the agent did not accept them.

* chore(native-chat): the send-once gate says what the desktop keeps across a reload

The desktop keeps a send's id in memory only: a send still unsettled at a
reload or crash is not resent and not handed back. The coverage notes no
longer credit the renderer tests with durable identity across a remount.

* fix(native-chat): say once why notes sent to a new agent did not go

Since notes keep their own text through a new agent's launch, a prompt
the host refused, nobody could confirm, or that found the chat's send
taken came back to the notes with nothing said anywhere: the chat shows
no notice for text its caller keeps. The notes menu now reports it once,
with the toast a send to an existing chat already uses: not accepted,
couldn't confirm, or not ready. A start that failed still says so in the
new chat instead.

* fix(native-chat): retry a history refusal the host says clears, and name it at the deadline

A send stopped before its request went out came back at once for any
refusal of the history read, including ones the host names as clearing
(its journal briefly unavailable, a chat detached while the host quits),
so the automatic retry was lost. Only a version mismatch and refusals
that won't clear come back at once now; the rest go again on the paced
schedule, and if the deadline still finds nothing sent, the words are
the host's refusal rather than Orca couldn't reach the agent.

* feat(native-chat): a send makes one request, and nothing ever sends it again

The desktop resent a message under its own id for up to 30 s when the
answer was lost. A send now makes exactly one request, as the common
pattern's clients do:
- An answer that is lost, dropped or proves nothing hands the text back
  at once with "couldn't confirm… check the chat".
- A failure before the request goes out (the environment check, the
  history read for the fence, a refused connection) means nothing went
  out: the text comes back at once with its own reason, or as not sent.
- The 30 s cap stays on the one request, so a host that never answers
  can't hold Send.

Nothing ever resends, so nothing can go out after a Stop: a Stop only
takes back a send still in its pre-send checks. The resend state goes
with it (tries, generation, awaiting, stopped, the resend timer, the
send-answers-proof probe, and the first-attempt/resend split in the
evidence), and the send-once gate says the desktop never resends.

* fix(native-chat): notes keep the chat's line, and a send held behind a rewind says it was not sent

- Only a send whose text belongs to the chat's composer clears the chat's line. Notes sent from
  outside the chat no longer wipe a "couldn't confirm... Check the chat" line that explains text
  already back in the box.
- A send the host turns away behind a rewind it could not confirm went back as "not sent" but said
  "couldn't confirm what happened. Check the chat". It now gives the rewind's reason and says the
  message was not sent.
- Reliability gate names the single-request test and records a fresh evidence run; the sender
  test drops its leftover resend mocks and a duplicate Stop test.

* fix(native-chat): a send ends even when putting its text back fails

If writing the returned text into the chat's draft threw, the send never settled: it stayed
"sending", the chat refused every later send until a reload. The send now always ends after a
hand-back, the failure is logged, and the chat's line adds "Couldn't save your message."

* fix(native-chat): a message typed during /clear stays in the box and follows the chat

A /clear moves the chat to a new conversation, and its host refuses any send while it runs. A
message sent then went out, came back refused into the old conversation's draft with its line, and
vanished when the chat moved on: the box and the line now belong to the new conversation.

- A /clear holds the chat's one send slot while it runs: Enter does nothing, Send shows busy and
  the text stays in the box, as for a send that is out. Notes sent from outside get "busy".
- Once it moves the chat, the old conversation keeps taking no send until the view leaves it, and
  what is left of its draft moves into the new conversation's draft, after anything there: when the
  composer's /clear settles, and again when the view moves.

* fix(native-chat): no "Send message?" or queue clear while the chat's send is out

While a send is out (or a /clear runs) the chat takes no message, yet Enter over a held queue still
opened "Send message?", and Clear queue deleted every card before its message was refused. Enter now
does nothing there and the text stays; Clear queue re-checks and deletes nothing if a send went out
after the question opened.

* refactor(native-chat): take the /clear draft carry out of this change

Moving the old conversation's draft into the one a /clear replaces it with fixes a bug main has too
(text left in the box during a /clear stays with the old conversation), so it goes in its own change.
Kept here: a /clear holds the chat's send slot while it runs, and the conversation it moved away from
takes no send until the view leaves it.

* fix(native-chat): a /clear's hold on sends always ends

- A view that unmounted while a /clear that moves the chat was out left the old conversation
  holding its sends until a reload: the late reply kept the hold for a view that was gone. The
  reply now releases it.
- The local call for a conversation command has no deadline of its own, so a /clear that never
  answers kept Send off for good. The hold now also ends at the command's deadline (195 s, the one
  the remote call already uses), whichever comes first.

* test(native-chat): opening a chat an older build left stuck; hand back its copy in send order

Pins, through the real chat hook, sends, legacy recovery and draft store,
what opening such a chat does: the queued messages behind a message the
host recorded in doubt come back to the composer once, in order, with the
"not sent" or "couldn't confirm" wording; the chat is free to send under
its read's fence; nothing happens while the chat has no fence here.

The legacy reader now hands entries back in the order they were sent
(queuedAt), as the older build's reader did, instead of array order.

* fix(native-chat): a send this window turned away for a re-paired server comes back as not sent

A managed server's update rotates its pairing, and this window's main
process then answers the next call itself, before forwarding anything,
with runtime_environment_changed. The send read that thrown answer as
proving nothing, so the person was told Orca couldn't confirm a message
that never left. It is now handed back as not sent, with the reason.
Every other thrown answer still reads as unconfirmed once the request
may have gone out.

* Save a new chat's first message during create

* Align opening-message types with the user-message wire

* Keep chat publication and cleanup outside store initialization

* Complete the create refusal fixture ledger

* test(native-chat): prove a created first message waits for the agent's start and is handed over once

* refactor(native-chat): keep the host and session hook under the line limit after the main sync

* test(native-chat): a chat created with its first message and no pick still teaches its agent's default

After the main sync, a created chat's agent starts from the delivery loop rather than the create
call. Grok (through the real ACP adapter and model catalog) and Claude (through the real adapter)
each still report the model their own config resolved to once the chat reads its options.
2026-10-09 13:05:35 -07:00