Commit Graph
1150 Commits
Author SHA1 Message Date
Neil 8452fc3315 Keep tool results with the right command and reveal all results on mobile (#26968)
* Pair native chat tool results in linear time with provider identity

* Preserve orphan tool output and independent disclosure identity

* Keep mobile tool detail attached to its command after row changes

* Keep named mobile results open across copied history refreshes
2026-10-09 21:47:51 -07:00
Neil 7c22f6f045 [CI] Consolidate tests, retire static scans, and update evidence (#27109)
* [CI] Consolidate renderer test modules and retire static scans

* [CI] Keep consolidated test within the existing line limit

* [CI] Refresh evidence for the preserved turn-duration gate
2026-10-09 21:16:00 -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
Jinwoo Hong 0703073b61 Add an inert read-only layout stream and the layout round-trip self-check (#26964)
* feat(layout): read-only layout.subscribe stream and the round-trip self-check (inert)

- layout.subscribe streams each workspace's layout as an explicit published projection
  (tabs, groups, panes and their terminals), never the model itself: no runtime records,
  closed-tab records, spawn inputs or legacy persistence values. It is re-projected from the
  Store's sessions after each write, only while someone subscribes, and only from the
  partition that owns each workspace. Not advertised as a capability and not on the mobile
  allowlist, so nothing calls it yet.
- Replaces the unused terminal-topology publisher (#25682) it generalizes.
- The round-trip self-check: a partition must load and save to a fixed point, changing only
  the kinds of value the Loader's rules change. It runs on Store writes in dev and e2e (logged,
  never thrown) and as the oracle's new roundTrip check.

* fix(layout): know the spawn handoff main's minimal row mint stores

* fix(layout): name-based invented ids, no partition on the wire, isolated listeners

- Every id the Loader invents is derived from what it stands in for (a workspace's group from
  its key, a pane from its row or tab and position, a repeated tab id from its terminal), so
  removing an earlier workspace or tab no longer re-ids later ones.
- The published layout no longer names its partition: the workspace key is the address and
  routing stays with the host; each tab still names the host that runs it.
- One throwing stream listener no longer costs the others the event.
- The self-check says plainly it is an idempotence check, and logs each new kind of first-load
  change with its count.
2026-10-09 17:29:02 -04: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
Jinwoo Hong 9021133929 Add the inert workspace layout model, Loader and Serializer (#26748)
* feat(layout): add the inert workspace layout model, Loader and Serializer

One model of a workspace's tab layout (all tab kinds, groups, split trees,
pane bindings, titles/colors/pin, sleeping records) in src/shared/workspace-layout,
with a Loader from today's stored session partition and a Serializer back to it.
The structural rules check and the pure pane/group tree helpers move into the
same folder so the module can run them; their callers import the new paths.
Nothing in the app calls the new module yet.

* fix(layout): avoid toSorted in shared code and drop an unused helper

The mobile bundle includes src/shared and Hermes lacks Array#toSorted.

* fix(layout): one stored home per layout fact; Loader reports every rule it applies

The row's terminal id, preview flag, worktree id and execution host are now
derived on save instead of kept a second time. Groups never stay empty. The
Loader reports row/tab-bar disagreements, re-minted ids, worktree id and host
disagreements, keeps a row stored in two workspaces where the tab bar names it,
and gives a pane id repeated in a second tab a new unbound pane. The rules check
runs on what the Serializer writes. Fixtures become test fixtures, and a test
lists every model field with the one fact it holds.

* fix(layout): derive each tab's host from its owner and keep one preview fact per editor tab

An editor tab names its file's owner (an SSH target or paired runtime), every
other tab its partition. Where the stored tab and file preview flags disagree
the tab loads permanent, so a draft can never be replaced. A row terminal that
differs from the one derived from its focused pane is now reported.

* fix(layout): one binding kind, one title, one host rule; records follow a reassigned pane

A legacy row with no pane layout now loads as a one-pane tab bound to its
terminal (reusing the leaf its records name), so the null-panes branches and
legacyPtyId are gone. A pane given a new id because two tabs shared it takes
its sleeping and incarnation records, scrollback and focus with it. A terminal's
live title is one fact (the tab-bar label wins on load). Every tab-bar entry
names its execution host by one rule. The fact-location test is now keyed by
on-disk field and checked by mutating every source.

* fix(layout): one change report by construction; records without copies

The Loader now picks values by fixed precedence only and reports one
field-by-field diff of the stored session against what the next save
writes, over the disk-field tables (now in workspace-layout-disk-fields.ts,
with session fields derived from SESSION_FIELD_OWNERS). Sleeping and
closed-tab records live in their workspace without the pane key, tab and
worktree id their key and workspace already give; the Serializer fills them.
Tab-bar entries are written in the one tab order.

* fix(layout): name the change report's key-list parameter's type

* refactor(layout): key pane data by leaf, check the model itself, one legacy record

- Each pane's data (terminal, incarnation, title, sleeping agent) is one leaf record keyed by
  leaf id, and scrollback beside it is keyed the same way. Pane keys are derived only when
  saving. The Loader gives a repeated leaf id a new id before attaching records, so
  rekeyPaneRecords is gone; records naming no pane are carried as stored.
- The rules check runs on the model directly (ungrouped tab, empty group, group tree that
  disagrees with the groups, pane data without a pane); the disk-format rules stay separate.
- The row-ownership marker and topology revision move into one legacy record outside
  WorkspaceLayout, updated by updateLegacyPersistence after each apply.
- Fixtures include a terminal tab whose tab-bar id and terminal id differ.

* fix(layout): join the group ids the tree-mismatch detail names

* fix(layout): leave the stored revision map as is when no repo advances
2026-10-09 15:05:22 -04:00
Jinwoo Hong e5412cccec test(e2e): headless layout oracle covers tab moves between groups (#26857)
Two headless scenarios drive session.tabs.move (split into a new group,
move into an existing group, reorder, and a move that empties a group) on
orcad and Electron serve, and check group order, the selected tab per group,
and a warm restart. The temp repo is resolved to its real path so macOS runs
list its worktree without a TMPDIR workaround. Two new main findings are
listed as known: headless saves groups without a tab bar, and move-to-group
leaves the target group's previous selection.
2026-10-09 15:05:17 -04:00
1f8dce85d4 refactor(runtime-rpc-terminal-io): clean up Phase 3-3.8 code (#26895)
* refactor(runtime-rpc-terminal-io): clean up Phase 3-3.8 code

- One admission prologue and one unary invocation shared by both RPC dispatchers
- Delete rpc schema re-export shims; import from the shared params catalog
- Delete the unregistered duplicate markdown tab method module
- Collapse multiplex installer stage types; add isMultiplexStreamAttached
- One source-identity guard in the ACK recovery snapshot
- Single stream teardown in the renderer multiplexer
- De-duplicate RPC method declarations; derive dispatch options from RpcContext
- Share viewport-frame decoding and one head-trim loop for pending queues
- Single sendTerminal call in sendTerminalStreamInput

* test: point cross-version and scope checks at the moved browser tab-create schema; run transport parity test under Node

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 11:50:31 -07:00
cbc6f54e1c refactor(runtime-core): clean up Phase 3-3.8 code (#26894)
* refactor(runtime-core): clean up Phase 3-3.8 code

- worktree.ps: summary lookup is a closure, not a 4-arg callback
- relay: shared RelayStreamAckWindow for fs and git response streams
- relay: terminal-frame slots live on RelayStreamRegistry
- one isBinaryBuffer/BINARY_PROBE_BYTES in shared/binary-buffer
- one automation owner fence on the controller, no casts
- drop unchecked Store cast in remote repo registration
- speech download reuses the shared abort-aware delay, no casts
- RelayRuntimeServices: drop leftover handler list and param casts
- use shared errorMessage() in audited files

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

* test(cross-version-wire): pass worktree ps summary lookup in both builds' shapes

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 11:40:51 -07:00
Neilandm4air 51e7181850 Keep remote terminal path links on their own host (#26696)
* Keep terminal workspace root links on their execution host

* Resolve terminal root-link source host from the owner, not a missing connection id

A direct SSH pane whose worktree id also exists on the desktop has no
connectionId (ambiguous owner), which was read as local and offered the
desktop workspace. Resolve the source via the owner-route resolver and
skip the root shortcut when the source host is unknown.

* Resolve terminal file links against the source owner's host

A same-id SSH workspace left connectionId unset, so a rejected root link,
the default-app row and Shift-click fell through to this machine's files.
The terminal file context now names the resolved direct SSH host and
refuses file access when no owner can be determined.

* Keep an ambiguous terminal link owner unresolved instead of local

The connection lookup ignores detected worktree rows, so a registered local
row could override an owner the full resolver found ambiguous (registered
local, detected SSH) and send link stat/open to this machine. Only a missing
route may now defer to the connection lookup.

* Re-resolve a held chat link's owner before rechecking it

Native chat rechecks held link targets when a turn ends. A target resolved
while its owner was local kept that context after a same-id worktree was
detected on an SSH host, so the recheck still asked this machine. Each
existence ask now re-resolves the workspace owner and refuses when it is
unresolved or names a different host.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-09 10:33:42 -07:00
Jinjing c9c6a00449 Keep usage overflow chip width stable as hidden count changes (#26873)
The "+N" overflow chip in the status bar changed width whenever its count changed, so the density probe that decides how many usage chips fit kept re-measuring. Reserve the width of the provider-count label with an invisible sizer span, so the chip keeps the same size across counts.

- Pass the total provider count into UsageOverflowChip and render it in an invisible, grid-stacked sizer span.
- Overlay the visible "+N" label on the same grid cell so the chip width tracks the total, not the hidden subset.
- Add an e2e test that loads ten usage providers, shrinks the viewport to collapse them, and checks the chip width stays stable and no "Maximum update depth" or React #185 errors occur.
2026-10-09 05:58:50 -07:00
Neilandm4air 05ab94bd65 Move shared file watches when the selected remote host changes (#26632)
* Invalidate shared file watch targets when the selected host changes

* Watch the selected host's own workspace root when hosts share a workspace id

* Watch each open editor on its own execution host instead of the selected host

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-09 05:47:35 -07:00
Neil 941c8d88c7 Reset explorer tree lifetime when its execution host changes (#26627) 2026-10-09 05:25:26 -07:00
Neil 22534fe644 Show whole-tab hover cards with icons and smooth tab-to-tab slides (#26811)
* Show whole-tab hover cards with icons and sliding handoffs

* Remove the gap below tab hover cards

* Keep tab hover handoffs reliable during rapid reversals

* Place hover-card icons and program names below the title

* Fix nested keyboard hints and complete hover-card translations

* Isolate keyboard hover checks from terminal activation focus

* Sample rapid tab slides within the renderer

* Control hover frame recording around actual pointer input

* Keep tab hover cards warm through StrictMode ref reattachment

* Keep hidden renderer animation frames live during hover checks

* Sample hidden tab hover cards with an unthrottled timer

* Narrow hover animation effects and avoid shadowing window

* Keep hidden renderers painting while checking hover card frames
2026-10-09 05:11:11 -07:00
Neil 93a6b1abff Keep managed file drags out of desktop browser paths (#26698) 2026-10-09 05:09:00 -07:00
Neil 6c20143807 Preserve the selected host in explorer watcher ownership (#26616) 2026-10-09 05:06:20 -07:00
6aef5c569c Phase 3.7: fix SSH/remote failures found by adversarial real-host testing (#26847)
* fix(remote-terminal): keep a reconnected pane's size after an SSH outage (P1-4) (#26625)

* fix(remote-terminal): keep a reconnected pane's grid when its stale stream is reaped (P1-4)

The host's width floor only handed ownership to viewers that had claimed
since subscribing. A reconnecting desktop pane re-subscribes without
claiming, so when the heartbeat later reaped the dead socket that owned the
floor, the host reclaimed the PTY to its spawn size (120x40) and held the
pane there. The owner's same-client replacement stream now inherits the
floor, and a pane whose stream was reaped before it reconnected resumes
ownership on re-register unless the host or another viewer claimed since.

* fix(remote-terminal): host typing during an outage revokes the reaped owner's resume

Host input reaches the PTY without a renderer viewport claim once the host has
reclaimed its grid, so the orphaned-owner entitlement survived and a passive
reconnect resized the host pane back to the remote grid. The host write path
now clears that entitlement on driving input (query replies, focus reports and
launch writes excluded).

* test(pty): stub the runtime's host-input note in write-path handler tests

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(remote): an oversized RPC reply fails that request instead of closing the host connection (#26624)

Expanding a folder with ~80k entries on a managed SSH host produced a
files.readDir reply over the 4 MiB encrypted-reply cap. The channel treated
that as a transport fault and closed the whole control socket (1013), and the
explorer rendered the failed folder as empty.

- The WebSocket RPC transport now answers an over-cap reply with a
  reply_too_large error for that request id, so the channel never sees it.
- files.readDir checks remote listings against the request's content budget
  and names the entry count.
- The explorer shows a subfolder's read error on its expanded row.

Finding: HH-2 (Phase 3.7 hostile-host).

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): let a slow host finish installing the pinned Node runtime (HH-3) (#26631)

* fix(ssh): let a slow host finish installing the pinned Node runtime

The POSIX promote (extract, hash, self-test and publish the ~110 MiB node
binary) ran under the 30 s default exec timeout. On slow home storage it
timed out, the remote work kept running, and the runtime store lock it held
stayed until the 20-minute stale rule, so relay rung A queued behind it past
its own deploy bound.

Give both platforms the 300 s promote bound and have the host release the
store lock itself when the promote exits, with owner-checked releases so a
late release never removes a lock someone else took. (HH-3)

* fix(ssh): let the host release the store lock after a slow under-lock re-hash

The under-lock re-check of a sibling-published runtime hashed the ~110 MiB
executable as its own command under the 30 s exec default and outside the
host-side lock release. On slow storage it timed out, the client correctly
kept the lock, and nothing on the host freed it until the 20-minute stale
rule, so later installers ran out their deploy bound.

Run the re-hash and the conditional promote as one host command wrapped in
the host release, and give every full-runtime hash, including the initial
probe, the slow-storage bound. (HH-3)

* test(ssh): count the combined under-lock re-probe and promote as one host command

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): shell startup output on stdout no longer breaks SSH connect (HH-1) (#26617)

* fix(ssh): fence captured exec stdout so shell rc output cannot break connect

sshd runs the user's login shell before Orca's /bin/sh wrapper, so any
startup-file stdout (fish/tcsh/zsh/bash echo, or a bare terminal-title escape)
was glued onto every captured answer. The home probe read "banner\n/home/u"
and connect failed with "Remote home is not a valid path".

execCommand now wraps wrapped POSIX commands in a per-call nonce fence and
returns only the payload between the markers; the SSH folder browser gets the
same fence. The fence is the existing WSL captured-login-shell fence, moved to
src/shared/posix-stdout-fence.ts so both transports share it.

Finding: HH-1 (phase 3.7 hostile-host). Refs #8324.

* fix(ssh): fence the login-shell Node probe and fall back from a noisy SFTP handshake

The login-shell 'command -v node' probe ran unwrapped, so output from sshd's
shell and the nested login profile became the Node path. It now prints a
one-line fence every login shell parses, inside that shell.

An external sftp-server is started through the user's shell, so startup output
corrupts the SFTP handshake. ssh2 reports that parse failure only before the
session is ready, so SshConnection.sftp tags it, and relay install transfers
treat it as SFTP unavailable and stream over exec stdin.

Finding: HH-1 follow-up (phase 3.7 hostile-host). Refs #8324.

* fix(ssh): keep the stdout fence marker out of the command text

The runtime-store inventory lists processes with ps, which prints its own
script, so a literal marker in the script was read as the fence and the
store GC skipped every pass. printf now joins the marker from two halves.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): finish an abandoned update on connect, and refresh renderers after a main-side re-pair (P1-A, P1-C) (#26635)

* fix(orcad): finish an update or rollback its client abandoned, and tell renderers about main-side re-pairs

P1-A: a wake that finds the activation fence now runs crash recovery, which takes
only a stale, previous-boot or exited-own fence. Recovery of an activation
interrupted after the candidate launched resumes it: the candidate commits if it
passes the readiness gate, else the activation is undone as before.

P1-C: invalidating a still-linked environment's transport restarts its status
owner, and a renderer that sees status under a newer pairing revision re-reads
the catalog before applying it.

* fix(orcad): a resumed update keeps the app version that started it

Recovery that commits an abandoned candidate stamped no activeAppVersion, so an
older desktop read the host as pre-tracking and downgraded it over state the
newer build had migrated. The activate journal now records the initiating app
version; recovery stamps it, and a journal too old to carry it is undone instead
of committed.

* test(runtime-env): answer nothing for the status restart a re-pair now triggers

* fix(orcad): recovery leaves running an update server a too-old journal cannot commit

A journal without candidateAppVersion was sent to undo, which stopped a
healthy candidate that had already migrated state and then refused to
restart the incumbent, leaving the host serving nothing. Refuse instead,
keeping the fence and the candidate, until the operator accepts Restore.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(terminal): a remote recovery never splices stale history above the new screen (P2-5, P2-7) (#26647)

* fix(terminal): a remote recovery never splices stale history above the new screen

A pushed remote snapshot (overflow recovery, seq-gap resync, first attach) is
sent because the client's stream broke, so the pane's own history ends before
that image. Since #26319 a screen-only image kept that history, splicing a
stale block directly above the final screen with a silent gap of up to ~270k
lines (P2-5), and a first attach to an older host left silent holes (P2-7).

- Host: the ACK-overflow recovery serializes up to 1000 history rows.
- Client: the seq-gap resync asks for the same 1000 rows.
- Client: a pushed snapshot always replaces the pane's scrollback; the
  keep-local-scrollback path is removed. A screen-only image from an older host
  now shows only its screen, which is lossy but never misleading.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(terminal): a pushed image replaces the normal buffer over a live alt screen too

Over a pane already on the alternate screen, an image that also ended on alt
repainted only its alt frame and kept the pane's normal buffer, inferring the two
had frozen together. Inside the gap the TUI can exit, the shell print and a second
TUI start, so once that TUI exited the pane showed its stale history where the host
had the missed output. A pushed image now always rebuilds the normal buffer and
history from the host before entering alt itself.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(terminal): bound recovery history by link slack and keep pre-TUI scrollback under screen-only images

A recovery that carries history now goes out only when the stream's ACK
window has drained; under a sustained flood recoveries stay screen-only and
the owed history follows once the client catches up, so snapshot bytes no
longer outgrow the client's intake.

A pushed image without history (scrollbackRows absent or 0) over a live alt
screen repaints only the alt frame again, so a reconnect to an older host
no longer wipes the pane's pre-TUI scrollback.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(source-control): Stage All stages every change when the listing hit the 1,000 cap (HH-4) (#26618)

* fix(source-control): Stage All stages every change when the listing hit the 1,000 cap

The status listing stops at 1,000 rows, but Stage All built its path list
from those rows, so it staged 1,000 of 20,000 changes and the next commit
was silently partial. When the listing is capped, Stage All (and the
Changes header Stage all) now asks the host to stage the whole worktree
via an optional scope on git.bulkStage; unresolved conflicts stay
unstaged. Older hosts ignore the field and stage the listed paths.

Fixes HH-4.

* fix(source-control): whole-worktree Stage All needs a host receipt, skips shared links, and reaches mobile

- Scoped stage sends no paths and requires a stagedScope receipt, so an older
  runtime or relay stages nothing and the client reports it instead of a
  silent partial stage.
- Scoped 'all' skips Orca's shared symlinks (configured and really a link),
  matching the status listing.
- The capped-listing banner offers Stage All, reachable when the visible
  prefix is entirely staged.
- Mobile reads didHitLimit and uses the same scoped stage and receipt.

* test(source-control): type the Stage All test stubs so lint and the mobile test typecheck pass

* fix(source-control): match Shared Paths saved as ./name or name/ against git's spelling

The Shared Paths setting keeps entries as typed, but git prints worktree paths
without a leading ./ or trailing /. Stage All's conflict path compared the raw
spelling to git's untracked list and staged the shared node_modules link, and
an exclude pathspec ending in / made git refuse the add after tracked changes
were already staged. Status listing had the same mismatch. getSafeRelativePath
now returns git's spelling, so every caller gets it, including saved settings.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): redeploy a managed server another desktop stopped; forget one whose host is gone (P1-D, P1-E) (#26628)

* fix(ssh): redeploy a server another desktop stopped; let a dead managed host be forgotten

P1-D: a connect whose linked server the host reports as not activated (another desktop
stopped it) now goes on to the update check, which redeploys it in place.
P1-E: add Forget (managedServer.forget, orca environment forget, Settings row) that runs
Stop's local unlink without contacting the host; the verdict stays unverifiable.

* test(ssh): type the forget test's hoisted store without an assertion

* fix(ssh): never downgrade a stopped host; redeploy it mid-session and retry after a failure

A stop now keeps the stopped build's Orca version, so an older desktop's
reconnect skips as host-newer instead of starting an older server on a
store a newer build migrated. A stop recorded without that version is not
guessed at. A stopped host retries the redeploy on each connect instead of
the once-per-version failure suppression, and a runtime call that finds
the server stopped mid-session redeploys it before the call.

* fix(ssh): rebuild the managed forward after redeploying a stopped host

A redeploy can bind a different port than the reused forward targets.

* fix(ssh): keep stop journals readable by older desktops and recheck version policy under the fence

- The stop journal's recordAfter is the core deactivated record older peers compute; the
  committed record keeps activeAppVersion (which released planners already honour) and
  rolledBackFrom, rebuilt from recordBefore on recovery.
- Auto-update re-applies its version policy to the record read under the host fence, and a
  census counted against another incumbent is treated as unverifiable.

* fix(ssh): file a dispatched stop where older desktops cannot commit it, so their recovery never drops the version guard

* fix(ssh): refuse a fresh deploy of an older orcad onto a host a newer desktop stopped

The unlinked deploy path never consulted the stopped record's activeAppVersion, so a desktop
without a link to the host could activate its older build over state a newer build migrated.
Both the auto-update planner and the fresh deploy now apply one downgrade rule under the fence.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): redeploy a managed server a pre-version Stop left; let a forced deploy run over a newer one (#26766)

A stopped activation record without activeAppVersion is what every Stop on
main writes, so refusing it stranded those hosts behind an unforceable
policy refusal. Only a provably newer host is now refused; a forced fresh
deploy runs this version anyway, and the refusal names both versions.

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(remote-terminal): no keystroke lost, duplicated or split across a silent outage (P1-1, P1-2, P1-3) (#26630)

* fix(remote-terminal): no keystroke lost, duplicated or split across a silent outage

Remote pane input was fire-and-forget: keys handed to a socket that had
silently died were gone, keys still in the 8 ms debounce at detection were
cleared, and held keys were discarded when the auto-recovery window ran out.

- New negotiated `inputAck` capability: the client names an input session
  and sequences Input frames (existing frame header `seq`); the host records
  the last applied sequence per (PTY, session) across connections, writes
  each sequence once and acks it with new opcode InputAck (18), sent only
  to clients that advertised the capability.
- The pane keeps sent-but-unacked input and replays it to the same
  terminal on reattach; the host dedupes. Old hosts keep current behavior.
- Debounced and claim-queued input at detection is held, not cleared.
- Held input survives the 'disconnected' phase; the hold coalesces keys so
  minutes of typing do not hit the per-entry cap.
- Banner copy no longer claims a one-minute retry window.

Fixes P1-1, P1-2, P1-3 (phase 3.7 network findings).

* fix(remote-terminal): never replay into a restarted runtime; keep Ctrl+C and validating paste in order

- Host publishes inputLedgerId with inputAck; the client drops unacknowledged input sent to a
  different ledger (runtime restart with the same PTY) instead of running it twice.
- A replayed duplicate is acked only after the original write settles.
- Accepted input (Ctrl+C, Escape, paste) rides the sequenced stream and resolves on the host ack.
- Keys typed while held queue behind a paste still in size validation.

* fix(remote-terminal): parse subscribed capabilities into a typed shape instead of Reflect.get

* fix(remote-terminal): ack only applied input; withdraw writes their caller gave up on

- Host ledger records each write's outcome: a refused write is rolled back and
  never acked, so the client's replay writes it; nothing queued behind it runs or
  is acked. A write that threw is acked as delivery-unknown, so the client
  reports it failed and never replays it.
- Paste and file-drop timeouts abort their accepted write: a copy still held for
  an outage is withdrawn, and a journaled copy is left out of the replay.
- A batch of sequenced input keeps one endpoint and ledger, so a send that
  retires the stream mid-batch no longer discards earlier unacked input.

* fix(remote-terminal): host writes sequenced input strictly in order and asks the live stream to resend after a refusal

- The ledger admits only the next sequence; a refusal or gap answers with an InputAck resend request instead of WriteUnavailable, so the pane keeps its journal and resends on the same stream (no remount, no carrier bookkeeping).
- Sequenced input uses the provider's settled write; an unverifiable handoff is delivery-unknown, not refused.
- Replays fill given-up slots with empty input so the host can continue; the client remounts only after every resend backoff is refused.
- Typing batched ahead of a paste goes out as its own sequences, so cancelling the paste keeps it.

* refactor(remote-terminal): deliver sequenced input with standard go-back-N

Replace the hand-built "block on refusal" bookkeeping with go-back-N:

- Host: per (PTY, input session) the ledger keeps only appliedSeq, the
  highest contiguous confirmed write. Frames run one at a time in arrival
  order: a duplicate is re-acked, the next sequence is written and acked
  on confirmed success, and a refusal or gap is answered with a resend
  request carrying appliedSeq. No blocked state; every stream is equal.
  A settlement that proves no byte left is a refusal; one that may have
  written advances as delivery-unknown and is never retried.
- Client: one journal keeps unacked input in order and resends from
  appliedSeq + 1 on a resend request (with backoff), on a new stream, or
  when acks stall. Refusals never remount the pane. Withdrawn pastes,
  stale query replies and over-budget bytes resend as empty slots.
- The recovery hold pauses (keeps the rest) when a release is cut short
  by another outage, and keeps delivering typing after an accepted write
  a sequenced stream withdrew or could not confirm.
- Add a seeded model-based test that drives the real pane transport,
  multiplexer and journal against the real host dispatcher and ledger
  through 2,000 random fault schedules.

* test(remote-terminal): cover the refusal remount on a stream without sequenced input

A sequenced stream now resends refused input on the same stream instead of remounting, so the end-to-end remount check negotiates no inputAck, as an older peer does.

* fix(remote-terminal): escalate persistent input refusals, drop input once recovery gives up, and never report a lost unknown ack as applied

- The pane falls back to its remount path when the host keeps refusing input past the resend backoff ladder.
- Held and journaled input is discarded when auto-recovery gives up, and input typed under the disconnected banner is not held.
- The host ledger answers a duplicate for its own sequence and remembers delivery-unknown outcomes; the client settles a write as applied only from its own ack.
- The input model now requires a write reported applied to have reached the PTY whole, and covers a refusal that never clears.

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

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: Claude <noreply@anthropic.com>

* fix(remote-terminal): keep input typed past the auto-recovery window for the same terminal, within a bounded grace (P1-3) (#26832)

The last round of #26630 discarded held and journaled input as soon as auto-recovery gave up,
and refused input under the disconnected banner. On a real host a 200 s outage lost 102 of
120 keystrokes, although the pane reattached the same terminal on its own seconds later.

Input now stays held for the same handle and incarnation while the pane is disconnected, for
up to 5 minutes after it gave up. Past that grace it is discarded, and typing into the dead
pane is refused, so stale keys never run at an arbitrary later reconnect. The grace is a
timestamp checked when input moves, so a latched pane stays free of timers.

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

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-10-09 04:58:48 -07:00
Neilandm4air 9be39b6eb7 Restore remote explorer updates after server recovery (#26612)
* Restore remote explorer updates through the recovered app watcher

* Keep the remote Explorer watched when it is shown as a fallback tab

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-09 04:44:14 -07:00
NeilandOrcaWin 4845ae8716 Restore editor file watching when remote host contact returns (#26606)
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
2026-10-09 04:22:54 -07:00
3bcb312043 Keep managed servers unverifiable until they answer (#26637)
* Require a server reply before treating a live managed process as serving

* Log the unanswered live managed server like the other unverifiable verdicts

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
2026-10-09 04:22:17 -07:00
c12c211804 Preserve retained desktop browser pages during SSH conversion (#26572)
* Preserve desktop browser page placement when SSH workspaces convert

* Place the browser-service-status page on the managed host explicitly

Converted SSH workspaces now keep retained desktop pages local, so the
spec opens its remote-placed page on the managed environment instead of
relying on the retained page moving there.

* test(e2e): refocus the managed browser page before retrying its Reconnect

The managed host can seed a terminal for the converted workspace and the
client mirrors it with focus, hiding the browser notice mid-test.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
2026-10-09 04:22:13 -07:00
OrcaWinandm4air 049bf808ab test(e2e): keep two host sections in the catalog-retry reconnect test (#26838)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-09 03:50:06 -07:00
Neil 1578716eb0 revert(runtime): temporarily restore the previous host Node minimum (#26823)
This reverts commit 46460d49c7 (#26171).

Pause the Node 24 requirement so existing SSH and WSL users are not
surprised by a host-runtime upgrade requirement in the next release.
This is a precautionary rollout hold requested to avoid near-term user
disruption, not a cancellation of the Node 24 transition.

Restore the prior Node 18 host minimum and companion build targets.
The previously shipped bundled Node 24 runtime and compatibility launcher
remain in place. Preserve the original change in a separate draft PR for
reintroduction after compatibility and rollout validation.
2026-10-09 02:00:57 -07:00
Neil df922d3709 Keep one local project identity after concurrent registration (#26789) 2026-10-09 00:57:04 -07:00
Brennan Benson c586cce8cc fix(acp): start chats for agents that report no model (#26587)
* fix(acp): start chats for agents that report no model

* fix(acp): omit unknown models from resting session reads

* test(acp): expect absent models for resting Claude chats

* refactor(native-chat): move known agent discovery out of session host
2026-10-09 00:42:23 -07:00
Neilandm4air bfd9858242 Retry failed catalog reads when SSH hosts move to Remote (#26531)
* fix(remote): retry failed SSH conversion catalog loads

* fix(remote): keep the loaded catalog across ordinary disconnects

* fix(remote): replay a reconnect that lands while a failed catalog read is pending

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-09 00:38:14 -07:00
Neilandm4air f01bd93748 Keep managed workspace paths out of desktop editor launches (#26668)
* Guard desktop open actions with the target workspace owner

* Wait for the folder catalog only in the folder menu exercise

* Resolve the row owner only while its Open in menu is open

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-08 23:49:11 -07:00
Neil 46460d49c7 feat(runtime): require Node 24 for servers and remote hosts (#26171)
* feat(runtime): default to bundled Node 24 and require modern hosts

* Update SSH deployment expectations for Node 24 guidance

* Expect Node 24 in Windows runtime installation guidance
2026-10-08 23:35:11 -07:00
Neil f9d0aa2f67 Refresh open Markdown links when workspace files change (#26599)
* Refresh open Markdown document links after file changes

* Use the stable Markdown refresh callback in the watcher effect
2026-10-08 23:23:14 -07:00
Neil d2d2d39cca Refresh rich Markdown links when document metadata changes (#26595)
* Refresh rich Markdown links when document metadata changes

* Type Markdown link storage for metadata refresh tests
2026-10-08 23:02:59 -07:00
Neil 45a969f83f Wait for retained editor ownership before listing Markdown documents (#26590) 2026-10-08 22:45:11 -07:00
Neilandm4air 6a15cd8972 Preserve editor file identity during SSH host conversion (#26593)
* Preserve editor file identity during SSH host conversion

* Keep a dirty in-session editor tab on its file record during SSH conversion

A file opened over the relay in the current session carries route provenance,
so the no-host-evidence check repointed its tab to a fresh clean record and
hid the unsaved draft. Keep any retained record with no runtime owner instead.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-08 22:18:51 -07:00
Neil b9a7830036 Keep new browser tabs on SSH routing after managed conversion (#26570)
* Keep local browser routing on the deployment SSH host after conversion

* Seed published SSH connection state in browser lifecycle fixture
2026-10-08 21:30:46 -07:00
Neilandm4air 8167ddaa7f Distinguish remote browser failures from server connection failures (#26573)
* Distinguish remote browser service failures from server outages

* Use the shared browser_unavailable code in the stream failure classifier

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-08 21:07:48 -07:00
Neil 1cda323b6a Report browser placement support from the installed provider (#26559)
* Report browser placement capabilities from the installed provider

* Run managed browser capability checks in the template-building CI lane
2026-10-08 20:51:50 -07:00
Neilandm4air bdf0d66d7c Provide Orca CLI on managed Linux and macOS servers (#26539)
* fix(orcad): provide execution host CLI in Unix managed terminals

* fix(orcad): keep the server starting when the CLI launcher cannot be written

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-08 20:50:37 -07:00
Neil 978c32bfe6 fix(remote): group migrated folders under their execution host (#26535) 2026-10-08 20:22:01 -07:00
Jinwoo Hong 7f0e067256 test(e2e): layout oracle scenarios with window, headless serve and SSH (#26674)
* test(terminal): check the runtime's workspace layout against structural rules

A pure rules check over the runtime's session partitions: a terminal in two
panes, a pane in two tabs, a tab outside exactly one group, tab rows and tab
bar disagreeing, two tab orders, and ids changing for the same pane, tab or
group across an update. Wired into the seeded layout model test after every
step, plus late-spawn cases (one known on main, kept as an expected failure).

* test(e2e): layout oracle scenarios with window, headless serve and SSH

Drives layout journeys through window gestures, CLI and paired-client runtime
commands, and after every step checks the runtime's layout against the
structural rules, the window's DOM, the paired-client and CLI views, the
expected end layout, restarts (relaunch, cold daemon, in-app daemon restart)
and per-pane terminal markers. Findings main still has are listed with their
cause; anything else fails. Includes headless orcad/Electron serve scenarios
for the serve issues and a Docker SSH scenario.

* test(terminal): check layout rules on every write and race client commands with the window

The layout model now checks the structural rules on every in-memory
workspace-session write, not only at each step's end, naming the writer
of any breach. Three documented two-writer transients (main mints a tab
row before the window writes its tab bar) are allow-listed by rule, op
and writer; they must still heal by the step's end.

New model ops race a stale window save against a paired client's tab
close, a host-created split, and main's PTY-exit retirement. A new file
runs main's updateMobileSessionPaneLayout (old phone pane trees) against
the real Store; the case with no topology revision is an it.fails: main
drops a live pane and its binding there.

* test(e2e): oracle expected outcomes, transient sampling, remount counts, more scenarios, latency

Every step checks the expected panes, which panes went away and that the rest
kept their terminals; a 100 ms sampler runs the rules between steps; xterm
creations are counted per pane and an unexpected recreation fails. Adds host
navigation, window reload (graph republication), quit right after a change
(quit-time save), an old client's stale pane tree, and a latency measurement
(command to drawn update, idle and under an output flood; no pass/fail).

* docs(e2e): describe the oracle's added checks and latency measurement

* test(e2e): #12723 SSH folder scenario, CI lane for the SSH oracle spec, short socket dir

Adds an SSH folder-workspace (no repo) scenario that checks the layout lives in
one partition across relaunch; registers the SSH oracle spec in the Docker SSH
lane's selection lists; headless hosts put long-TMPDIR profiles under ~/.orca-lo
instead of /tmp.

* ci(e2e): run the headless layout oracle in the job that builds orcad; Linux relaunch findings

The headless oracle spec needs out/orcad, which only the orca serve mode-switch
job builds, so it runs there (and fails rather than skips in CI if orcad is
missing). Records what the first Linux CI run found on main: after a relaunch
main can hold tab rows with no tab bar past the settle wait, and Electron serve
on Linux also lists a renamed terminal's title as null after a restart.

* test(e2e): join partition lists in the SSH folder scenario's log lines

* test(e2e): retry the latency spec's reads through a brief terminal-ownership gap

Linux CI saw terminal.list answer terminal_surface_ownership_unavailable while
the window re-published its terminals; reads retry for up to 10 s, commands do not.
2026-10-08 21:18:21 -04:00
Brennan Benson bb44f74d00 Keep native chat /clear in the same conversation (#26579)
* Keep clear in the same chat and reset its provider context

* Derive clear pauses only from journal membership and cover older rewind clients

* Point queued command rewind test at the structured hook

* Group queue consume errors and rewind replay assertions
2026-10-08 16:00:29 -07:00
Brennan Benson 22974c5809 Default experimental native chat to structured chat (#22933)
* refactor(native-chat): make structured chat the only Chat UI runtime

Retire the experimentalStructuredNativeChat opt-in. Chat UI on is now the
one gate for structured chat: new agent tabs that default to Chat UI start a
structured session wherever the launch is supported, and unsupported
launches keep falling back to terminal-backed chat.

- Shared isNativeChatEnabled predicate replaces prefersStructuredNativeChatByDefault;
  host admission, renderer routing, orchestration mode, startup projection,
  tab sync and the resume offer all read it.
- The retired key is stripped on load and on save, dropped from the settings
  type, defaults, the published client-settings projection and telemetry.
- Settings no longer offer the structured toggle; resume-after-restart and
  shell-environment rows show under the Chat UI default view.
- Startup structured-tab projection moves beside its sync hook so both gates
  read the same predicate.

* docs(native-chat): describe structured chat as the Chat UI runtime

New local Claude and Codex chats no longer sit on a PTY, so the page stops saying the terminal is always the source of truth and scopes the chat/terminal toggle to terminal-backed chats.

* fix(native-chat): stop promising a Windows fallback in the Chat UI scope note

A host refusal after the route picks structured chat still ends as a failed launch.

* fix(native-chat): keep open structured chats reachable when Chat UI is off

Turning Chat UI off hid open Claude/Codex chat tabs while their provider
processes kept running, because the host and the desktop mirror gated
serving existing sessions on the same setting that decides how new
launches open.

The host now asks two separate questions. Creating a new structured
session (agentSession.create, createSupport, ensure) needs the negotiated
capability plus the Chat UI setting. Serving a session that already
exists (listing, reading, sending, resuming, closing, and the mobile
projection and restore) needs the capability only. The desktop mirror,
startup restore and resume-on-restart offer no longer read the setting,
and the toggle-off teardown and the mobile re-publish on setting change
are removed as dead.

* fix(native-chat): run the local structured tab sync only where chats may exist

The previous commit started the every-worktree session-tab census and
subscription, the startup restore, and the resume-offer host read for every
desktop user. Gate them on a derived fact instead: Chat UI is on, or the
local host holds at least one structured session record. With Chat UI off
nothing can create a session, so the answer can only go from yes to no.

The host answers from its installed record store, or from whether the store
file exists when the host is not installed, so a machine that never had a
structured chat pays one existence check and never starts the host. Chat UI
turning on starts the sync; turning it off never stops it.

* fix(native-chat): answer an empty restart offer without starting the chat host

With every Chat UI user now on structured chat, the launch-time resume offer read built the host even on machines with no stored chat, which on Linux scans every process environment. No store on disk means no offer, so the listing answers empty without installing the host.

* fix(native-chat): release the structured tab sync when it never subscribed

A failed capability probe reads as unsupported, so the sync returned without subscribing while still holding the one-sync slot. Nothing could start another, and chat tabs stopped live-updating until relaunch. The start now reports whether it subscribed, and a sync that did not releases the slot so the next Chat UI change retries.

* fix(native-chat): let the host answer the restart offer whatever Chat UI says

The renderer asked main whether a structured store existed before reading the restart offer, so it would not start the chat host for users without one. The host now answers an empty offer without starting itself, which made that probe an extra round trip. In paired web it was also wrong: the probe is hard-coded to no and the gate read the browser's own Chat UI setting, so a paired host's chats were never offered back once that was off.

* refactor(native-chat): drop the structured tab sync generation fence

The fence existed so a Chat UI toggle-off could retire in-flight inventory replies before it cleared the mirror. That teardown is gone: turning Chat UI off keeps open chats, and only a test reset still bumped the generation, so every production check compared equal. The sync's own disposal still fences its subscription, and the startup restore latch and publication cursors stay.

* fix(native-chat): retry the structured tab sync when the capability probe fails

A failed runtime status probe maps to an empty capability list, which the sync read as "unsupported" and gave up on. The release only let a later Chat UI change try again, so one transient startup failure left chat tabs without live updates for the whole session. The sync now tells an unknown answer apart from a host that answered without the surface, re-asks unknown answers on its existing subscribe backoff, and stops only on a definite no.

* refactor(native-chat): begin single-switch structured launch routing

* Make Chat UI the single structured launch default

* Remove retired chat settings copy from localization catalogs

* Keep locale checks aligned with the single Chat UI switch

* Keep explicit chats accessible and older client defaults terminal

* Align newly landed Settings tests with the Chat UI switch

* Expect terminal fallback on older paired structured hosts

* Correct terminal fallback launch expectation

* Update tests that still expected the retired chat settings

* Keep chat composer options off terminal launches and restore route coverage

Chat composer model and effort no longer become CLI flags on terminal
launches, since a new launch can no longer open in the terminal chat
view; structured launches seed their own options. Restores dropped
route coverage, including that an explicit resume-in-new-chat request
stays available while Chat UI is off.

* Replace the stale remembered-options test with terminal fallback coverage

The test expected chat composer model and effort on a codex launch with
Chat UI on, but its mocked host has no structured capability, so that
launch is the terminal fallback that must not carry them. It now checks
the fallback with Chat UI on and off, with shipped and stored arguments;
structured creates are covered where the seed is applied.

* Lower the terminal pane listener budget to the measured 15

The explicit-chat change retired the per-pane Chat UI setting read, so
each mounted pane opens one fewer store listener. Pin the budget to the
measured count and count that read among the saved listeners.
2026-10-08 11:46:55 -07:00
Jinjing d997ef06f5 Warn on slow profile saves instead of failing them (#26447)
* Stop timing out slow profile-state writes; warn instead

Profile-state writes used to fault after 30 seconds. A stalled main loop or a slow disk could then drop the writer and show a "could not confirm your save" dialog, even when the write was still in progress. Slow requests now record a diagnostic breadcrumb and keep waiting for the worker's reply. Close waits for the worker to actually exit, and a termination request alone no longer counts as proof.

Pane-alias and migration-listener changes now save only their own sections, not a full state rebuild. Quit teardown also keeps the profile admission held until the final flush finishes, so a pending write is no longer released early at the deadline.

* fix tests

* Warn when profile saves run slow

Slow profile-state writes used to leave only a diagnostic log entry, so users had no way to tell that recent changes were not yet confirmed saved. The writer now reports when a save write passes the slow-warning threshold and reports recovery when the reply arrives. Writes are never failed or retried because of the delay.

- Main keeps the current delayed/healthy status and publishes changes to the renderer, so a reopened window gets the correct state.
- The renderer shows one persistent warning toast while a save is delayed and dismisses it on recovery.
- Observer errors are logged and do not interrupt saving.
- The web build reports saving as healthy because it does not use the desktop profile writer.

* Fix save-delay toast dismissal racing with a re-shown warning

The save-delay warning used one fixed toast ID. Sonner applies a dismissal
a frame late, so a dismissal could remove a warning that had just been
shown again with the same ID. The hook now tracks the ID of each delayed-save
warning it shows and dismisses only that toast. A new delay gets a fresh ID.

Add Sonner-level tests for remount, StrictMode cleanup, and recover-then-
delay races. Harden the e2e spec with a paused clock and a retry around
main-process evaluate.

* Report slow profile revision checks and exports as save delays

Revision checks and JSON exports run on the same profile-state worker as
saves, so a slow one holds up later writes just like a slow write does.
The save-delay warning only tracked write-* commands, so those stalls
went unreported. Every command except initialize and close now raises
the warning.

Tests cover the export-latest path and assert the delay signal fires
once with true before clearing.

* Clarify slow-save toast to name profile storage

The delayed-save warning said recent changes were not yet confirmed saved, which overstated the risk while Orca was still retrying. The real condition is a slow profile-state write, so the toast now names profile storage and says later saves may wait until that operation finishes. The copy is updated in every locale, and the unit and e2e tests that assert it are updated to match.
2026-10-08 11:01:04 -07:00
e3639ef8d6 Phase 3.5: fix the top SSH/remote issues at the root cause (#26483)
* fix(remote-runtime): stop arm64 Linux servers dropping clients every heartbeat (empty WebSocket write EFAULT) (#26200)

* fix(remote-runtime): never write an empty WebSocket payload on its own (arm64 Electron EFAULT)

Patch ws@8.22.0 Sender.sendFrame so a frame with an empty payload is written
as its header alone. Under Electron on arm64 kernels with 39-bit virtual
addresses, socket.write() of a zero-length buffer fails with EFAULT and kills
the connection, so every empty heartbeat ping, auto-pong and close dropped
the remote runtime with 1006. Wire bytes are unchanged.

* test(remote-runtime): run the ws empty-payload guard on real Node

Bun resolves 'ws' to its built-in client, so under the Bun test project the
guard exercised Bun's implementation instead of the patched package.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): ship the session scanner child so agent session history lists on orcad hosts (#26309)

orcad built every forked child except session-scanner-service-entry.js, so
aiVault.listSessions always failed on an orcad host. The resolver also skipped
the adjacent entry whenever isPackaged was true, so shipping the file alone
would not have been enough.

- build every ORCAD_CHILD_ENTRY_POINTS entry from one loop and add the scanner
- add it to ORCAD_ARTIFACTS so a deploy missing it fails integrity checks
- share one forked-child path resolver (asar layout vs flat app root) between
  the watcher and the scanner
- smoke-fork the built scanner during build-orcad under both runtimes

Fixes #24105

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(remote-runtime): one owner for a remote pane's binding and recovery (#26308)

A remote pane no longer latches in connecting on terminal_not_found, holds
keystrokes typed during same-terminal recovery instead of dropping them,
reports transport-owned retries so the pane is not remounted mid-retry, and
no longer infers absence from an inventory that predates the leaf.

Co-authored-by: m4air <m4air@Mac.localdomain>

* ci(e2e): move apt off the Azure mirror on runner images that use a mirror list (#26335)

Ubuntu 24.04 runner images resolve apt sources through /etc/apt/apt-mirrors.txt, so rewriting only the source lists left package downloads on azure.archive.ubuntu.com, which intermittently fails ('Ign:' on every package). Rewrite the mirror list too, and retry the install once with --fix-missing.

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(terminal): remote snapshot replay keeps scrollback it cannot replace (#26319)

Desktop subscribers of a remote terminal received screen-only snapshots,
and every pushed snapshot replay cleared the pane's scrollback with ESC[3J.
A reconnect, a remount, or a recovery after a dropped frame therefore wiped
history the host never resent.

- Host: desktop subscribe snapshots use the same scrollback-row ladder as
  mobile, and SnapshotStart now carries an optional scrollbackRows field.
- Client: a pushed snapshot clears scrollback only when the host says it
  carries history; screen-only images (scrollbackRows 0, or older hosts
  that omit the field) repaint the screen and keep the pane's history.

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(daemon): clean launch env, validate client frames, fit the socket path (#26306)

* fix(daemon): clean launch env, validate client frames, fit the socket path

- Drop Chromium's DBUS_SESSION_BUS_ADDRESS=disabled: marker from the daemon,
  its systemd-run scope launch and every PTY env (#25580, #21119).
- Use the durable user-manager scope only when that manager outlives the
  caller: lingering on, or the caller already runs inside it (#24201).
- Shape-check hello and request frames, and catch request rejections, so a
  malformed frame closes one connection instead of the daemon (#17841).
- Move the daemon socket to a short per-uid private dir when the data root
  would overflow sun_path (#17840).

* fix(daemon): validate a relocated socket dir before any connect, not just before bind

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(ssh): keep crediting a PTY after its recovery delivery is canceled (#26341)

A canceled recovery delivery released its source spans while the renderer
and model could still settle them; the stale projection then threw on every
later ACK, so the next delivery was never credited and the relay paused the
PTY for good. Settling or transferring a span of a closed delivery is now a
no-op.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(rpc): deny-by-default caller permissions and a scoped SSH bridge credential (#26337)

* fix(rpc): deny-by-default caller permissions and a scoped SSH bridge credential

Every RPC method now declares a required permission, and RpcDispatcher checks it
against the caller's scope on every transport (owner, mobile, paired runtime,
SSH bridge). The SSH relay bridge no longer spawns the client CLI with owner
authority: each invocation gets a short-lived credential bound to its SSH target,
revoked when the child exits. Without the new per-host opt-in, that credential
reaches only its own host's terminals; with it, workspace methods but never
desktop control, accounts, settings writes, skill install/share, pairing or host
admin. Paired runtime clients need a pairing-time grant (orca serve
--grant-desktop-control) for computer.* actions.

Fixes #23835
Fixes #25259
Refs #18269

* fix(rpc): let a bridged SSH worker report to its own coordinator; allowlist paired-runtime permissions

Without the per-host opt-in, orchestration send/ask/reply/check/inbox now pass when the caller is a
terminal on the bridged host, binding pane key, payload Dispatch, run and recipient to that caller's
own orchestration (recipient off-host only as the creator of its active Dispatch).

Paired runtime clients get an explicit standard permission set; pairing-admin and desktop-control
are grant-only.

* fix(rpc): resolve a bridged SSH caller's own Run and Dispatch mailboxes before admission

Mail routing stores a worker's follow-ups under dispatch:<id> and worker
questions under run:<id>, so comparing the original recipient to the caller's
terminal handle refused an SSH worker replying to its coordinator, an SSH
coordinator answering its own Run's question, and explicit sends to the
caller's own run:/dispatch: address. The bridge now resolves the exact
mailboxes the caller's live pane and process read (as check does) and admits
replies and recipients against those.

* refactor(rpc): parse SSH bridge selectors with a typed schema instead of Reflect.get

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(projects): remove a project only on the host the user picked (#26340)

* fix(projects): remove a project only on the host the user picked

removeProject now requires a hostId and matches that host's row exactly.
The bare-id fallbacks (unique candidate, focused host) could resolve to
another host's live project and delete it. Orphaned SSH purge and group
deletion now pass each row's own host.

Fixes #13071

* fix(projects): group delete removes only the group's member rows

Same-id rows on other hosts outside the group were removed because membership
was rebuilt from bare ids after the cascade cleared it. Capture (id, host)
member pairs before deletion and remove only those.

* fix(projects): never let a stale catalog widen a project removal to every host

The renderer chose id-only `repos:remove` when its own catalog showed no
same-id row on another host. A stale catalog (another window, a paired
client, or a pending refresh) hid that row, and main then deleted every
host's registration and saved workspace metadata.

- The local catalog route now always calls `repos:removeForHost` with the
  owner row's host, for local, direct SSH, git and folder projects.
- `Store.removeProject` refuses an id that rows on more than one host share,
  so no id-only caller can delete another host's row.
- Setup rollbacks remove the just-created row by its host.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(serve): load SSH targets on a headless serve; a user Quit only closes serve-hosted windows (#26348)

* fix(serve): load SSH targets on a headless serve; a user Quit only closes serve-hosted windows

- launchServeMode registers the SSH handler layer before RPC binds, so a windowless
  'orca serve' lists and connects the profile's SSH targets (#25886, #8489).
- Under serve, the app-menu Quit/Exit, tray Quit and renderer-recovery Quit close the desktop
  windows instead of quitting the process (#15537). Signals, supervisor stops, relaunches and
  update installs still quit normally; on macOS a signal reaches before-quit the same way
  Cmd-Q does, so the guard lives on the user commands, not in before-quit.

* fix(serve): SSH profiles serve on Electron; native macOS quits and frozen windows no longer trap or stop serve

- orca serve: a profile with saved SSH targets stays on the Electron host by default, since
  orcad has no SSH stack yet (#25886, #8489). ORCA_SERVE_RUNTIME=orcad still forces orcad.
- macOS serve: Dock > Quit, app-switcher Cmd-Q and other native terminates only close the
  desktop windows (#15537). Signals, supervisor stops, relaunches, update installs and
  logout/shutdown still quit.
- The serve signal listeners now reclaim SIGINT/SIGTERM/SIGHUP from Electron's native handler.
  Any signal that had a Node listener before app ready was left with Electron's handler, so
  these listeners never ran.
- A serve user Quit closing a frozen renderer's window arms the same 10s ack deadline as a
  desktop quit, and skips minimize-to-tray.

* fix(serve): stay on Electron when the profile's SSH targets cannot be read yet

A valid older-schema profile is rejected by the read-only probe, and the
selector took that as "no SSH", so a default serve before the first
migration started orcad with an empty SSH registry. Fail closed instead.

* refactor(window): type user-Quit close marks as a named window slice, not object

* test(serve): pass the desktop-control grant default in the headless SSH launch fixture

* test(serve): run the serve-selection SQLite fixtures in the Node runtime project

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(terminal): keep the main-process terminal tail linear under TUI redraw floods (#26316)

* fix(terminal): keep the main-process terminal tail linear under TUI redraw floods

Full-width in-place row rewrites rebuilt the row string per character
(O(width^2)), a long chunk past the line cap spliced the row array once
per newline (O(lines x cap)), and multi-frame chunks summed every
cursor-up so they fell back to the whole-tail path. Rows now hold
mutable cells joined once, capped rows are dropped in batches, and the
redraw window uses the net cursor reach.

Fixes #11315

* fix(terminal): do not rebuild a redraw row the cursor only revisits

Rows stayed cell arrays once touched and re-joined on every newline, so a
cursor revisiting one wide row thousands of times cost O(width) per
newline. Runs now splice into the row string, a row switches to cells
only after 32 edits between snapshots, and cells are joined once and
dropped.

* fix(terminal): keep one cached row per chunk so unchanged redraw revisits cost O(1)

A row now keeps its cells, right-trimmed snapshot and content end across
newlines and only invalidates them on a real content change; identical
writes leave it clean. Adds a work-accounting matrix over restore, the
append path and live onPtyData, plus a per-character row oracle fuzz.

* fix(terminal): return redraw rows to flat strings with cached snapshots

Cells made every changed snapshot a full-width join, which was far slower
than the old flat-string splice. Rows are flat strings again, written one
run at a time; each keeps a cached right-trimmed snapshot and content end
that only a real change invalidates, so identical rewrites and cursor-only
revisits stay O(1). A wall-time test now times every review probe against
a vendored copy of the old row model on restore, append and onPtyData.

* test(terminal): read runtime leaf and PTY tail through typed members

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(ssh): relay fallback runs Orca's pinned Node by default, never a host npm install (#26303)

* fix(ssh): relay fallback runs the pinned Node ladder by default, never a host npm install

A host whose connect falls back to the relay route now gets the A/B/C ladder
(Orca's uploaded Node and prebuilt addons) unless it opts into Host Node. A
refused rung C steps to rung D with an actionable reason instead of compiling
native deps on the host.

* fix(ssh): mark the opt-in Host Node relay runtime as unsupported on the SSH host card

The relay fallback now defaults to Orca's pinned Node ladder; the host-npm path is
reachable only by an explicit per-host Runtime or ORCA_SSH_REMOTE_RUNTIME override.
When a relay actually launches on it, main publishes hostNodeRuntime on the
connection state (optional, admitted field-by-field for paired clients) and the
SSH host card shows an unsupported-configuration warning.

* fix(ssh): keep the relay runtime ladder honest about what it cannot verify

- NixOS's stub loader refusing Orca's generic Linux Node (exit 127, 'Could not
  start dynamically linked executable') is classified wrong_libc, so the ladder
  steps on to the host Node instead of failing the connect at rung A.
- In plain SSH mode the shell provider answers absent only for shells it saw
  exit; a PTY id it never minted (one an earlier relay may still run) is
  unverifiable, never exited.
- The rung D message offers Host Node only when the host's own Node refused the
  prebuilt addons, and calls it an unsupported configuration; a download,
  unresolved-host or Windows refusal gets no Host Node advice.

* fix(ssh): keep the host-Node relay where the ladder can't judge the host

A client without Orca's runtime artifacts (a dev, e2e or source build with no
orcad template, or an offline client) refused rungs A and C with
artifacts_unavailable, and the connect settled on rung D. That reason describes
the client, so it now lands on the host-Node relay it had before the ladder,
marked unsupported, and persists no host decision. A proved noexec still lands
on D.

Windows hosts have no rung B or C yet, so any rung A refusal there keeps the
host-Node relay too. The rung D message is worded from the host OS: Windows
gets no noexec-mount wording and is offered Host Node.

* fix(ssh): a host-Node fallback with no Node settles rung D, and mutable refusals expire

The host-Node relay the ladder chooses in place of D (a client artifact gap,
or a Windows rung A refusal) hard-failed the connect when the host had no
Node: the resolver threw a plain Error, which plain SSH mode does not admit.
The fallback now proves host Node with a strict probe first; an answered
"no Node" settles D with a classified, host-OS worded reason, and an
unanswered probe stays a retryable failure. A gap no longer rewrites D to
host Node once rung C proved host_node_missing.

Mutable pinned-runtime refusals (missing_lib, security_software, and noexec
on Windows) are no longer persisted and replay from memory for ten minutes
only, so a repaired host gets rung A back. The in-memory refusal cache moves
to ssh-relay-pinned-refusal-cache.ts.

* fix(ssh): no host does worse than the pre-ladder host-Node default

Restructure the relay runtime ladder around one invariant: A, B and C are tried
first, and any refusal past them falls back to exactly the pre-ladder default,
the host-Node relay marked unsupported. That covers an unidentified libc, a
client artifact gap, Windows, mutable refusals and a C N-API floor the npm build
would not have hit. Rung D is reached only when that fallback proves the host
has no usable Node, or a proved noexec home would defeat it too.

Strict host-Node resolution now treats only a command that ran and exited as an
answer, so a refused channel (MaxSessions), session limit or timeout stays a
retryable failure instead of a 'no Node' verdict. The launch reuses the Node the
fallback probe found.

Mutable refusals (missing_lib, security_software, Windows noexec) persist again
with their proof time and replay for 24 hours, so a host that still blocks
Orca's runtime is not re-uploaded to on every connect, while a repaired one gets
rung A back. A table-driven test asserts the invariant per host class.

* fix(ssh): every answered rung failure reaches the host-Node default, and D only past it

Two shortcuts skipped the pre-ladder default. An answered but unclassified
install failure (tar ENOSPC, an extract or hash marker, an unverified Windows
promote, a self-test that failed outside every refusal class) aborted the
connect on every attempt; it now steps down as a non-persisted install_failed.
Only unanswered failures (abort, timeout, channel-open, session limit, an
unconfirmed termination, SFTP connection loss, ssh exit 255) stay retryable,
and nothing steps down once a relay launch was issued.

A proved noexec no longer jumps to D: exec denial can be per binary (AppArmor
allowing /usr/bin/node), so it skips B and C, which load from the same tree,
and reaches the host-Node fallback. D now follows only that fallback answering
with a failure, so its outcome is a superset of the default's by construction.

The never-worse test is now the cross-product of every A, B, C and host-Node
answer on Linux and Windows, plus named review cases.

* fix(ssh): system-ssh upload failures and a broken cached runtime step down before launch

System-ssh directory upload and system sftp threw plain Errors that dropped the
host's exit status, so a remote tar ENOSPC or a Windows sftp permission refusal
aborted the connect instead of stepping down. The remote side now throws
SystemSshCommandExitError (ssh exit 255, signals and lost-connection
diagnostics stay retryable), and a local tar failure is still blamed on the
client rather than the remote tar that saw its truncated stream.

The warm runtime check only tested that the verified executable existed, so a
cached runtime that the host's exec policy or a removed library had since broken
was launched and hung on every reconnect. The same command now also runs it and
reports a refusal the way promotion does, so it steps down before any launch,
with no extra round trip.

* fix(ssh): keep the remote answer when local tar only failed writing to it

When the host's tar exits early (ENOSPC, a refusal), BSD tar on macOS exits 1
with 'Write error' and no signal, so the upload rethrew the local error and
dropped the typed remote exit, aborting the connect. The remote answer now wins
whenever local tar finished, was signalled, failed after ssh closed, or reported
a write or pipe error; a local tar that failed on its own input first is still
the client's fault.

* test(ssh): hostile-host cells allow read-only version probes, never installs; probe each Node once

The noexec-home cell failed on 'npm --version': under the never-worse ladder a
proved noexec reaches the strict host-Node fallback, whose probe asks npm its
version before settling D. The cells now judge install and compile calls (npm
install/ci, node-gyp, cc, make) and allow '<tool> --version' / '-v'.

The probe ran twice because the login shell named the Node the path probe had
already rejected. Candidate checks are now memoized per resolution, for the
host-Node fallback and rung C alike.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* chore(i18n): regenerate the runtime-required English catalog after the main merge

* fix(serve): serve on the profile the CLI selected, and refuse any other before the instance lock (#26424)

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(remote-runtime): open terminal tabs reattach after a managed server update re-pairs the host (#26433)

* fix(remote-runtime): a remote pane rebinds after a server update re-pairs its host

A pane captured the host's pairing revision once, when it mounted. A forced
managed-server update re-pairs the host under the same environment id, so every
later request from that pane was refused as stale, and the refusal counted as
fatal: the pane stopped retrying and Reconnect replayed the same stale revision.
Each request now carries the current revision, a pairing refusal re-reads the
catalog before the retry, and the pane treats it like lost contact.

* fix(remote-runtime): close a stream left on the old pairing and ignore a 0x0 grid hold

A stream that reopened before main re-paired stayed open while sendFrame refused
its stale revision, so typed keys vanished. A revision change now closes it as
recoverable so the pane rebinds on the new pairing. A just-restarted server also
published a 0x0 hold that shrank the visible pane to one cell; holds with no grid
are ignored.

* fix(remote-runtime): a pane follows only its own machine's re-pair

A re-pair under the same environment id to a different host key published its
revision before the catalog retired the old tabs, and the pane recovered at
the new revision, so its next call was authenticated as the replacement peer.
The catalog now classifies each re-pair before publishing revisions: a
replaced or removed machine's terminal streams end for good first, and a pane
only advances its pairing along a proven same-host rotation (same registration
and host key). An unresolved identity holds the pane until it is proven.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* refactor(rpc-permissions): Phase 3.5 elegance pass (#26471)

* refactor(rpc-permissions): drop never-read incarnation and invocation id from bridge credentials

* refactor(rpc-permissions): define runtime device grants once and reuse bridge scope fixtures

* refactor(rpc-permissions): decide terminal host membership in one predicate and share the opt-in hint

* refactor(rpc-permissions): parse bridge selectors once per call

* refactor(rpc-permissions): admit and bind unopted bridge methods from one table

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* refactor(remote-pane-binding): Phase 3.5 elegance pass (#26470)

* refactor(remote-pane-binding): keep the pane's pairing revision in one slot

* refactor(remote-pane-binding): share one hold decision across the input entry points

* refactor(remote-pane-binding): define the persisted-handle adopt once in attachPane

* refactor(remote-pane-binding): build the pushed-snapshot meta once

* refactor(remote-pane-binding): drop the test-only peerReplacedEnvironmentIds wrapper

* test(remote-pane-binding): share the input and subscribe frame readers via stream fixtures

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* refactor(relay-ladder): Phase 3.5 elegance pass (#26474)

* refactor(relay-ladder): put the host-Node fallback in the ladder before D

The Auto ladder now lists legacy as the rung before D, so the step function
is one rule plus the next rung. Deletes nextRelayRuntimeStep, the hostOs
step context and the mutable hostNodeFallback flag (now a constructor-time
laddered field). Windows still ends at legacy: B and C refuse there before
any host I/O.

* refactor(relay-ladder): one rung-D reason and message over reachable states

D is reached only from the host-Node fallback, so its refusal is
host_node_missing or install_failed. Merges the reason and message
functions, drops three message branches that could not run, and folds
noexecRemembered/noexecProved into one noexec field.

* test(relay-ladder): one never-worse cross-product with a remembered axis

Drops the named cases (each matches a cross-product outcome), the transport
case that only checked the model against itself, and the third case the rank
check already implies. Adds rung B's noexec answer and per-rung remembered
flags for A and B, the only rungs that replay a cached refusal.

* refactor(ssh): one answered-miss rethrow for the node resolution probes

Four catch blocks repeated the same session-limit, abort and strict checks.
They now call rethrowUnlessAnsweredMiss and keep their own fallthrough.
Also drops a stale 'Caches nothing' comment.

* refactor(ssh): share the host-answered system-ssh exit rule; flatten the upload throw cascade

isHostAnsweredSystemSshExit owns the 255/null transport rule for both the
upload pipeline and isAnsweredHostFailure. settleUploadPipeline drops a
branch that could not run: a failed tar is judged once, then ssh, then pipe.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* refactor(serve-daemon-misc): Phase 3.5 elegance pass (#26478)

* refactor(ssh-pty): build obligation transitions behind one closed-delivery guard

* refactor(build-orcad): table-drive the shipped worker smokes

* refactor(daemon): pass scope probe seams as one deps object and share cgroup line parsing

* refactor(repos): route every project removal through removeProjectForHost

* refactor(build-orcad): type the worker smoke table for the type-aware gate

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: m4air <m4air@Mac.localdomain>

* refactor(terminal-tail): Phase 3.5 elegance pass (#26465)

* refactor(terminal-tail): share mulberry32 and compare whole results in redraw equivalence tests

* refactor(terminal-tail): keep one splice ceiling at the onPtyData entry point

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(terminal): bound the backing a redraw row can pin across chained erases (#26547)

Each erase that keeps at least half the row skipped the copy, so a chain
of them left nested slices pinning the original wide row. Rows now track
an upper bound on the string they slice and copy once the cumulative
shrink passes half.

Co-authored-by: m4air <m4air@Mac.localdomain>

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 05:36:25 -07:00
OrcaWinandm4air f5ece0f912 test(e2e): retry Hangul-digit attempts the input method never delivered (#26566)
* test(e2e): retry Hangul-digit attempts the IME itself never delivered

The nested Wayland lane failed ~1 in 6 repetitions on main because mutter+IBus
occasionally discarded the preedit or the digit keydown before Chromium saw it.
Each attempt now checks what the page actually received; only those upstream
drops are retried, and any line Orca loses after the page received it still fails.

* test(e2e): keep the engagement receipt call the gate contract pins

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-08 05:32:06 -07:00
Neil 47165c90b4 test: load session owners directly and skip mocked recovery pacing (#26567) 2026-10-08 05:29:23 -07:00
Brennan Benson 5252884a9c Native chat: say when Codex is signed out or the agent isn't installed, without disabling Send (#25666)
* fix(native-chat): disable Send for unavailable agents

* test: update sign-in copy expectations

* fix: show automatic chat availability only on Send

* fix: keep signed-out evidence when a live session lists models

* refactor: split Send availability presentation and tests

* refactor(native-chat): keep the sign-in verdict on the catalog's failure record

The probe's "signed out" / "CLI missing" verdict now lives on the account's
existing model-catalog failure record instead of two extra maps, with the
failure TTL as its only lifetime:

- Only the probe sets or clears the verdict; a chat's own model listing
  (which works while signed out) never clears or renews it. An aged-out
  verdict makes the next read re-probe, however fresh the catalog.
- A healthy fresh catalog is no longer re-probed every 30 s; it keeps the
  normal 10 min fresh window. A Claude start refused as signed out ages the
  catalog so the next read re-probes.
- A waiting picker again answers from whichever catalog lands first and never
  waits on a probe. A read that still owes the probe's verdict says so, and the
  composer waits for it once with the new optional waitForAvailability param.
- The open-time start-failure line and its transcript row are hidden only
  while Send is disabled for the same reason; otherwise they show as before.
- A host verdict lifetime longer than the client's hold is clamped, not
  dropped. The catalog read watches only the account settings and the paired
  host's contact instead of every store write.

* fix(native-chat): one send-gate rule, and a missing CLI as its own failure

- The sign-in/CLI verdict is gated once where the session hands it to the
  composer: while a turn runs it neither shows nor refuses, so a follow-up
  queues as before; goal mode is held by the same gate. The start-failure line
  and row hide only while that same value is up.
- A start whose CLI was never found (a direct spawn's ENOENT, or the one a
  provider supervisor reports) is now a `cliMissing` failure with the disabled
  Send's sentence, instead of a generic "couldn't start" beside it. An older
  client shows the host's sentence for the new kind.
- Signed-out words for a slash command keep "Run /x again".
- The Claude catalog reads its models only from the reply to its own
  list_models request, never from the initialize reply.
- The Codex probe lists models while its account check runs; a signed-out
  verdict still wins.
- The missing-executable check moves to provider-process, out of native chat.

* test(native-chat): signed-out command failures name the command to run again

* fix(native-chat): probe the Codex home a launch would read

- Before the catalog probe's read-only app-server reads Orca's shared Codex
  home, it runs the same sync a launch and the usage poll run, so a
  `codex login` made in ~/.codex is seen at once instead of leaving
  "Run codex login" up until something else re-syncs (forever on a headless
  host). Per-account homes are left alone.
- A "no account" answer counts as signed out only when the home's auth.json is
  absent or holds no credential; one caught mid-write by a token refresh, or
  holding a credential, is unknown.

* test(codex-accounts): type the node:os mock with a type import

* fix(native-chat): keep the sign-in verdict until a newer answer replaces it

- No gap while signed out: the host serves an aged verdict while the probe that
  re-derives it runs, and the composer never clears its last answer before a
  read; only an answer without a verdict, an error, lost contact or an account
  change does. Send stays disabled with its reason through every re-check.
- After sign-in the stale "isn't signed in" start-failure line stays hidden:
  a probe that found the account fine now says so (`accountVerified`), and the
  line shows only when the host has no answer at all.
- A Claude start refused as signed out, or any start whose CLI was never
  found, is recorded as that home's verdict, and the chat reads it once when
  that start failure arrives.
- The re-probe mark is read only where the catalog read starts its probe, so a
  Codex chat's own picker no longer re-lists for it on every read.
- Choosing an account or signing in again in Accounts settings re-derives the
  verdict at the next read; the account key now compares settings by value.
- A re-read that brings an identical catalog keeps the option state, so focus
  re-reads re-render nothing.

* fix(native-chat): Codex's signed-out words name only the step that unblocks this chat

A chat is pinned to the Codex home it started in, so choosing another account
in Codex Accounts settings never unblocks it. The system-account sentence now
says only to run `codex login`, in every locale.

* refactor(native-chat): give an account's sign-in status its own owner

The sign-in verdict lived inside the model catalog's failure records, so
the store needed guards for who wrote a failure, which listing may clear
it, and whether a verdict was served while re-probing. Each guard fixed one
way a chat's own listing or an older probe overwrote the account's answer.

The answer now lives in AgentAccountStatuses: one in-memory, capped status
per account home (ready, not signed in, or CLI missing). Only the
session-less probe, a refused start and an account change write it. A
blocked answer older than 30 s, or one an account change marked, makes the
next read start the probe whatever else is listing, and it is served until
that probe answers. Older evidence never replaces newer. The catalog's
failure code is main's again, and agent-model-catalog-failures.ts is gone.

The wire carries one optional `availability` field in place of
`unavailable` and `accountVerified`. The renderer holds its answer per
host, agent and chat, re-reads only on that agent's account inputs, and
re-reads on a timer only while blocked. After an account change, Send
stays disabled with the previous words until the re-probe answers, then
changes once. Account switches and re-sign-ins mark the status before the
settings change a chat's read follows.

* fix(native-chat): re-derive the sign-in answer after an account change mid-probe, and back off while blocked

Round-4 review findings on the account status owner:

- An account switch or re-sign-in made while a probe was running left the
  old answer up for up to 30 s: the next read joined that probe, whose
  answer was then dropped as older than the change. A read now chains
  exactly one fresh probe after a probe that began before the change.
- Removing the selected Claude account swapped another sign-in into the
  shared config dir without marking the answer; it now marks it first.
- Evidence was ordered by wall-clock time, so a clock that stepped back
  froze a blocked answer. It is now ordered by a sequence and aged on a
  monotonic clock; a negative age reads as due.
- A chat left open while signed out started a CLI probe every 30 s.
  Probes that keep finding the same block now space out (30 s, 60 s,
  2 min, 4 min, then every 5 min), carried to the client as recheckInMs.
  An account change or a different refusal starts the hold over.
- The renderer dropped no settled wait from its leave set; it now does.
- A failed Claude probe logged its whole stdout, which now includes the
  initialize reply naming the account. Control replies are dropped from
  the log and the failure message.
- The account services take the status re-check as an injected
  onSignInChanged hook instead of importing the native-chat store.
- Imports moved below file headers; comments no longer say "ready"
  means re-checked after the failure.

* test(claude-accounts): say why the runtime-auth fakes are cast where the hook was added

* fix(native-chat): type the held-answer handle as the account status

* fix(native-chat): re-check a block when someone returns, and catch an account change during a home's first probe

Round-5 review findings:

- After a chat sat blocked for a few minutes, the backoff made every
  read wait out a hold of up to 5 minutes, so someone who ran `/login`
  or `codex login` and came back still saw Send disabled for minutes.
  The backoff now applies only to the client's own timer read, which
  it marks with an optional `scheduledRecheck` param. A read from
  someone looking (window focus, the pane becoming visible, a new chat,
  a start failure) re-probes once the block is older than 30 s. The
  param is sent only on that timer, which only a blocked `availability`
  answer arms, so a host that predates the param never receives it.
- An account change made during a home's first probe marked nothing,
  because only existing statuses were marked, so the old account's
  answer was written as current. Each agent's last account change is
  now one sequence number that every status and running probe is
  compared against; statuses are no longer mutated in place.
- Removing an account that nothing had selected no longer re-checks.

* refactor(native-chat): keep the sign-in and missing-CLI words in their own module

Main's retry wording pushed the failure-words table past its line budget.

* refactor(native-chat): build the structured chat's view session in the view-state module

Main's send-reveal work brought NativeChatStructuredSession.tsx and the
session controller to their line budgets.

* fix(native-chat): keep a stated start line hidden through a running turn, and re-probe an account changed mid first probe

- A chat whose open-time start failed signed out had that line hidden
  while the host said the same. When a new signed-out answer landed
  during a running turn, the line came back for the whole turn: the
  hide read Send's gate, which a running turn suspends. The hide now
  reads the host's answer itself (blocked for the same reason, or
  ready); Send's gate still steps aside while a turn runs. Both are
  views of the one answer the session holds.
- An account change during a home's first probe dropped that probe's
  answer but left nothing to re-check the new account, and the picker's
  reply said no probe was pending, so the chat stayed unknown until the
  catalog aged out. A read now chains one fresh probe whenever the
  running probe began before the change, and its reply says that probe
  is still owed so the client asks once more for its verdict.
- The client's timer read no longer carries `scheduledRecheck`. The
  timer already fires only when the host's backed-off `recheckInMs`
  says, which is past the 30 s after which any read re-probes, and
  every answer re-arms it. One fewer strict-schema param.

* build(cli): typecheck the missing-executable check the provider process now imports

Main's #25753 added managed-provider-process.ts to the CLI project, which
imports this PR's provider-executable-missing.ts.

* fix(native-chat): disable Send with the fix when Claude or Codex is signed out or missing

What you see: in a native Claude or Codex chat whose CLI is signed out, or not installed on the
computer running the chat, Send is disabled and its tooltip (hover or keyboard focus) says how to
fix it, in words that match the account ("Codex isn't signed in. Run `codex login`.", "This
Claude account isn't signed in. Sign in again in Claude Accounts settings.", "Claude isn't
installed on the computer running this chat."). A message refused after sending gets the same
words. On desktop the open-time "Chat could not be started" line, its Retry and its transcript row
no longer repeat these two reasons, unless a message of yours is still waiting on that start.
An unknown answer never disables Send, and while a turn runs Send stays Stop and Enter queues.

How: the host's existing session-less model-catalog probe also says why no chat can start.
Claude's probe child sends initialize before list_models and applies the rule chat start already
uses (token source none with no API key); Codex's probe asks account/read beside model/list; a
spawn that finds no executable is "CLI missing". The probe's failure record in the catalog store
carries that reason, and every catalog reply carries it as an optional `unavailable` field. Only
a probe writes or clears it: a live chat's own listing (which a signed-out Codex still answers)
never does. It lasts the store's 30 s failure lifetime; after that the next read starts or joins
the probe, serving the held reason meanwhile, and only a read that already asks to wait gets the
probe's answer. A change to the managed-account settings expires it. Before probing the shared
Codex mirror home, the probe runs the same sync a launch does. The renderer keeps the latest
answer until the next one replaces it, and only while blocked re-reads on window focus and when
the chat gains focus. No status store, timers or new request params.

* fix(native-chat): no picker loading while Send re-checks a sign-in, and plain keyboard focus on the disabled Send

A chat whose account is signed out and has no model list no longer shows the model picker as
loading each time it re-checks the sign-in after 30 s: the re-check is for Send, so a reply that
already carries the verdict ends the picker's wait. The disabled Send's wrapper takes keyboard
focus (so its tooltip opens) without becoming a second button; the inner button keeps its label.
Trims tests that repeated rules other tests already prove.

* fix(native-chat): lift the Send gate whenever Stop shows, including while a queued card is about to send

The composer shows Stop while the host's queue is about to send its next card, but the sign-in
gate stayed on in that window, so Enter was refused with no message.

* fix(native-chat): never disable Send for a sign-in or missing-CLI verdict; show a dismissible notice instead

The host's pre-send check can be wrong while the chat would work (saved
Arguments it never sees, project settings it can't read, gateways that
accept a signed-out Codex), and a disabled Send left no way past it.

- Delete the Send gate: the composer, Enter and goal mode no longer read
  the verdict, and the Send tooltip reason and focusable wrapper go back
  to main.
- Show Codex not-signed-in and Claude/Codex not-installed as one entry in
  the chat's existing notice card. Dismissed per verdict; shows again
  when the verdict changes or clears and returns; steps aside when the
  failed start (launch line or latest failed-start row) says the same.
- Claude: drop the probe's signed-out verdict (initialize-on-probe stdin,
  request-id filtering, control-reply log scrubbing). The chat start's
  refusal, with its account kind, is the only Claude sign-in signal.
  Missing-CLI from the probe stays.
- Restore the failed-start line and transcript row for these causes
  (no more hiding on desktop) and drop the chat-root focus re-check; a
  window-focus re-read stays while a notice is showing.

* fix(native-chat): re-check the notice's reason after a chat starts, keep a signed-out Codex's model list

Review round 1 of the notice rework.

- A chat that proves its start (at acquire, or on its later "started"
  event) marks the catalog's held reason for that account due, so the
  next read re-probes instead of trusting it until the TTL; the chat
  re-reads when a turn starts or ends while a notice shows.
- The Codex probe keeps a model list it got beside a signed-out reason,
  so a gateway user with a wrong verdict keeps the picker list.
- Failure records move to their own module; an expired failure with no
  reason is dropped, and expiry drops reason-less failures.
- Notice: any verdict change resets the dismissal; signed-out is
  Codex-only; the say-once rule ignores a failed start a turn has run
  past.
- Missing-CLI copy: "{{agent}} wasn't found on the computer running this
  chat. Install it, or check its Command in Settings → Agents." (6
  locales).

* fix(native-chat): only an acquiring start re-checks the catalog reason; drop the unreachable Claude filter

Review round 2 of the notice rework.

- A re-attach to a live child proved nothing new, so only an attach that
  acquired a child which proved its start marks the reason due.
- Reword: a start proves the program exists; for a Codex sign-in it only
  prompts an earlier re-check.
- Drop the notice's Claude signed-out filter: no host sends that verdict.
- Tests: a re-attach does not notify; a turn ending re-reads once more.

* fix(native-chat): never newly show a catalog reason the host is still re-checking

After signing in to Codex, a new chat flashed the old "isn't signed in"
notice for about half a second: past its 30 s lifetime the host answers a
plain read with the held reason while its probe re-checks. A chat now
adopts such a reason only when it already shows it; otherwise the joined
read's answer decides.

* test(native-chat): follow main's harness and the clearer Claude sign-in words after the merge

* test(native-chat): run the start-proof catalog test in the Node runtime project with the other SQLite host tests
2026-10-08 02:28:13 -07:00
Brennan Benson e4cd14d914 Remove legacy OS file-drop routing (STA-6940 PR6/6) (#26385)
* feat(file-drop): add element owner preparation plumbing

* Fix terminal and chat file drop destination ownership

* fix runtime terminal drop ownership and queued chat retries

* fix(file-drop): preserve feedback acceptance and destination ordering

* fix(file-drop): attach chat and composer files at the drop surface

* fix(file-drop): preserve project destinations and live chat availability

* fix(file-drop): keep path resolution in filesystem namespace

* test(file-drop): use filesystem path bridge in PR3 fixtures

* fix(file-drop): bind terminal drops to pane elements

* fix(file-drop): compare terminal pane identity across public views

* test(file-drop): align composer lifecycle with element-owned drops

* Fix interrupted chat composition and refuse unused quick-create drops

* Type the quick-create drop regression transfer

* fix(file-drop): let the explorer, project sidebar, tab strip and editor own OS drops

STA-6940 PR5. The file explorer tree, the project sidebar, each editor
group's tab strip and its editor area now take OS file drops on their own
element through the shared owner hook, with identity from their own render
(the explorer's shown workspace and target row, the tab strip's and editor
group's worktree and group) instead of the active worktree. The explorer and
sidebar broadcast subscribers and their legacy drop markers are deleted;
editor-open moves out of useGlobalFileDrop into editor-dropped-file-open,
which the legacy unmarked-chrome route still uses until PR6.

* fix(file-drop): keep editor delivery tied to its destination group

* fix(file-drop): guard delayed opens and share floating ownership

* fix(file-drop): remove legacy OS drop routing (STA-6940 PR6/6)

* test(file-drop): finish owner test and comment cleanup

* Clarify dropped-file default editor destination

* Retain UI test namespace for terminal resume coverage
2026-10-07 23:11:37 -07:00