Commit Graph
12746 Commits
Author SHA1 Message Date
Brennan Benson 761d63a4e5 feat(agent-launch): keep long prompts off the launch line and paste them after readiness (step 1 of 7) (#24257)
* feat(agent-launch): host-side prompt delivery for agent.launch

The host's agent.launch typed any launch prompt into the shell as part of the
launch command. A long or multi-line prompt then ran line by line in the
shell, and an agent that never showed readiness or crashed at startup had
nothing guarding where its text went.

agent.launch now carries a prompt on the typed line only when the line stays
one line, control-free and at most 512 bytes; otherwise the agent starts
clean and the host pastes the prompt once the agent's own ready signal fires
(bracketed paste plus its composer marker or a quiet render, read only after
the shell's last hand-off, never while the pane's own shell is proven in
front), with main's draft-paste bytes and an Enter 50 ms later. Orchestration
worker starts wait on tui-idle as before. A replay-safe launch admits and
claims its ledger row in one write, Qwen Code gets a second Enter, the
desktop and phone share one launch-refusal classifier, and hosts advertise
agent.launch.prompt-carry.v1.

Split out of #23748, which moves the desktop source-control buttons onto
this path.

* fix(agent-launch): keep a short-lined multi-line prompt on a local zsh launch line, as main did

#24257 moved every multi-line or over-512-byte prompt off the typed launch line and pasted it after readiness. The phone's AI buttons and review notes, whose multi-line prompts main typed whole into zsh, then reached Claude 0.5-3 s later and their RPC reply waited for the paste.

The host now names the shell a local macOS or Linux line is typed into, the way the spawn picks it, and a multi-line prompt rides a zsh line when every line is at most 512 bytes and the whole line at most 8 KB. A real-zsh test types such a line through Orca's own ready barrier and startup write, including when a slow user config makes the write land early. Elsewhere the measured unsafe cases keep the paste: bash 3.2 runs multi-line lines piecemeal, fish drops an early multi-line write, and any shell loses a line over 1 KB written early.

* test(agent-launch): keep the real-zsh launch-line test out of the Windows lane's gate scan

The Windows lane registration check read `const ZSH_PATH = process.platform === 'win32'` (the head of a multi-line ternary) as a Windows-true flag, so `describe.skipIf(!ZSH_PATH)` looked like a Windows-only suite. The file is POSIX-only; the zsh lookup is now a function.

* refactor(protocol): move the agent.launch capabilities into their own module

Main's protocol-version.ts sits at the 300-line cap, so the prompt-carry capability pushed it over. The four agent.launch capabilities and their doc move to agent-launch-runtime-capability.ts, re-exported by name and spread into RUNTIME_CAPABILITIES at the same position; the advertised lists and every export are unchanged.

* refactor(protocol): import the agent.launch capabilities from their own module

`export *` from protocol-version left the four names undefined under the mobile recording loader, which resolves a relative import through a Proxy with no own keys, so 37 phone recordings lost agent.launchReplay. Importers now name agent-launch-runtime-capability directly; protocol-version only spreads its list.

* refactor(agent-launch): drop the unshipped viewMode field and trusted local caller id

Both were inert in step 1 and existed only for step 2. agent.launch will become a
public plugin API, so every wire field is permanent once shipped; a top-level
viewMode reads as "choose terminal vs chat", which the host decides. Step 2
introduces placement and view intent under a placement object instead.

* fix(agent-launch): read Codex's provisional startup from the rule files' hold anchor

Main (#24375) moved Codex's provisional-header check into codex.json's
provisional_startup hold anchor and deleted codex-terminal-readiness.ts, so the
launch readiness hold now asks showsHoldAnchor, as main's own settled check does.

* fix(agent-launch): hold rule-file name titles to quiet for a launch, and census the zsh fixture

Main (#24375) answers a name-only title from each agent's rule file ahead of the
sustained-title lane, so gemini.json's name_title settled a launch readiness wait
on the shell's auto-title while Gemini was still booting. A launch now asks quiet
of every weak idle verdict, as that lane did.

Main's readiness census requires a recorder for every runtime fixture; the zsh
prompt recording is a non-agent control. Gemini's synthetic baseline is
regenerated for this PR's stated change: a bare gemini title is no longer its
rest mark, so name-only rows settle weak, and a fresh working or blocked status
is no longer overridden.

* fix(agent-launch): paste a launch prompt only when the launched agent is proven in front

A launch pasted its prompt unless a shell was proven in the terminal's
foreground, so any read that could not prove one let the prompt through. After
an agent exited at startup, its shell turned bracketed paste on at the next
prompt, readiness fired on it, and the prompt was typed into the shell:

- macOS: a pane runs its shell under login, so the process-group fence's root
  was never the shell's group and never proved it; the cached foreground name
  could also still name the exited process.
- Windows Git Bash and WSL: the shell-alone-in-its-job check never answers.

Now one fresh read of the terminal's foreground decides: agent, shell or
unknown. Only 'agent' lets a write through (paste, Enter, second Enter, reused
panes too); 'shell' still drops a ready signal. A Windows host never proves the
agent, so there the launch line carries the prompt at any size, as on main.

* test(agent-launch): cover the Windows QA stub, a grok override that exits at once

* fix(agent-launch): keep the local socket alive while a prompted launch waits for its agent

A launch with a prompt now waits up to 60 s for the terminal agent to be
ready before it writes the prompt, and reports not-delivered when the agent
never is. The local runtime socket closes a connection idle for 30 s unless
the request is a long poll, so a launch whose agent exited at startup lost its
reply and the caller saw 'runtime closed the connection' instead of
not-delivered. Classify a prompted agent.launch and agent.launchReplay as a
long poll, as orchestration.workerStart already is for the same wait.

* refactor(agent-launch): narrow the launch params by 'in' instead of a cast

* fix(agent-launch): find a launched agent behind a wrapper that leads its process group

A tcsh or nu launch line runs the agent from /bin/sh '<script>', and a
wrapper script that does not exec its agent does the same: the wrapper leads
the terminal's foreground process group and the agent is a member of it. The
fresh foreground read names the group's leader, sh, so a prompted launch was
refused or pasted late (M4Air tcsh: 2 of 4 not delivered, 2 pasted ~9 s late).

Before that read, take the host's process-group observation as positive proof
when it names the launched agent among the foreground group's members and is
younger than a ready signal's quiet window. It never proves a shell.

* fix(agent-launch): judge the foreground-group proof by when its capture began, not how long ps took

The age the host stamps on a process-group observation runs from the start
of its whole-machine ps, so on a loaded Mac a capture begun after the read
was asked for still read as older than 1 s and the proof was dropped. Count
an observation whose capture began after the read was asked for, less the
window a shared capture is reused across.

* test(agent-launch): keep the crash-guard live test out of the Windows lane's gate scan

The Windows-lane registration scan read the const assigned from a platform
check as a Windows-only gate, though the suite runs everywhere but Windows;
find zsh in a function instead, as the real-zsh typed-line test does.

Under load the fresh foreground scan can fail to answer, which lets the
shell's prompt settle readiness (2 of 4 paired runs). The guard still refuses
that write, so assert the refused write, the property that must always hold.

* perf(agent-launch): read a local pane's foreground from its own terminal, not the whole process table

The foreground read that gates every launch paste ran the daemon's
inspectProcess capture and then a fresh scan, each a whole-machine ps; the
fresh one also waits for any capture already running before it starts its own.
Measured here at load 5: 1.2 s a read (M4Air QA: 3.4-5.0 s, and worker starts
17.6-32 s against main's 9-12 s at load 25-84).

On a local macOS or Linux host, take the pane's root pid from the provider's
session inventory and run one ps limited to that pane's terminal. Its
foreground process group decides: the launched agent or any non-shell member
is the agent (a wrapper that did not exec its agent leads the group), a group
of shells alone is the shell. Same pane, same verdict: 2.7 ms a read. SSH hosts
keep the relay's observation and name.

* test(mobile): re-measure the web app's script sweep after agent.launch's capabilities moved out of protocol-version

The mobile web bundle check failed at 124 assets against a ceiling of 123. Main already sat
exactly on that ceiling: its sweep table read 69 scripts at 16 routes while the tree builds 73,
the whole margin of 4. This branch imports the agent.launch capabilities from their own module,
so protocol-version is no longer pulled into the root layout and four other routes. That moves
which routes share which modules, and the Qoder capability module, imported by protocol-version
and the AI-vault resume path, no longer shares an importer set with anything, so it gets a chunk
of its own: 74 scripts.

The fence says to re-derive the bound rather than raise it, so the sweep is re-measured on this
head (every prefix of the sorted route list). The worst route now adds 10 scripts (session), not
9, which moves the pinned shell crossing from 32 to 30 routes; main re-measured on its own lands
on the same crossing.

* fix(agent-launch): a worker's brief needs its agent found in front, and Grok's start answers on its composer

A paired-server worker start whose agent exited at startup typed its brief into the server's
shell, which ran it: the idle wait can settle on a shell back at its prompt, and the brief was
written with no foreground read. Both worker-start paths now check before each brief write, as a
launch prompt is checked: on a host that can find the agent in front it must be there; on one that
cannot (Windows) a shell proven in front still refuses, and anything else writes as before.

A Grok worker start waited ~10 s more than main: its only rest signal is its bare name, which a
launch holds to quiet output, and Grok animates its logo for ten seconds after its composer glyph.
A worker start for an agent whose rest signal is its bare name and whose composer draws a marker
(Grok, DSH, mimo-code) now also answers on that marker, whichever comes first.
2026-10-05 11:26:52 -07:00
Brennan Benson f250db55b7 fix(jira): read a missing or malformed Jira status as disconnected (#25579)
The paired web client has no Jira preload, so window.api.jira.status()
resolves undefined from the fallback proxy. Since #24376 the first status
check stored that undefined and the sidebar crashed reading `.connected`.
Parse the status reply at the runtime-client boundary so every reader
gets a well-formed JiraConnectionStatus.
2026-10-05 11:25:48 -07:00
Jinjing d19e064044 Fix legacy worker recovery snapshot timing for rollback safety (#25413)
* test: add legacy worker recovery persistence snapshot budget tests

Rollback requires the final stored state after processing all workers,
not intermediate snapshots after each candidate. This bounds the snapshot
count and preserves concurrent edits during the durable write.

* fix test

* clean up code
2026-10-05 11:21:05 -07:00
OrcaWinandOrca Worker a68ee67cf9 fix(codex): stop prompting a Codex restart when only its home changed (#25421)
* fix(codex): stop prompting a Codex restart when only its home changed

A Codex terminal that outlived the move to ~/.codex was blocked behind "This
Codex session is using an outdated configuration" until the user restarted it,
which starts a fresh codex and ends the conversation. That terminal keeps
working on Orca's old Codex home, which is refreshed from ~/.codex, so the
change did not need the user's answer; the prompt also never appeared for
Codex in Git Bash terminals, whose process tree Orca cannot see through.

The restart prompt now covers only an account switch, which the user caused.
The pane record keeps its home route, which the retained-home refresh and the
shared-server check still read; only the prompt and the custom-home downgrade
that existed to keep it from guessing are gone.

* refactor(codex): drop what the home-route prompt left behind

- Record a pane's own CODEX_HOME override as-is. The filter that kept only
  overrides Orca could re-derive existed for the removed route comparison;
  its one remaining reader, the shared-server check, returns null for the
  shared-home route those panes record either way.
- Treat custom-home like shared-home in resolveCodexPaneHome: the removed
  downgrade only wrote it without an override, so it never named a home.
- Inline the restart notice key; its route/account prefix was its only job.
- Delete the two pane-local override tests, including a POSIX-only one that
  still expected custom-home and would have failed on Linux and macOS CI.
- Cover the account recheck branches the deleted route-recheck file was the
  last to exercise: main reporting a pane current, and main not answering.

* refactor(codex): retire custom-home and the last re-check helpers

- Read an older build's custom-home record as shared-home, which it always
  was, and drop custom-home from the route type and every check.
- Delete shellStartupCodexHomeOverrideMatches and its comparison helper;
  nothing re-checks a recorded override any more.
- Build the pane launch record in one return: the route is always set, and
  only a resumed launch differs, in how it picks the account.
- Drop the restart dialog's notice key; its focus effect now depends on the
  pane and both account labels directly.
- Give the account recheck test a typed window stub, so CI's type-assertion
  gate passes, and remove timing entries for deleted test files.

* fix(codex): drop the removed dialog strings main added to es.json

* refactor(codex): stop recording a pane's custom CODEX_HOME

The pane record kept a pane's CODEX_HOME override so the removed route
prompt could re-check it later. Its one other reader, the shared-server
check, only looked at it for a real-home pane, and a custom CODEX_HOME
always routes a pane to Orca's mirror, so it was never used.

Drop both record fields, their validators, equality checks and spawn
plumbing. getCustomCodexHomeOverrideForLaunch folds into the existing
hasCustomCodexHomeOverrideForLaunch, and real-home resolves to ~/.codex.
Records from older builds still parse; the parser keeps only known keys.

The fish test's decoy no longer sets CODEX_HOME, so the boolean check
still fails if the launch env XDG_CONFIG_HOME is ignored.

* test(codex): drop setup the yes/no CODEX_HOME checks no longer need

---------

Co-authored-by: Orca Worker <orca-worker@localhost>
2026-10-05 11:06:40 -07:00
Brennan Benson 98171a8934 fix(native-chat): never delete chat history on read; a chat Orca can't load says why once (#24576)
* fix(native-chat): an older Orca keeps a chat with newer content read-only, and an unreadable annotation costs only itself

A body or lifecycle mutation of a kind this build does not know, inside a row of a known kind,
now latches the chat read-only with every row kept, as a newer row kind or version already does.
It used to read as damage, and the open deleted the journal from that row on.

Each optional field of a body schema now declares what an unparseable value means:
- droppable (an annotation): removed from the row in memory; the chat stays writable;
- must-understand (a prompt's questions, a plan approval's plan, a turn's lifecycle, a goal
  change): the row is unreadable and the chat latches read-only.
Only a required field that fails is damage, repaired as before. A test holds every optional
field reachable from a body schema to a policy; the known body kinds are read off the schema.

Prompt options and questions no longer reject a key this build does not know, which deleted the
journal from that row on. The context-usage drop is now one case of the droppable rule.

* feat(native-chat): a chat an older Orca keeps read-only says so before a send is refused

A chat a newer Orca wrote opens read-only, and until now nothing said so: the person found out
when a send failed with "Chats were saved by a newer Orca". The host now names the reason on
every whole history page it serves (`page.readOnly: 'written-by-newer-orca'`: hydration, history
and catch-up resets), the shared client reducer keeps it from the latest whole page, and the
desktop status strip and the phone's composer area say "This chat was saved by a newer Orca, so
it's read-only here. Update Orca to continue this chat."

Every read-only latch the host has is a newer Orca's (its database, a row version, a row kind, a
body kind or must-understand fact), so the reason is derived from the journal's latch, not stored.
The field is optional: older clients ignore it (cross-version test against the newest release's
reducer), and older hosts never send it, so a new client against one says nothing, as today. A
reason this client does not know shows nothing rather than words that may be wrong.

The window-merge helpers move out of the client reducer into their own module.

* fix(native-chat): only a newer build's value goes read-only; damage repairs as before

An older Orca keeps a chat read-only only on evidence a newer build wrote it: a value outside a
closed set this build knows (a body kind, a nested discriminant or literal, a mutation kind), read
off zod's issues. That evidence wins over damage beside it. Anything else that fails (a wrong type,
a missing field, a bad optional value) is damage, repaired as before, so damage no longer freezes
a chat behind an "update Orca" it cannot fix. Drops the per-field droppable/must-understand
policy. A lifecycle mutation's kind is decided before its item id, so a newer kind without one is
kept. The context-usage drop is unchanged from main.

* fix(native-chat): a read-only chat locks its composer and says why there

Removes the separate read-only line (desktop status strip, phone notice component). The host's
`readOnly` on whole pages now feeds the existing composer lock instead: desktop disables the
composer with "Saved by a newer Orca. Update Orca to continue this chat." as its placeholder, and
the phone adds a 'read-only' input-lock reason with the same words. A read-only chat shows no
prompt card it would refuse; the draft stays in the composer. The phone's send-failure line is
back exactly as on main.

* fix(native-chat): a read-only chat pages back through its history

A read-only journal answered every history request with a reset, so a chat this PR now keeps
read-only more often could show only its newest page. Backward reads now serve the snapshot the
open folded, as the first page already does; a forward read of rows still resets.

* test(native-chat): an unknown key in a question entry is read and kept

* test(mobile): the read-only overlay test typechecks

* chore(native-chat): satisfy the changed-code gate in admission and the read-only overlay test

* fix(native-chat): a new value inside a known block or goal arm reads as a newer build's

The open fallback arm of the block and goal unions now aborts, so zod reports the known arm's own
failure instead of only the fallback's; a future closed set there reads unreadable, damage there
still repairs. The schema headers name the context-usage exception and say a new open-string
value must be safe for every older build (rewind keeps only known states; pages carry no row
version).

* fix(native-chat): a read-only chat reconnects for a whole page, so an updated host unlocks it

The client keeps `readOnly` until a whole page replaces it, but reconnected at its cursor and got
only batches, so a desktop attached to a host updated in place stayed locked with "Update Orca".
While it holds the latch it now subscribes without a cursor; the snapshot re-derives the state on
any host version. The phone already subscribes without one.

* fix(native-chat): every desktop write control follows the one read-only fact

The structured session's transport state derives one fact from the page's `readOnly`: the words
for why the host refuses writes. Every write control reads it. While it holds, the chat has no
turn and no work, as the host projects it, so the working row and Stop go away; a pending prompt
stays shown above the composer with its card disabled; queued cards' Send now, Delete and Edit,
the queue's Resume, the goal banner's actions, the model and option pickers, dictation and
background-task Stop are disabled or withheld. The composer placeholder stays the one place the
reason is said. `lockReason` now locks the composer by itself.

* fix(mobile): every phone write control follows the one read-only fact

The phone session derives one fact from the page's `readOnly`, the words for why the host refuses
writes, and every write control reads it: no turn or work while it holds, so no Stop; the pending
prompt stays shown with its card disabled; queued cards and Resume are inert; the model and option
pickers stay shut; the composer field is not editable, so its placeholder keeps saying why, and the
lock applies at once instead of after the transport lock's settle.

* test(native-chat): the newest release hydrates a read-only chat and scrolls back through it

* test(native-chat): a read-only chat reads idle even with a send its provider never answered

* test(native-chat): composer lock test without type assertions

* fix(native-chat): round-3 read-only fixes

- An old /model or option request no longer reopens its picker when a read-only chat unlocks: the
  menu is keyed by the request alone, and a lock only keeps it from mounting open.
- A disabled question card still steps through its questions; only answering is locked.
- A failed send's notice in a read-only chat says only that the message was not sent, with no
  Retry: the composer already says why, and no retry can land. Its wiring moves into
  useStructuredAgentSessionDeliveryNotices.
- The host's background-task stop flags stand; the client no longer overrides them.
- The view reads the read-only fact through one local flag.
- The schema header says what an older build's rewind does with each open string.

* test(native-chat): count picker mounts without an effect

* fix(native-chat): read-only keeps other failures' words and shuts open option menus

- In a read-only chat, only a send the newer-Orca refusal stopped is shortened to "Your message
  was not sent."; any other saved failure keeps its own words, and none offers a Retry.
- A model or option menu open when the lock lands has its items disabled, so it cannot send a
  change the host would refuse.

* test(native-chat): lock-lands picker test uses a typed surface

* fix(native-chat): a locked composer draws its reason

The editor's placeholder used the default that draws nothing while the editor is not editable, so
a read-only chat's composer was empty and its reason lived only in the aria-label. The placeholder
now draws while disabled, and its words are empty unless the composer is locked with a reason, so
every other disabled composer (a pending prompt, no terminal) looks as before.

* fix(native-chat): sync the locked-placeholder flag in an effect, not during render

* fix(native-chat): a read-only chat is not offered for resume or counted as failed to resume

After a relaunch, a chat whose journal this build keeps read-only (a newer Orca's) was listed in
"Resume interrupted chats?", failed with "Orca couldn't resume this chat. Open it to continue
manually.", and left a status-bar "N chats failed to resume" that never cleared, since nothing can
continue a chat this build cannot write. The resume set now skips such a chat after the checks
that end an offer (fork, moved on), so the offer is kept, not spent, and an updated Orca offers it
again; the failure ledger keeps an already-filed failure for it but does not show it. Both read
the chat's open journal, which listing and acting open first.

* fix(native-chat): a refused resume continuation keeps the refusal's reason

A send refused while continuing a chat after a restart was filed with its code alone, so a newer
Orca's refusal lost its `journalWrittenByNewerOrca` reason. The continuation now carries the whole
refusal, as a refusal from the agent's start already did, so the filed failure keeps it.

* fix(native-chat): a newer Orca's chat keeps its resume offer however it is met

- A newer Orca's whole database counts as read-only for resume, so a chat whose journal cannot
  be opened at all (a table the newer schema changed) is not offered, run or counted either.
- The checks made right before sending no longer skip read-only chats: turning one away there was
  read as the user having moved on and deleted the offer. Its send is refused instead.
- Settling a resume files nothing for a newer Orca's refusal; the existing rollback reopens the
  offer for an updated Orca.
- The continuation records the refusal as a reference, without the wire prose.

* chore(native-chat): one import of the session wire types in the continuation

* docs(native-chat): resume and read-only comments match the current rule

* test(native-chat): a closed set of a journal row cannot change without the row version

The test walks the body schema and the row and mutation kind tables, collects every closed set (a
row kind, a mutation kind, each enum, literal or discriminant), and compares them with a snapshot
recorded beside the row version. A new value makes older builds go read-only, so it fails until
the reader ships first or `v` is bumped and the snapshot updated. Tags a catch-all arm accepts as
any string (block types, goal states) are listed apart: older builds read a new one as-is.

* feat(native-chat): a body or plan subject of a newer kind is kept and the chat stays writable

An item body kind and an approval subject kind this build does not know now read through the
same catch-all blocks and goal states use (`openDiscriminatedUnion`), instead of making the chat
read-only. The item is kept, drawn by nobody, and ignored by everything that reads items (turns,
prompts, status); a rewind carries it as it was, kept by its place like every other row. An
approval whose subject this build cannot draw shows its `detail`, which Orca's writers fill with
the subject's text. A sent message of a kind this build does not know stays a newer build's.

* fix(native-chat): rewind narrows a retained body by its kind before normalizing it

* fix(native-chat): one chat that cannot open no longer stops the startup restore of the rest

* fix(native-chat): a damaged chat fails to load and nothing is deleted

A row this build cannot parse, a gap, or an epoch without its first row used to
be repaired on open: every row from the damage on was deleted and a rebuild from
the provider transcript was attempted. The open now fails through one refusal
(failLoadOnJournalDamage, journalCorrupt), every row stays, and a damaged
per-chat file is kept whole and never copied. A newer build's row still wins
and keeps the chat read-only. The body classifier for closed-set values, the
repair marker and disclosure, and journal recovery from the transcript go.

* chore(native-chat): no reset or adapter left on the open's fixtures

* feat(native-chat): an approval of a newer Orca's subject kind can only be cancelled

Desktop and phone show its detail and the line "This request needs a newer
version of Orca.", disable every answer, and keep the card's cancel, which ends
the turn. The chat stays writable.

* test(native-chat): the closed-set guard says a missed version bump fails older builds' load

* test(native-chat): the closed-set guard names what a missed version bump does

* fix(native-chat): a row the reader rejects is never written, and a chat that cannot load keeps its tab

Every journal insert reads its serialized row back with the reader inside the
write's transaction and refuses one it rejects, so Orca's own writer can no
longer leave a chat that fails to load. A restored chat whose open fails keeps
its tab and says why when opened. An epoch named with no rows is founded afresh
again, deleting nothing. The at-rest test now expects a damaged chat to be
refused; the replay gate keeps its 2026-09-11 evidence as it was run.

* fix(native-chat): a newer Orca's approval subject is carried as it was, and its card cancels with or without a turn

The approval subject's type is open, so code that reads a plan narrows first; the
host's prompt bounding carries an unknown subject instead of rebuilding it as a
plan, which threw in every start's stale settlement. A Codex rewind keeps a
newer Orca's item in its place. A whitespace-only tag is damage. The card's
cancel reaches the host without a running turn on hosts that take that. A
provider row the reader rejects ends its turn as interrupted and the next send
works. A guard fails if an approval subject kind is added before clients can be
gated (STA-9262).

* fix(native-chat): a card's cancel sends nothing without a turn again, and rewind keeps a newer row after any held row

The host's cancel request has to name a turn on every version, so the turnless
prompt cancel is reverted on desktop and phone. The rewind merge moves its anchor
for every row the provider still holds, so a newer Orca's row stays after one
whose kind this build does not know.

* fix(native-chat): a card this build cannot answer leaves the composer open, and a send starts a turn

An approval of a subject kind this build cannot draw, raised by a newer host's background agent
while no turn runs, left nothing to press: its answers are disabled, its cancel needs a turn, and
the desktop composer gave the card its slot. A send the host queues waits behind any pending
prompt, so even the phone's open composer only queued.

While every pending prompt is one this build cannot answer, the desktop composer stays open, and
desktop and phone send without asking to queue: the send starts a turn, and the card's cancel then
settles it. A chat a dead run left is unaffected: opening it already cancels those prompts.

* feat(native-chat): a chat a newer Orca saved fails to load and says to update, with nothing to send into

Opening a chat a newer Orca saved (a row kind, batch-change kind or sent-message kind this build does
not know, a newer row version, or a newer history store) used to open it read-only: the history
shown, the composer locked with a reason, every control greyed. Now it fails the load, like a
damaged chat, with its own words: "This chat was saved by a newer Orca. Update Orca to open it."
Every row is kept. The read is final, so the pane stops retrying; reopening the tab reads again.

A chat whose load failed for good (damaged, or a newer Orca's) offers no composer under the error,
on desktop and on the phone, so the failure is said once and no send can be refused a second time.

A restart offer for a newer Orca's chat is spent without a word: no failure filed, nothing counted.

Removed with the read-only mode: the journal's read-only latch and every check of it, the history
page's readOnly field (never released), the cursorless reconnect, read-only paging, the composer
lock and placeholder, the greyed controls, and the resume exclusion.

* test(native-chat): a newer Orca's records open no chat, and a status fake carries its submissions

* test(native-chat): the status fake's cast says what the feed reads

* fix(native-chat): a chat's read that failed for good takes the whole pane, over a loaded transcript too

* test(native-chat): the final-read-failure test names its reasons

* fix(native-chat): a newer Orca's chat keeps its restart offer, and its refused load is logged once

* refactor(native-chat): the newer-Orca chats a restart listing skips live beside the candidate reader

* fix(native-chat): a refused chat load keeps where it failed, so the log names it and a new reason logs again

* chore(native-chat): the reducer's window merge back where main has it, and comments say a newer Orca's chat fails to load

* test(agent-hooks): the rename test's journal fakes carry submissions, as a journal snapshot does

* test(agent-hooks): the rename test's journal fakes say what the status feed reads

* fix(native-chat): Dismiss all ends the restart offers this Orca lists, and keeps a newer Orca's hidden ones

* test(native-chat): a resend against a newer Orca's store answers unknown, and the phone says nothing beside the read's words

This build never opens a chat a newer Orca's database holds, so a resent send id there cannot be
answered from its journal: it answers "outcome unknown", never a refusal and never a made-up
record, with no second delivery and no write. On the phone, a send error left beside a read that
failed for good is not shown; the pane keeps only the read's words.
2026-10-05 10:45:58 -07:00
Brennan Benson ad5aa010b6 fix(native-chat): a chat's finished-turn alerts come from the host recorded on its tab, not its workspace id (#25081)
* test(native-chat): a mirrored paired chat carries its host, so a workspace-id collision no longer hides its owner

* fix(native-chat): a chat's finished-turn alerts come from the host stamped on its tab, not its workspace id

* fix(native-chat): the attention bridge reads a chat's owner from the same rule as the chat pane
2026-10-05 10:43:59 -07:00
Brennan Benson 8f846d471e fix(native-chat): a Stop binds only the turn it actually stopped (#24864)
* fix(native-chat): a Stop binds only the turn it actually stopped

A Stop pressed before any turn showed used to claim, at end-write time,
whatever turn the stopped send later opened, even when the Stop stopped
nothing. A turn that then died on its own read as "Interrupted" (your
cancellation) instead of "Failed".

Now a person's Stop that named no turn binds, in memory only, every turn
that ends while the Stop settles, and afterwards only the turn its
interrupt took. The settle ends a still-running stopped turn once. A
relaunch finds nothing in memory, so an unsettled turnless Stop binds no
turn. A Codex Stop whose answered turn does not open within its wait, or
whose send's answer was lost, now answers refused, so the host ends the
child and the turn can never run.

* fix(native-chat): keep the person's queue pause and close binding after a Stop settles

A host stop or eviction with no turn running now defers to a person's
Stop while its queue pause still holds with nothing sent since, read from
rows, so a held card is not handed off on reopen after a Stop that did
nothing or whose kill failed.

A person's close that named no turn opens a settle around its child's
end, so a turn that end cuts reads as theirs.

A press opens its settle only when the latest Stop event is its own or
the one in force it repeats: a late Stop, a card's interrupt or a lost
event row reopens no earlier Stop.

A Codex Stop that cannot reach a turn still able to open says the Stop is
unconfirmed rather than that no turn ran, and a second Stop still reaches
a turn an earlier wait left unopened.

Also drops the unused openedBy plumbing and the unreachable "a written
cancellation stays one" rule, and pins a relaunch after a named Stop.

* fix(native-chat): a Codex Stop agrees on both presses when a turn is still owed, and pin the close's settle

A Codex Stop that waited for a turn Codex answered a send into now answers
"may still open" whenever that turn neither opened nor ended and its send
is still owed, however the wait ended (it ran out, or the thread went
idle). Before, a first press after an idle thread said no turn was
running and kept Codex, while an identical second press ended it.

Adds a test that a person's close the conversation outlives (as /clear
does) closes its settle, so a later turn that ends on its own reads as a
failure.

* test(native-chat): a Stop whose event row failed binds no turn to an earlier Stop

With one ordered journal writer the Stop's event is in the fold when its
write returns, so the press reads whether it owns the latest Stop from the
fold instead of awaiting the write. Pins the case the read must refuse.

* test(native-chat): name the settle, not a stream drain, in the Stop's own-end test

* test(native-chat): a Codex Stop answered before Codex ends the turn reads interrupted throughout

Codex answers an interrupt it took before it sends turn/completed (interrupted):
on TurnAborted the app-server answers pending interrupts, then ends the turn, on
one channel. The test fake did the reverse. It now answers first and ends the
turn on a later read, and the tests that read the turn's end right after a Stop
wait for it.

New end-to-end test through the shipped host, journal and Codex adapter: with
the real order, every end row of the stopped turn reads interrupted by the Stop
(named, unnamed, and a Stop pressed while turn/start was in flight). Breaking the
settle window turns the in-flight case red: the Stop's own end row then has no
verdict, which reads as failed until Codex's end lands.

* fix(native-chat): a Codex Stop interrupts a turn Codex answered but has not opened at once

A Stop that named no turn, made after Codex answered a send but before the turn
opened, used to wait up to 5 s for the turn to open before interrupting, and
ended the Codex process when it didn't. The stated reason, that Codex refuses an
interrupt until it opens the turn, holds only part of the time: with no turn
active, Codex takes an interrupt once its thread runs (turn_interrupt_inner),
and refuses it with -32600 "no active turn to interrupt" before that or once
the turn has ended.

The Stop now sends the interrupt at once. Only on that refusal, while the turn
has neither opened nor ended, does it wait for the turn to open (bounded at
5 s) and send it once more. A turn that ended meanwhile was nothing to stop. One
that never opens, or that an earlier wait already gave up on, fails the Stop,
and the host ends the child as before. Sends still wait for the turn to open
before steering into it.

The test fake models Codex taking an interrupt once the thread runs (run()).

* fix(native-chat): every Codex Stop waits for an answered turn to start, as the first did

A Stop whose interrupt Codex refused as finding no active turn skipped the wait
when an earlier wait, a Stop's or a send's, had already given up on that turn.
Every press now waits its own bound and retries once if the turn starts, so a
turn that opens during a later press is still stopped. Both presses still reach
the same verdict when it never starts.

* fix(native-chat): a Codex Stop that ends the child before any turn opened withdraws its send

A Stop on a Codex turn that was answered but never opened ends the Codex
process. That end settled the send as in doubt (unknown, recovered), and the
client's outbox holds every later send behind a send in doubt until the person
presses Retry, which re-sends the very message they stopped. The chat looked
stuck.

Codex records a prompt only once its turn has started, so a send whose turn
never opened never ran. When the Stop's refusal says so (turnMayOpen), the child
end now settles the unanswered sends as withdrawn, the verdict Codex's own
interrupted-turn end already gives an unechoed send. The flag rides on the owed
wind-down, so a retry after a failed child end withdraws them too. Claude's
child end still leaves its unanswered send in doubt.

* fix(native-chat): derive the withdrawal of a Codex send whose turn never opened

Replaces the flag the Stop carried to the child's end, and its copy on the owed
wind-down, with a reading of the journal at the settlement that lands. A Codex
child's unanswered send is withdrawn when a person's Stop is in force since it
was sent and no turn row ran, or was written, after it; any other end (a turn
that opened, a host's close, a crash, Claude) still leaves it in doubt. A
retried wind-down reads the same rows, so it withdraws the same sends.

* fix(native-chat): keep a send in doubt when a turn was open for it

The derived withdrawal read a turn as open for a send only if it still ran or
was written after the send. A send steered into a running Codex turn whose
interrupt failed met neither once the adapter's end settled that turn ahead of
the host's settle, so it read withdrawn, though Codex drains a steer into the
running turn and may hold it. A turn that ended after the send was handed over
was open for it too: such a send stays in doubt, as before.

Pins that case, and that a send made after the Stop, to a child that then dies
before its turn opens, stays in doubt.

* fix(native-chat): withdraw a Codex send by whether it started its own turn, not by timing

Whether a turn was open for a send was read from end times: a turn that ended
after the send's handover counted. A send made while a Stop ended the turn is
handed over once that turn reads ended, yet Codex's own end for it can arrive
later, so such a send whose own turn never opened read in doubt again, and the
chat's queue held behind it.

The handover already records where the send went: its message joins the turn
running then (a steer) or belongs to no turn (it starts its own). Only a send
that started its own turn, with none opened since, is withdrawn; one that
joined a running turn, or has no recorded place, stays in doubt.

The Codex test fake takes an answered interrupt as Codex does, dropping the
turn before its end arrives.

* fix(native-chat): leave no Stop row when the Stop took back a send that never ran

A Stop on a Codex send whose turn never opened ends the child, and the child's end
takes the send back into the composer. The Stop still wrote "Cancellation
requested." at the conversation level, so with the send gone it sat under the
previous finished turn and read as if that turn had been stopped. A Stop that found
no turn running and whose child end took back every send it found now writes no
row; a Stop of a running turn, or one that leaves a send in doubt, still does.

* fix(native-chat): count a send whose answer was lost when a Stop takes it back

The no-row rule counted only pending sends, but the child's end also takes back a send this process left in doubt when Codex's turn/start answer was lost. That case still wrote "Cancellation requested." under the previous turn. Both now read one predicate, so they cannot drift apart.

* test(native-chat): pin which sends a Stop's child end can take back

A send an earlier process left in doubt is never withdrawn and never holds the row back, and a queued card's send is never counted.

* test(native-chat): read the outbox reconcile from where main moved it
2026-10-05 10:40:38 -07:00
Jinwoo Hong 2a68ea9496 refactor(terminal): one commit module for terminal topology closes, with a boundary check (#25329)
* refactor(terminal): move the topology revision and leaf-lookup helpers into terminal-topology

advanceTerminalTopologyRevision, findTerminalTabIdForLeaf and
hasHostAuthoritativeTerminalMembership move verbatim from the renderer-save
membership rebase into persistence/terminal-topology, the home of the commit
boundary. Importers are repointed; no behavior change.

* test(terminal): test-only guard for topology writes outside the commit boundary

The three session sinks now publish through one commitWorkspaceSessionPartition
helper, which hands the prior and published partition to the topology write
guard. Production never arms the guard, so each sink pays one global lookup.

The unit suite arms it in report mode: a sink write that changes class-(a)
topology (membership, root, bindings, titles, incarnations, sleeping records,
remote session ids, tombstones, default-applied, revision) outside a commit
scope is attributed to its writer's file by stack, and fails the test unless
the writer is on the unrouted-writers allowlist that later routing PRs shrink.
Renderer saves and test seeding are exempt. Deep-freeze is available but stays
off suite-wide until the in-place writers return new sessions.

* refactor(terminal): add the topology commit module with bindLeaf, closeLeaf and closeTab

terminal-topology-commit.ts is the boundary for class-(a) terminal topology.
bindLeaf forwards to persistPtyBinding, whose write now runs in a commit
scope; the spawn commits and the relay reattach bind through it. closeLeaf and
closeTab wrap the existing close mutation in a commit scope and one
persistence.terminal-topology span (kind, outcome, refusal reason; no ids),
and the runtime close goes through them. Session output is unchanged: tests
compare it byte for byte with the old writers for local, ssh: and folder
workspaces.

* chore(terminal): drop an unused lint suppression from the topology write guard

* fix(terminal): attribute topology guard writers relative to the repo root

The guard read a frame's file through its last /src/ segment, so a test under
tests/ (folder-upgrade-identity-persistence.unit.test.ts) had no source frame
and failed as an unknown writer. Frames are now taken relative to the repo
root the setup passes in, tests/ counts as test seeding, Windows backslashes
are normalized before the node_modules skip, and nested src/ paths keep their
full path. The two runtime funnel files skip only their funnel function, so
another writer in them still shows. The R9 allowlist key names the file whose
frame actually writes. The class-(a) diff and the attribution move into their
own files; the stack limit is restored in a finally.

* refactor(terminal): drop bindLeaf until binding reaches a sink; add the boundary ratchet

bindLeaf and the commit scope inside persistPtyBinding changed nothing: the
binding write never reaches a session sink, and a scope inside the Store method
would have admitted every direct caller once it did. Both return in B1-4; the
spawn commits and the relay reattach call persistPtyBinding directly again.

The runtime close now calls one closeLeafOrTab entry, so its callbacks keep
their contextual types. A census test is the primary enforcement: only
persistence/terminal-topology and the callers it lists may call
persistPtyBinding, the session setters or the three sinks, and every
unrouted-writer allowlist entry must name an existing file.

* fix(test): resolve the topology guard's repo root without the global URL

Under happy-dom the global URL is not Node's, so fileURLToPath(new URL(...))
threw in the setup file and failed every happy-dom test file.

* refactor(terminal): drop the runtime topology write guard; the boundary ratchet enforces

The AST boundary ratchet is the enforcement for B1. The stack-attributed
runtime guard, its class-(a) diff, the unrouted-writer allowlist, the vitest
setup and the freeze option are removed; it saw two writers in the whole unit
suite and its real value starts only once binding reaches a sink (B1-4).

Also: one closeLeafOrTab wraps the close in the span (no per-kind copies or
narrowed types), the span has one finish like persistence.pty-binding, the
sink helper is publishWorkspaceSessionPartition (it publishes; the commit
boundary is the module), the ratchet drops the private publishSession row and
checks that every listed caller file exists, and the close comparison keeps
its two meaningful cases with span cases chosen by name.

* refactor(terminal): trim the B1-1 commit module and ratchet to what they enforce

- Point the acknowledged-tab-retirement audit fixture at the moved
  advanceTerminalTopologyRevision; its old import no longer resolved.
- Drop publishWorkspaceSessionPartition: it was the removed guard's
  interception point, so the three session sinks return to origin/main.
- One traced(kind, mutate) wrapper in the commit file replaces the span
  factory; closeLeafOrTab is one call.
- The ratchet walks src/main with the shared scanSourceTree, drops the
  loading-store-internal rows and the redundant file-exists test; exact-set
  equality already fails on a missing file.
- The commit test is a pure unit test of the span outcomes: no Store
  harness, electron mock or self-comparing close.

* refactor(terminal): census the runtime session controller's write and drop stage ids from comments

The controller's setter was named set, which the boundary census could not
list without matching every Map.set, so a new OrcaRuntime mixin could write
sessions through it unseen. Rename it setForWorktree and census it.
Comments now describe state instead of citing plan stage ids.

* refactor(terminal): census writer references and trace refusals by callback

- traced() takes refusalOf instead of assuming an Error refusal, and only
  mutate() sits in the try, so a span outcome of threw means the write threw.
- The boundary ratchet counts references, not just direct calls: non-null
  calls, bracket keys, aliases, destructures, .call/.bind and parenthesized
  callees all count; declared names and type positions do not.
- Census terminalSurfaceCloseMutation (boundary-only) and the partition sinks
  setLocalWorkspaceSession / setHostWorkspaceSession.

* test(terminal): count writer uses in extends clauses and instantiations, skip type-only imports and local declarations

The census skipped ExpressionWithTypeArguments as a type, which also holds
`extends f(x)` and `x<T>` value expressions. Type-only import/export
specifiers and declared names (variables, parameters, accessors, enum
members) no longer count as uses. The audit fixture is listed in the table
instead of a separate exemption.

* test(terminal): count quoted and assignment-pattern destructures of layout writers

* test(terminal): count every mention of a layout writer except its definition

Telling definitions from uses per syntax kind kept missing nested and
for-of destructures. Exempt only the writer's own function or class-member
definition; any other mention (including object-literal keys) counts, so the
census errs toward a loud false alarm rather than a silent miss. Quoted names
count only in member-name position.

* test(terminal): count every string literal naming a layout writer

Member-name positions missed wrapped keys like store[('name')] and
store['name' as const]. Counting every string literal outside types is
shorter and errs toward a loud false alarm.

* test(terminal): exempt only class members and functions as writer definitions

Object-literal methods and accessors were exempt while equivalent arrow
properties counted; all object-literal keys now count alike.

* test(terminal): parse files with unicode escapes in the writer census

A name spelled with a \u escape never appears verbatim, so the text
prefilter skipped it.

* test(terminal): parse any file with an escape in the writer census

\x, identity and line-continuation escapes also decode to a writer name
without it appearing verbatim.
2026-10-05 13:39:21 -04:00
github-actions[bot] d17351401d Update README downloads badge 2026-10-05 12:43:43 +00:00
Neilandkambarakun e2da3a1eb1 fix: prevent IME confirmation from submitting text fields (#25480)
Keep IME confirmation out of text-field submission and command selection. Reuse shared gesture ownership, preserve marked-release redispatch, guard overlay Escape capture, and retain Markdown save shortcuts.

Verified with independent adversarial reviews, 891 tests across 71 files, six hidden Electron tests, project typecheck, full lint, the changed-code quality gate, and all final-head CI checks.

Fixes #25035.

Co-authored-by: kambarakun <kambarakun@gmail.com>
2026-10-05 03:51:25 -07:00
Neil 1569e062d9 Reduce repeated terminal scans and unused Qoder test imports (#25425)
Name the local hook confirmation function to avoid misclassifying internal selectors as dialog text.
2026-10-05 03:43:14 -07:00
fa180a9ee2 feat(jira): pick the assignee when creating an issue — Automatic, me, or anyone (#24376)
* feat(jira): pick the assignee when creating an issue — Automatic, me, or anyone

The new-issue dialog gains an Assignee picker: Automatic (Jira applies the
project's default assignee), one-click "Assign to me" resolved from the
target site's stored identity, or any user via search. The selection resets
with the custom field values on project/type switches, since account ids
are site-scoped.

Creates now also send userFieldKeys, so the host shapes user-typed values
into the {accountId}/{name} refs Jira requires — previously the dialog
never passed them and user create fields were sent as bare strings. Keys
are only named when a value is present, keeping older remote hosts without
the user-fields capability working for assignee-less creates.

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

* fix(jira): address assignee review — project-scoped assignable search, per-site picker remount, keep assignee across type changes

- Search the create dialog's assignee picker against /user/assignable/search?project=…
  (new listAssignableUsersForProject, IPC split into jira-user-search.ts for the
  max-lines budget); remote environment targets fall back to the existing
  site-wide search since older hosts have no such RPC
- Remount the picker per target project so cached results from the previous
  project or site cannot be selected after a switch
- Reset the picked assignee on project selection (account ids are site-scoped)
  instead of on every create-fields reload, so issue-type changes keep it
- Hide the assignee field and omit it from the payload when the target create
  screen does not accept an assignee — Jira rejects fields absent from the screen

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

* fix(jira): scope create assignee to the effective project and provider

* fix(jira): isolate assignee search candidates across provider changes

* fix(jira): search assignable users with Server-compatible project keys

* fix(jira): refresh assignee identity with provider connection status

* fix: distinguish hook confirmation from browser confirm

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Neil <neil@stably.ai>
2026-10-05 03:30:48 -07:00
Neilandnwparker 8b5de39217 Submit initial OpenCode 2.0.12 prompts through the native startup plugin (#25428)
* fix(opencode): submit initial prompts for reviewed 2.0.12

Route the reported release through the existing native startup intent.
Retain conservative behavior for other unreviewed versions.

* Wait for OpenCode startup fixture file reads in the hydration test

---------

Co-authored-by: nwparker <nwparker@users.noreply.github.com>
2026-10-05 03:08:22 -07:00
40f9e7a3fc fix(codex): resume account switches without blanking the terminal (#25485)
fix(codex): restart account switches without blanking or repinning the pane

Integrate the atomic pane replacement from #24350 and adapt the explicit
conversation resume from #14877. Keep terminal protocol replies flowing
while account notices block user input. Ordinary restores retain provenance.

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: itisvincent <vincentcgamer@gmail.com>
Co-authored-by: rayim <rayim@fxy.global>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-10-05 01:51:49 -07:00
kazuma.mikiandkazooooo-ma b3328390f9 Remove completed SSH picker E2E plan (#24824)
Co-authored-by: kazooooo-ma <zooooomer-com@gmail.com>
2026-10-05 01:49:54 -07:00
Daiki HirayamaandNeil c2c7649849 fix(mobile): stop Android from selecting words while the chat transcript scrolls (#22871)
* fix(mobile): stop Android from selecting words while the chat transcript scrolls

On Android every paragraph, heading, quote, code block and table cell in
the native chat transcript was a selectable TextView. Android starts a
word selection, with the magnifier, on a double tap or a long press, and
two flicks in the same spot while scrolling a FlatList register as a
double tap, so scrolling the chat kept selecting words. iOS is unaffected:
its UITextView path arbitrates scroll against selection itself.

Android now renders transcript text without inline selection: one gate in
MarkdownText covers every selectable span, and the user bubble follows
it. A long press on a message opens a sheet with "Copy message" and
"Select text", the latter a screen whose only content is one selectable
Text, so a selection can only start where the user asked for it. The
message row owns that sheet and mounts it only while open. iOS and web
keep their inline selection and get no long-press handler.

Verified on a Pixel 10 Pro Fold (Android 17): an adb double tap on the
transcript selects nothing, the same double tap inside "Select text"
selects a word, tool rows inside the bubble still expand on tap, and the
long press opens the sheet.

* fix(mobile): scope the Android selection gate to the transcript and route span long presses

Review follow-ups on #22871:

- The gate now applies only where `rangeSelectable` is passed (the chat
  transcript). Task comments and file previews keep their selectable text
  on Android as before.
- On the Android transcript, spans that take taps (links, file paths)
  also take the row's long press, so a link under the finger no longer
  swallows the copy/select sheet.
- Copied text keeps its whitespace; only whitespace-only blocks are dropped.
- The Android markdown test compares `String(node.type)` instead of a
  type assertion, which the changed-code quality gate rejects.

* fix(mobile): route long presses on Markdown images to the row on Android

An image block is a Pressable of its own, so on the Android transcript it
now carries the row's onLongPress like tappable spans do; a long press on
an image opens the copy/select sheet instead of being swallowed. Test
extended with an image block.

* test(mobile): pin the Android long press from a chat row to its actions sheet

The existing row suite runs as iOS, where the bubble has no long press.
This one runs as Android: the bubble's long press mounts the actions
sheet with the message, the markdown receives the same handler, closing
unmounts the sheet, and the user bubble carries no inline selection.

* fix(mobile): preserve Android message selection while replies stream

---------

Co-authored-by: Neil <neil@stably.ai>
2026-10-05 01:41:19 -07:00
Neil 1b957b70a8 fix: close worktree dialog before slow script checks (#25472)
* fix: move worktree script checks out of creation dialog

* fix: retain creation host across background preparation
2026-10-05 01:29:44 -07:00
Neil 67fc708b3b Group CSV editor modules and tests in their own folder (#25476) 2026-10-05 01:16:20 -07:00
Ken Fukuyama e46fa0df13 test(linear): cover numeric-leading issue identifiers (#10241)
Expand numeric-leading Linear identifier coverage in the current parser and workspace source flow while rejecting numeric-only team keys in bare inputs and URLs. Main already implements the production behavior through #23423.

Repair two stale localization test assumptions found by full CI. All changes are regression tests.

Validation: 369 focused tests across 46 suites, local typechecking/lint/formatting, and full final-head CI passed.

Co-authored-by: Ken Fukuyama <kenfdev@gmail.com>
2026-10-05 01:10:43 -07:00
Neil c53b6f235a Edit CSV tables directly and remember column widths (#25442)
* Add direct CSV table editing and persistent column widths

* Preserve pending CSV edits across concurrent saves and panes

* Keep CSV input stable during autosave and table commands

* Cancel delayed menu paste after CSV pane focus changes

* Prove delayed CSV menu paste starts before focus changes
2026-10-05 01:00:20 -07:00
Neilandmmarabel 2b0ce17514 Show provider credit balances alongside usage limits (#25408)
Display provider credit and currency balances separately from rate limits. Preserve quotas when balance data is unavailable and translate complete labels.

Supersedes #9363; credit to @mmarabel for the original implementation.

Co-authored-by: mmarabel <166927047+mmarabel@users.noreply.github.com>
2026-10-05 00:07:23 -07:00
Brennan Benson a154562a89 fix(native-chat): show a reply cut off by an Orca crash or quit like a finished turn, with one explanation (#25043)
* fix(native-chat): read a crash-cut reply like a finished turn, with one explanation

A reply that an Orca crash or restart cut off said it failed three times: the
turn bar read "Failed after N", the chat's notice row said Claude stopped, and
the sidebar dot stayed red after the chat was read.

The turn bar now reads "Worked for N" for a proven crash/restart cut, the
notice row stays the one explanation, and the sidebar card's dot and agent
rows read failed only until the user has visited the chat (the same
acknowledgement that un-bolds the row), then read done. A failure, a user's
Stop, a replaced turn and an unconfirmed end keep their labels. The stored
outcome is unchanged.

* fix(native-chat): explain a turn a quit or eviction cut, once

A turn cut off by quitting Orca, an idle eviction or a teardown recorded the
same outcome as a crash-cut turn but wrote no notice row, so with the turn bar
now reading "Worked for N" nothing in the chat said it stopped.

The host stop's settle now writes the existing providerExited notice for the
latest turn when it ends as news (no person's Stop decided it): in the same
write as the host's own turn end, or right after the adapter's. It is keyed by
the turn, and skipped when an error row already explains that turn, so a retry
or an earlier exit row never leaves two.

* test(native-chat): narrow the turn scope and add the seen map in the crash-cut tests

* fix(native-chat): derive a cut turn's one notice on read instead of writing it at stop time

A turn cut short when the agent stopped without anyone asking now reads
"Worked for N", so it needs a row saying it stopped. Writing that row only on
the quit and eviction paths missed journals written before this change, a quit
that died between its drain and its settle, and any future stop cause.

The transcript now derives it from the journal on desktop and phone alike: a
root turn that ended interrupted with no verdict and no row about the stop
gets one notice in the provider-exit row's words. A stored exit row, matched by
its exit fact or its writer's identity rather than its tone, stays the
explanation, and so does the restart continuation's own outcome note, so a
refused resume says it once. The host's stop path is back to what it was.

* fix(sidebar): read a cut-short turn's red mark from its acknowledgement on every surface

A turn cut short with nobody asking reads failed only until the user has seen
it. The previous revision passed an optional "seen" flag to each caller, so any
surface that did not pass it, the chat's own tab dot among them, stayed red
beside a sidebar that read Done.

The verdict's display now takes the acknowledgement as part of what it reads:
the entry's stateStartedAt beside the time the user last acknowledged it, both
required, judged by one shared rule. Each surface joins it once where it builds
its rows: the sidebar's agent rows (and so the notes send menu), the workspace
card summary, the terminal tab bar, Cmd-J recent rows, and Activity threads.
Failures, Stops, replaced turns and unproven ends keep their marks as before;
the phone, which has no acknowledgement record, keeps the unseen reading.

* refactor(attention): use the one acknowledgement rule where it was copied

Auto-acknowledgement, the Activity unread count, dashboard row buckets and
notification acknowledgement each spelled the same "acknowledged at or after
the current state began" comparison; they now call the shared rule.

* test(mobile): type the cut-turn notice test's client and hook holder

* test: pin an older host's exit row by its writer and the sidebar rows' acknowledgement join

* test(sidebar): re-read the card and the tab when only an acknowledgement changes

* fix(native-chat): keep a cut turn's notice through a resume that carries on

A resume after a quit writes its "asked this agent to continue" note after its
own message, so counting that note as the cut's explanation removed the notice
once the resume went on, and made it flash away under automatic resume. Only the
notes that say the chat was not carried on (refused, not connected, not
confirmed) stand in for the notice now.

A row about the whole conversation now explains a cut turn only when no other
turn lies between them, and a failed start's row, which is about a start, never
does. The host writers and the rule share the row identity prefixes, with the
contract that a new row explaining a stop carries the provider-exited fact or
one of them.

* fix(sidebar): judge a cut as seen by the main agent's clock when a subagent holds the row

A subagent can keep a row working after its main agent was cut, and the row's
clock then predates the cut, so a look at the working chat counted as having
seen the cut and no red mark showed. The mark now compares the acknowledgement
with the later of the row's and the main agent's clocks; auto-acknowledgement of
the chat on screen reads the same clock, and its stamp covers it, so looking at
the chat still clears the mark. Bold rows, dashboard buckets, notifications and
unread counts keep the row's clock.

* fix(native-chat): tie a conversation-wide exit row to a cut only with no message sent since

A send after a quit's cut whose new agent died before its turn opened leaves a
provider-exit row about the whole conversation. That row is about the send,
not the earlier cut, so the cut kept no explanation. An exit row now explains
a preceding cut only when no message was sent in between; the restart notes,
which follow the continuation's own message, still need only no turn between.

The notes that say a resume did not carry the chat on are now told apart from
the continued note by their tone ('error' or 'warning'), which every host that
wrote them has set, instead of by their words.

* fix(native-chat): keep an owner's proven death the explanation of its cut after a send

A chat read before the startup reconcile settles its cut turn unverifiable;
if the user then sends a message, the reconcile proves the old agent dead and
writes its row about the conversation after that message. The row names the
old owner's death, never the send, so a message since no longer detaches it
from the cut. Only a provider-exit row, which a later start can write, still
needs no message sent since.

* test(sidebar): give the activity-status store mock the acknowledgement map

The card summary now reads acknowledgedAgentsByPaneKey; this test's hand-built
store state lacked it, so every summary read threw.

* refactor: move the seen-gated red mark out of this change

This change now keeps only the cut turn's label and its one derived notice.
The red mark that clears once the user has seen a cut turn moves to its own
change, so each can land alone; until it lands, the sidebar, tab bar, Cmd-J,
Activity and the notes send menu read a cut turn as failed, as before.

* test(native-chat): pin that a restart note after a continuation's turn leaves the cut's notice

* test(native-chat): keep a cut turn's notice beside a later message drawn as not sent
2026-10-04 23:19:58 -07:00
Jinjing e02adc5320 Add translations for terminal shell settings (#25418)
* Add translations for terminal shell settings and search

Wrap Terminal Pane shell configuration strings and terminal search keywords with translation calls to enable localization across supported languages. Add translated strings to all locale files and update the localization coverage allowlist to reflect properly translated content.

* fix translation
2026-10-04 23:01:02 -07:00
Jinwoo Hong d279adc6ef fix(activity): keep code blocks in row previews from rendering as scroll boxes (#25410)
The activity row previews the agent's last reply with the compact markdown
renderer, which draws fenced code blocks and tables as their own scrollable
boxes. Those boxes escaped the row's line clamp, so a reply with a code block
put a tall grey box with scrollbars in the middle of the list.

Row-scoped overrides now flatten code blocks and tables into plain wrapping
text, so the existing clamp cuts them like any other line. Other markdown
surfaces are unchanged.
2026-10-05 01:53:53 -04:00
Jinjing f873aaac5b Fix duplicate session option flags and preserve argument values (#25382)
* Fix duplicate session option flags and preserve argument values

* improve tests
2026-10-04 21:57:55 -07:00
Neil 16d937d7be Keep large CSV previews responsive and add column resizing and links (#25381)
* Bound CSV preview memory and virtualize resizable linked cells

* Fix dense CSV previews and retain bounded viewport pages

* Align CSV record limits across BOMs and line endings

* Respect headful mode in CSV visibility checks
2026-10-04 21:53:19 -07:00
Neil 7863871270 Keep OpenCode foreground evidence when its background service starts (#25378) 2026-10-04 21:34:09 -07:00
Jinwoo Hong b94cfa7fd0 feat(relay): declare Asia spare cell c34 as migration-only (#25336)
Adds a sixth asia-east2 cell at the C31 shape (cap 3000, 6000 request
units, pool 16, e2-standard-4) in asia-east2-c, pinned to the f30b5cb1
cell image. It gets its own topology and registration wave but no
promotion wave, so the admission script and workflow refuse to promote
it; it stays a migration-only landing zone and out of the fleet pool list.

Claude-Session: 1145a80d-dec4-4a9b-9373-bbbb876b9041
2026-10-05 00:26:33 -04:00
Jinwoo Hong e347aa4e67 fix(mobile): honour the desktop pinned-worktree placement setting (#25301)
* fix(mobile): honour the desktop pinned-worktree placement setting

Mobile always kept pinned rows in their groups as well as in Pinned. Desktop's
showPinnedWorktreesInGroups (default off) shows them only in Pinned. Read that
setting with its own settings.get operation, refreshed with the list's view
settings, and leave host and device-local pins out of their groups unless it
is on.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): keep lineage children with a pinned parent

Under single-location a pinned parent left its group while its unpinned child
stayed behind as a root. Visible descendants now follow a pinned ancestor into
Pinned, nested under it, as on desktop. Drop the inert host-id half of the
pinned-policy stale-reply guard; a host switch replaces the client.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): fold pinned placement into the desktop view-settings sync

One "sync from desktop" callback now also reads settings.get for pinned
placement, unawaited so a slow read never holds the ui.get merge; the separate
callback and its plumbing are gone. Re-record the three host.view-settings
goldens, which gain only that settings.get send. The pin expansion walks the
lineage children index the renderer now shares, over visible rows only.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): one stale-reply rule for the desktop view-settings sync

Both reads in syncViewSettingsFromDesktop now drop a reply only when the client
was replaced; the host-id half compared a captured value with itself. Pin the
no-wait invariant: the ui.get merge applies while settings.get never answers.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): follow a pin through rows hidden by search

Desktop's Pinned section walks lineage over every worktree and keeps the
visible descendants, so a grandchild still follows a pinned root when search
hides the middle row. Mobile walked visible rows only and left it in its group.
Build the children index from the unfiltered list; membership stays visible-only.
Drop the hostId arg the view-settings sync no longer reads. Pinned-lineage
tests move to their own file to stay under max-lines.

* refactor(mobile): read pinned placement in its own hook

The placement read was folded into the desktop view-settings sync, so that
callback juggled an awaited and an unawaited read and three goldens recorded a
settings.get that never answers. useHostShowPinnedInGroups now owns the setting
and the catalog refreshes it beside the view-settings sync on connect, focus and
mount; the sync, screen state and goldens are back to main.

The reader returns desktop's boolean, as its siblings do, so transport no longer
imports a worktree type and the two-literal policy union is gone. makeSection
always applies lineage, the pinned walk is a Set worklist, and the lineage
children index drops a set that duplicated its map's keys.

* fix(mobile): key pinned placement to the client that reported it

The host screen is reused across hosts, so the previous host's "show in groups"
value survived a switch until the new read landed, or for good if it failed.
The setting now carries the client that reported it and counts only for that
client, which also makes a late reply from a replaced client inert and drops
the clientRef plumbing.

* fix(mobile): drop a replaced client's late placement reply

Keying the value to its client hid a previous host's value but still let that
host's late reply overwrite the current host's, reverting the list to the
default until the next refresh. Restore the clientRef write guard; the client
key still covers the effect-long window before clientRef follows a switch.

* refactor(mobile): treat pinned placement like the screen's other host state

The placement hook kept its own host-switch handling (a client-keyed value plus
a clientRef guard) beside the screen's existing one. showPinnedInGroups now
lives in HostScreenState, resets with the other host-scoped values in
useHostScreenIdentity, and is read in the catalog refresh behind the same
clientRef check as the catalog fetch. The hook and the catalog argument go.
2026-10-04 23:53:01 -04:00
Jinwoo Hong 3f6225deeb fix(orchestration): accept max and ultra effort for Codex models Orca does not list yet (#25375)
Codex models newer than Orca's built-in list (gpt-6.x) were capped at xhigh,
so worker-start refused --effort max/ultra that the installed Codex supports.
Unknown Codex models now accept the full known effort vocabulary; unknown
effort names are still refused.

Refs STA-9312
2026-10-04 23:08:02 -04:00
Jinwoo Hong 52bd9afe9d refactor(relay): stop creating three tables nothing writes (#25354)
* refactor(relay): stop creating three tables nothing writes

relay_confirmable_splices, relay_cell_drain_attempts and
relay_migration_leases are created at every boot and referenced nowhere
else on main except tests. In production all three hold 0 rows and
pg_stat_user_tables shows 0 inserts, 0 sequential and 0 index scans since
the 2026-09-28 stats reset.

This removes them from the schema and the transaction-phase table, and
drops the test references. It does not drop them: an older image still
runs CREATE TABLE IF NOT EXISTS at boot, and a drop racing that create can
fail the older boot's schema step. A follow-up can drop them once no image
that creates them can be rolled back to.

* docs(relay): note the account-erasure prerequisite for dropping retired tables
2026-10-04 23:04:17 -04:00
Neil 1bec53ceb2 Reduce repeated CI setup and overlap mobile typechecks (#25359)
* Measure remaining CI import, diagnostic and checkout savings

* Qualify remaining CI candidates on hosted runners

* Qualify independent mobile typecheck overlap on Actions

* Keep explicit RPC test registries from loading unused methods

* Qualify complete RPC registry cohort and mobile cancellation

* Promote measured CI setup and typecheck savings

* Recognize the shared RPC test guard in lint policy

* Align the mobile barrier contract with independent typechecks
2026-10-04 19:58:35 -07:00
Jinwoo Hong e884681eca fix(relay): retain confirm results for a week and audit events for 90 days (#25353)
* fix(relay): retain confirm results for a week and audit events for 90 days

relay_confirm_results (8.3M rows, 5.8 GB) and relay_audit_events (8.4M
rows, 3.6 GB) were never deleted. The director's credential cleanup now
reaps both after its existing passes:

- Confirm results older than 7 days. The only reader replays a stored
  result for a retry of the same request on the same connection basis; a
  different basis is already refused as a tuple mismatch. committed_at has
  no index, so this uses the TID-window reaper from the reservation prune,
  capped at 250 rows a tick (the 7.4M-row backlog drains over 2-3 days).
- Audit events older than 90 days, ordered by `at` so the batch walks
  relay_audit_events_at. Nothing in the relay reads them back. The oldest
  row is from 2026-07-14, so this deletes nothing until 2026-10-12.

reapBatch now deletes by `ctid = ANY(ARRAY(...))` on Postgres: with
`ctid IN (...)` the planner can choose a hash join over a sequential scan
of the whole table.

* chore(relay): record the decided audit retention
2026-10-04 22:56:31 -04:00
Neil ebeea319b2 Support modern Qoder commands and verify authenticated resume
Start and resume Qoder through the existing execution-host selector when only the documented qoder command exists. Preserve legacy qodercli preference, explicit commands, quoting and session identity; disconnected SSH execution refuses without local fallback.

Mobile history uses command-at-create only with the optional owned-create capability and an existing stable mutation identity. Reconcile authoritative execution-host inventory before retrying creation, adopt the same surviving operation, preserve WSL/incarnation metadata and restore only missing original launch metadata. Changed retry settings cannot replace original capture. Bounded evidence expires after 15 minutes; unavailable inventory or original evidence refuses recovery and leaves surviving execution untouched. Authoritative inventory proving absence preserves the existing recreation behavior; this is not a durable exactly-once ledger for completed one-shot side effects. Older hosts retain the acknowledged create-then-send path.

Scope mobile launch authority to the current committed host/client generation, and recheck operation ownership after asynchronous preparation before later sends. Retire the mutation once the host acknowledges the resume; later ownership changes stop navigation without reporting a completed resume as failed or reusing a cached legacy pane. Preserve genuinely interrupted mutation identity. Preserve current-main OpenCode validation and merged Pi/Cursor behavior. Correct unchanged-main editor test fixtures to their production insertion-range and store contracts while retaining original assertions and production behavior.

Credit: Neil Parker (@nwparker); Soperf and jyang for Qoder integration/history groundwork in #24614; actual Pullfrog and CodeRabbit reviews and the independent Source reviewer for the mobile retry, launch capture, evidence lifetime and connection ownership findings. Conservative positional process recognition and existing folder-history matching-worktree limitations remain documented.
2026-10-04 19:50:45 -07:00
Jinwoo Hong aac1c8f9c2 chore(relay): move US cells c32 and c33 to the general same-cap list after promotion (#25367)
Both were promoted to general on 2026-10-01 (selector 344 shows them general),
but the same-cap job still classed them migration-only. Rollback mode on either
would isolate (general -> migration-only) before its no-op check failed, demoting
a live cell. They stay out of the fleet pool list (pool 10, not 16).

Claude-Session: 1145a80d-dec4-4a9b-9373-bbbb876b9041
2026-10-04 22:39:12 -04:00
Jinwoo Hong e9daf9b746 fix(relay): prune released control-connection reservations in bounded batches (#25352)
relay_control_connection_reservations was never deleted: 13.8M rows and
9.1 GB in production, 99.999% of them in state 'released' and ~400k more
a day. Nothing reads a released row (every reader filters it out by
state), but each placement still locks all of its host's rows, ~160 on
average.

A new director sweep step deletes released rows older than a day. It walks
the heap in TID ranges of 16 pages, because without an index on
released_at a `LIMIT n` delete plans as a sequential scan from page 0 that
gets slower as the head of the heap empties (production EXPLAIN). Each
statement selects its rows FOR UPDATE SKIP LOCKED, so a row a request holds
is skipped rather than waited on, and deletes them by `ctid = ANY(ARRAY(...))`
so the delete is always a TID scan. A tick stops at 400 rows, 128 pages or
250 ms, which drains the backlog over about three days at five directors.
2026-10-04 22:38:24 -04:00
Jinwoo Hong b9b0f29172 fix(relay): skip the dead-cell sweep's host scan when no cell qualifies (#25350)
evacuateDeadCells ran one query that joined every assignment to its cell's
liveness and fence state, ordered by primary key with LIMIT 100. In
production no row ever matched (the only dead cells are stale existing-only
cells without a committed fence), so the planner walked the whole
relay_assignments primary key every call: 551 ms mean over 85k calls,
~12% of relay database time.

A cell-level pre-check now evaluates the predicate's cell-only half over
the ~33-row cell tables (3 ms in production). It is a superset of the cells
the host query can act on, so the sweep returns early when it is empty and
otherwise restricts the host query to those cells. The host predicate is
unchanged.
2026-10-04 22:38:16 -04:00
822fc5bed4 Add repository OpenCode permission defaults (#25326)
* test(config): reproduce rejected repository OpenCode config

* Add repository OpenCode permissions and allow its reviewed root config

* fix: preserve sensitive OpenCode confirmation prompts

---------

Co-authored-by: Orca campaign recovery <campaign-recovery@example.invalid>
Co-authored-by: Orca OpenCode Campaign <opencode-campaign@local.invalid>
2026-10-04 19:34:47 -07:00
+3 46c3c3b44d Keep OpenCode worker model selection separate from the default (#24768)
* feat(orchestration): support OpenCode worker model selection

Allow supervised OpenCode workers to use per-launch model overrides through the existing launch-preference and receipt path.
Preserve existing OpenCode agent arguments while replacing only model flags, and reject unsupported effort values explicitly.
Keep Native Chat option exposure unchanged and update the worker CLI and orchestration guidance.

* fix: wait for OpenCode worker composer before first dispatch

Reuse captured composer readiness on local and paired execution hosts and revoke launching-shell paste anchors.

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>

* feat(opencode): probe execution-host CLI capabilities

* fix(opencode): select plugin default for execution host loader

* fix(opencode): limit prompt prefill capability to verified release

* feat(orchestration): gate OpenCode worker model preferences by host capability

Co-authored-by: user141514 <user141514@users.noreply.github.com>

* feat(opencode): probe launch capabilities on the execution host

* feat(opencode): probe execution-host CLI capabilities

* feat(opencode): probe launch capabilities on the execution host

* feat(orchestration): resolve explicitly configured command aliases

* fix(opencode): select plugin loader for the launched host binary

* fix(opencode): match WSL probe cwd and declared guest environment

* fix(orchestration): verify available OpenCode model on execution host

* Add host-owned OpenCode and Devin account profiles

* fix(orchestration): inspect OpenCode models with selected account environment

* fix(orchestration): bind model validation to direct existing-workspace launch

* fix(opencode): preserve launch environment deletion boundaries

* fix(orchestration): limit effective model contract to verified OpenCode release

* wip(opencode): authorize native startup prompt intent at execution owner

* fix(opencode): atomically replace status plugin entrypoints

* fix(opencode): retain plugin permissions across restrictive umasks

* test(opencode): resolve permission fixture from primary cwd

* feat(opencode): install startup prompt plugin independently of status hooks

* Restore inherited account environment and preserve cleanup retries

* fix(opencode): select plugin loader for the launched host binary

* fix(opencode): match WSL probe cwd and declared guest environment

* fix(opencode): preserve launch environment deletion boundaries

* wip(opencode): authorize native startup prompt intent at execution owner

* fix(opencode): atomically replace status plugin entrypoints

* fix(opencode): retain plugin permissions across restrictive umasks

* test(opencode): resolve permission fixture from primary cwd

* feat(opencode): install startup prompt plugin independently of status hooks

* Check relay environment values before merging

* fix(opencode): wait for admitted startup intent and preserve failed-launch briefs

* fix(opencode): confine overlay manifest cleanup to owned directories

Co-authored-by: Adnan Khan <adnank11427@gmail.com>

* fix: wait for OpenCode worker composer before first dispatch

Reuse captured composer readiness on local and paired execution hosts and revoke launching-shell paste anchors.

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>

* fix(orchestration): validate configured aliases with target shell grammar

* fix(orchestration): use actual shell and refuse assignment-only aliases

* fix(opencode): wait for admitted startup intent and preserve failed-launch briefs

* Support verified OpenCode v2 model selection in fresh native workers

* fix(opencode): refuse unsupported startup preferences before model probing

* feat(opencode): probe execution-host CLI capabilities

* fix(opencode): select plugin default for execution host loader

* fix(opencode): limit prompt prefill capability to verified release

* feat(opencode): probe launch capabilities on the execution host

* fix(opencode): select plugin loader for the launched host binary

* fix(opencode): match WSL probe cwd and declared guest environment

* fix(opencode): preserve launch environment deletion boundaries

* wip(opencode): authorize native startup prompt intent at execution owner

* fix(opencode): atomically replace status plugin entrypoints

* fix(opencode): retain plugin permissions across restrictive umasks

* test(opencode): resolve permission fixture from primary cwd

* feat(opencode): install startup prompt plugin independently of status hooks

* fix(opencode): wait for admitted startup intent and preserve failed-launch briefs

* fix(opencode): unsubscribe hook settings during async host shutdown

* fix(opencode): confine overlay manifest cleanup to owned directories

Co-authored-by: Adnan Khan <adnank11427@gmail.com>

* test: preserve typed calls in configured worker target checks

Replace Reflect.apply with the existing typed prototype call pattern so the unchanged regression cases pass the anti-slop lint gate.

* test: check typed model host calls and terminal delay state

Replace Reflect dispatch and property reads with checked access and typed calls. Complete the existing resume request fixture with its required identity fields without changing the rejection boundary or assertions.

* test(readiness): census recorded OpenCode composer boots

* fix(opencode): reject redirected overlay parents before cleanup

* fix(orcad): retain runtime cleanup when subscribing to hook settings

* refactor(launch): extract OpenCode config and attachment authority

* fix(opencode): retain host version selection across relay restarts

* fix(opencode): pass run prompts as positional messages

Preserve run flags and use the existing shell quoting and run-command detector
to append the initial message after --, reusing an existing separator.
TUI launches retain their version-selected prompt transport and draft behavior.

Original run-order work: @coelho-doti (#13065, tracked in #17551).

* fix(opencode): keep wrapped run tasks positional

Recognize supported environment prefixes and PowerShell call operators without
mistaking prompt arguments for executables. Keep environment and run separators
separate, preserve the task text and exclude run commands from native submission.

Source-parent: 23fc08b4e9
Related-to: stablya/orca#17551
Credits: @coelho-doti (stablya/orca#13065)

* Prepare complete private OpenCode launch validation source

Integrate the complete reviewed readiness, capability, native prompt, overlay and positional-run source onto frozen main. Preserve canonical atomic ACL retry, status generator/disposal, restrictive-umask fixtures and unowned source. Keep supported wrapped run commands positional.

Private-validation-source: a44345ce49
Original-full-source: 23fc08b4e9
Original-core-base: 8186ded0bd
Frozen-main: 08ee7ba9ef
Owned-source-paths: 111
Publication-policy: private validation only; preserve the six separate PR boundaries and held model/provider drafts

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Adnan Khan <adnank11427@gmail.com>
Credits: juli-gonzalez readiness contribution; Ahmed Nagy atomic plugin writer; coelho-doti positional run contribution

* Prepare private complete 111-path launch validation on current main

Private validation only. Preserve main credential additions and original launch ownership. Held model and provider topics remain excluded.

* Recognize env options before positional OpenCode run messages

* STRICT launch CI contract correction

* CAPS launch CI contract correction

* INTENT launch CI contract correction

* test(opencode): wait for malformed claim retries before expiring intent

Observe real endpoint I/O completion under fake timers before forcing expiry.

* test: initialize Claude prompt state in output retention fixture

* Verify OpenCode catalog model launches and preserve current launch behavior

* Verify OpenCode catalog model launches and preserve current launch behavior

* Refuse unverified new-worktree OpenCode models and complete host audits

* Wait for OpenCode location hydration in intent startup

* Refuse unverified new-worktree OpenCode model launches and record startup attribution

* fix(opencode): bind startup readiness to the composer location

* Bind OpenCode startup readiness to the current location in intent startup

* Restore the owning Orca CLI path after shell profiles

* Use a literal marker for the Bash lookup regression

* Preserve plain panes and initialize zsh after prompt hook replacement

* Preserve user line-editor dispatchers during deferred startup

* fix: retain CLI startup when global Zsh replaces prompt hooks

* test: replay global Zsh hook replacement after host startup

* test: isolate controlled Zsh widgets from distro keyboard setup

* fix(shell): preserve user hooks during deferred zsh initialization

* Retry interrupted OpenCode startup prompt claims

* Keep completed Zsh startup hooks retired when the wrapper is sourced again

* Reject truncated OpenCode catalogs and explain worktree model limits

* Use a template literal in truncated-catalog coverage

* Refuse unfinished OpenCode model catalogs

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: user141514 <user141514@users.noreply.github.com>
Co-authored-by: Ahmed Nagy <ahmednagy25t@gmail.com>
Co-authored-by: Adnan Khan <adnank11427@gmail.com>
Co-authored-by: Orca startup hydration review <agents@stably.ai>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Orca maintenance <orca-maintenance@users.noreply.github.com>
Co-authored-by: Orca <dev@stably.ai>
Co-authored-by: Orca campaign <orca-campaign@local.invalid>
Co-authored-by: Orca OpenCode issue campaign <opencode-campaign@stably.ai>
Co-authored-by: OpenCode issue campaign <codex@localhost>
Co-authored-by: Nathan Parker <nwparker@users.noreply.github.com>
2026-10-04 19:27:20 -07:00
Jinwoo Hong d9846e64fe fix(test): give async image-insert store mocks the openFiles list (#25357)
#25176 and #24489 landed together and collided in
insertRichMarkdownImageFromPath. #25176 added three test files whose
@/store mock returns { settings, folderWorkspaces, worktreesByRepo }.
#24489 made the same function look up the open document with
state.openFiles.find(...) to pass document-folder access to the import.
Under the mock, openFiles is undefined, so the lookup throws, the catch
shows "Failed to insert image", and no image is inserted -- 38 tests
fail on main.

The real store always carries openFiles, so production behavior of both
PRs is intact; the mocks were just written against the pre-#24489 shape.
Add openFiles: [] to the three mocks.
2026-10-04 21:39:22 -04:00
Brennan Benson 0a8c5722f3 fix(native-chat): start a new chat after an earlier start failed (#24917)
* fix(native-chat): start a new chat after an earlier start failed

A chat whose start the host refused stayed registered under its agent and
workspace, so the + menu and new-tab search kept that agent disabled with a
spinner, and any later "new chat" for that agent (for example "Send notes to
> New agent") restarted the failed chat instead and dropped its own prompt.

Only launches in flight now hold the agent/workspace slot that later starts
join. The registry keeps every launch by session id and derives the holder,
so a failed chat keeps its own Retry while a new start makes a new chat.

* test: import the launch status from its own module

* test: name the owning host on the launch intents the new tests build

* fix(native-chat): a new start never joins an unconfirmed or retried chat

A new start for an agent joined a chat whose create answer was lost, or a
failed chat whose Retry was in flight, and dropped its own text: the
source-control "Fix with AI" action reported success with nothing sent,
"Send notes to > New agent" sent nothing, and after a reload the text became
an unsent draft in the old chat.

Only a new start's own first create now coalesces later starts (double click,
two callers at once). Each attempt records whether it is that first create or
a Retry/re-check of an existing chat; an unconfirmed or retried blank chat
keeps its own Retry and a new start opens a new chat with its text. A resume
still re-checks or joins its conversation's launch, since the host refuses a
second adoption.

* fix(native-chat): a different new request always opens its own chat

A new start used to join any blank chat still in its first create or still
sending its opening text, so a second "Fix with AI" for another check, or
notes sent to a new agent, landed in that first chat. Each first attempt now
keeps the request it was started with (its text and whether it is sent or
drafted). Only a repeat of that request (a double click, a retried call)
joins it, and it shares the text already staged, so the text is sent once.
Any other request opens a new chat with its own text. The + menu, new-tab
search and send-notes menus disable an agent only when their own pick would
join. Resume launches are unchanged.

* fix(native-chat): an empty chat still starting takes the first text sent to it

A blank chat that is still starting (a + pick, the empty-workspace default
chat) is empty, so the first request with text, such as notes sent to a new
agent, now goes into it instead of opening a second chat beside it. That
request becomes the chat's own request: an identical repeat joins and is sent
once, and any other request opens a new chat.

Move the logic that decides which launch a start joins out of the launch
registry into its own module, so the registry stays under the line limit
once #24904 lands beside it. Pin that a repeat arriving while the opening
text is still sending is sent once.

* test: an empty chat delivers the claiming text the way its request asked

* fix(native-chat): only a chat its user has not used yet takes another request's text

A blank chat that is still starting was claimable by the first request with
text even after its user had sent a message into it or typed into its
composer, so notes or a Fix with AI prompt could land in a conversation the
user had already started. Emptiness is now read from what the chat holds:
nothing in its outbox and no text in its composer draft. A chat its user has
used stays theirs, and the request opens a new chat.

If the claiming text cannot be saved, the claim is not recorded: the request
falls through to a new launch, whose save failure shows on that chat as for
any new launch, instead of reporting a failure nothing showed.

* fix(notes): notes handed to a send leave the next send until it settles

Notes sent to an agent stayed in the notes shelf until their chat delivered
them, so a second "Send notes" made while the first new chat was still
starting collected the first notes again. With every different request now
opening its own chat, those notes reached two chats.

When notes or browser annotations are handed to a send (a new agent, a
running agent from the menu, or a sidebar agent row), they are held in
memory against that send's own delivery result and left out of the next
send. Delivered notes are removed as before; a failed, refused or
undelivered send releases the hold, so they come back for the next send.
The notes menu now builds each scope's prompt from the notes it will send.

* test: give the annotation tray fixture its hand-off callback

* fix(notes): offer no send when every note is already on its way

When every note or annotation in a send is held by an earlier send still in
flight, the built prompt is empty. The notes menu already disabled its
trigger then, but its New agent rows still started an agent with no text,
the annotation Send buttons still opened, and a sidebar agent row could be
sent an empty prompt. The New agent rows, the annotation Send buttons and the
sidebar send now offer nothing when there is nothing to send.

* fix(notes): a new chat's staged prompt decides when its notes leave the shelf

Notes sent to a new agent were released back to the shelf as soon as the
chat's start failed, while that failed chat kept the same text staged for its
own Retry. Re-sending and pressing Retry put the notes in two chats, and Retry
alone left delivered notes listed as unsent.

For a new chat, the notes now follow the prompt it staged: held while that
message is in the chat's outbox, cleared from the shelf when any dispatch
(Retry or re-check included) sends it on, and back on the shelf when the
chat is closed and its outbox thrown away. A paired server's chat is found
once its start settles. Running-agent sends keep their own result.

While notes are only on their way, the send button reads "Sending…" rather
than "All notes sent".

* fix(notes): a new chat's saved message keeps its notes out of another send across a reload

The hold that keeps notes sent to a new agent out of the next send lived only
in memory. A reload while that chat had not yet sent them put the notes back
on the shelf while the chat's saved message still carried their text, so a
second "Send notes" sent them twice.

The staged message now saves the send keys of the notes it was built from.
The shelf treats a note as on its way while any saved message carries its key,
read from the saved outboxes on first use and kept current by the outbox's one
write funnel. When a message carrying notes is sent on (its own start, a Retry
or the re-check), those notes are cleared from their shelf; when it is thrown
away with its chat, they come back. Browser annotations sent to a new chat use
the same keys while the app runs. Running-agent sends keep their in-memory hold.

This replaces the per-message watch and outcome promise from the previous
change.

* fix(notes): release a gone chat's notes, and clear sent ones in the web client too

Two gaps in keeping notes out of another send while a saved chat message
carries them:

- The web client never installed the clearing that removes notes once a
  chat sends them on, so after a Retry there the notes stayed listed. It is
  now installed, once per renderer, by the background services both the
  desktop and the web client load with App.
- A saved message carrying notes was thrown away only by this window's own
  close. A chat closed from the phone, another window or while Orca was off,
  or closed here without a known host, kept its notes held for good. A chat
  the host stops listing (affirmed, and not a launch still starting or
  failed) now has its queued messages thrown away once that sync lands, as
  does a close that can name no host, so the notes come back.

* Revert "fix(notes): release a gone chat's notes, and clear sent ones in the web client too"

This reverts commit ae0b9f3fea.

* Revert "fix(notes): a new chat's saved message keeps its notes out of another send across a reload"

This reverts commit b88d0dc2cc.

* fix(native-chat): a new chat joins only a re-delivery of the same user action

"The same request" was recognised by its content (agent, workspace, trimmed
text, sent or drafted), which split one action whose text changes between
deliveries ("Fix with AI" re-fetches logs) and merged two different actions
that happen to carry the same text.

Each user action now mints one request id where it is handled (the click,
menu pick, notes send, shortcut, Fix with AI press, quick command; a worktree
create uses its creation id; programmatic starts mint once at their entry)
and passes it through the launch plan, its verdict and the structured launch
options, where it is required. A new start joins a chat only when its first
attempt carries the same id: a double click or a caller retrying its own call
makes one chat and sends the first delivery's text once; any other action
opens its own chat, whatever its text. An empty chat that is still starting
is still claimed once by the first action with text, and then belongs to that
action's id. Resume launches keep joining their conversation's launch.

The + menu and new-tab search no longer grey out an agent while one of its
chats starts: every pick is a new action and opens its own chat. The id lives
in memory only; nothing reads it after a reload (a restored launch is reached
by its session id through Retry, never joined).

* fix(native-chat): a new chat with no text reuses an empty one instead of stacking another

Two bare "+ > Claude" picks (or new-tab search, the new-agent shortcut, the dashboard) opened two
empty chats. A request with no text now focuses an existing empty chat for that agent in that
workspace: one still starting (joined in the launch, as a re-delivery is) or one that published and
sits idle (its host's journal holds no request, read live from the status feed). Empty also means
nothing queued, no composer text or images, and no launch draft its composer has not taken. Failed,
unconfirmed, resumed and other-agent/workspace chats are never reused. A request with text still
opens its own chat, or claims an empty starting one as before; request-id dedupe is unchanged.

* fix(native-chat): a new chat with no text reuses an empty chat only in the split it was opened from

Pressing + in the right-hand split focused an empty chat sitting in the left-hand split. The reuse
now looks only in the tab group the pick targets (the caller's group, else the workspace's active
group, which is where its tab would open); a pick in another split opens a new chat there. Applies
to both an empty chat still starting and one that published and sits idle.

* fix(native-chat): text sent to a new agent claims an empty starting chat only in its own split

Notes sent from the right-hand split could land in, and focus, an empty chat still starting in the
left-hand one. A request with text now claims an empty starting chat only in the tab group it opens
in, the same rule a request without text follows; elsewhere it opens its own chat there.
2026-10-04 18:12:10 -07:00
github-actions[bot] ccfc8fe2c4 Update README downloads badge 2026-10-05 01:10:28 +00:00
Jinwoo Hong 6f8eee3776 fix(test): pass getInsertionRange in the image-insert access test (#25351)
#25176 replaced insertPos with getInsertionRange and #24489 added a test
using insertPos; together they broke main's typecheck.
2026-10-04 21:08:15 -04:00
Brennan Benson 51fe6f3fba fix(editor): restored tabs for files outside your projects no longer fail with Access denied (#24489)
* fix(editor): read files outside projects without a grant a restart loses

A file opened from outside every project (e.g. ~/notes.txt from the floating
workspace) read through an in-memory grant. After a restart the restored tab
only renewed that grant when it stored a full path, so a tab saved relative to
the floating workspace folder failed with "Access denied" and Retry repeated it.

Single-file reads (read, stat, exists) and open-editor-tab saves now resolve a
path outside every project in place. Paths inside a project keep the full
containment check, so a project's symlinks still cannot escape it, and every
other write stays inside projects.

* fix(editor): re-grant restored floating-workspace tabs by owner, not path shape

Problem: a file opened from the floating workspace (e.g. ~/notes.txt via
Cmd-click in the floating terminal, the floating markdown picker, or a .md
opened from the OS) loads until restart, then shows "Access denied: path
resolves outside allowed directories". Main's external-path grants live only
in memory. On restore the editor re-granted only tabs that stored an absolute
path, but floating tabs store a path relative to the floating root (~ by
default), which is deliberately not an authorized root, so they were never
re-granted. Restored floating notebooks also failed to start a kernel.

The previous commit on this branch let main read and save any path outside a
project without a grant. That widened fs:readFile/stat/pathExists for every
caller, including automatic reads of untrusted content (markdown preview
images), which opened a Windows UNC credential leak and a /dev/zero
main-process memory blowup. This reverts that model entirely.

Fix: one helper decides which client-local path a tab needs re-granted by
ownership: a floating-workspace tab, or a tab stored outside its own project.
It never grants paths a local project root covers (a grant would also
authorize a project symlink's outside target), and skips SSH-owned,
runtime-owned and not-yet-hydrated owners. Every reader that can touch a
restored tab before or without the editor loader uses it: the loader, the
restored dirty-tab conflict scan, and the paired-mobile markdown bridge.

* fix(editor): let main decide which restored-tab paths a project already covers

Problem: the restore re-grant helper decided "already inside a project" in
the renderer from its worktree list. At startup that list only holds repos
the session references, so a floating tab inside an unlisted repo was granted
(including a symlink's outside target), and the renderer's path matcher
disagrees with main's on WSL \\wsl$ vs \\wsl.localhost, which stranded a
folder-workspace tab with "Access denied" after restart. The helper also
treated a folder workspace with a missing or ambiguous host as local.

Fix: the renderer now decides only by owner (a floating-workspace tab, or a
tab stored outside a project whose owner is explicitly local) and asks main
with `skipIfInsideAllowedRoots`. Main checks the path against its own allowed
and registered roots, in both the named and canonical-parent spelling against
both root spellings: a path a project covers gets no grant, an alias spelling
of a project path gets only that spelling, and a project symlink's outside
target is never granted. Explicit-open grants (Cmd-click, drag, explorer) are
unchanged. Tests now prove each reader waits for the grant before reading.

* fix(fs): decide a restored tab's project membership from every ancestor's real path

Problem: the restore re-grant decided "inside a project" from the named path
and the real path of its parent only. When a tab path crossed a project
directory symlink and named the project through a spelling that was neither
the registered root nor its realpath (a second alias, a `..` segment, a case
variant on a case-insensitive disk, a /var-style alias of an ancestor), no
check matched, the path took the full grant, and the symlink's outside target
became readable and writable.

Fix: a path is inside a project when the named path is inside a root, or the
real path of any ancestor folder is inside a root in its registered or real
spelling. Such a path gets at most its named spelling, never its realpath. An
ancestor that fails to resolve for any reason other than "missing" now fails
closed to the named-spelling grant instead of falling through to the full one.
Tests cover each spelling; the non-symlink case also runs on Windows.

* fix(fs): read local files as regular files only, from one bounded handle

Problem: fs:readFile stat'ed a path and then read it to EOF. A character
device such as /dev/zero reports size 0, passes the size limit and never ends,
so the main process buffers until memory runs out; a FIFO hangs the open.
Writes could also target an existing device or FIFO.

Fix: every local fs:readFile (editor and log snapshot) opens the path once,
non-blocking, refuses anything but a regular file, and reads the size check,
binary probe and content from that same handle, capped at the limit even if
the file lies about its size. The AI Vault log tail opens non-blocking too,
and fs:writeFile refuses an existing non-regular target.

* feat(fs): let desktop file requests declare their shape

Problem: main decided every local file request against one allow-list plus a
set of in-memory grants the renderer had to recreate after every restart, so
a file the user opened outside a project (for example from the floating
workspace) was denied once Orca restarted.

This adds the request shape the common pattern uses, alongside the grants for
now:
- no shape (the default): the path must be inside a project root main
  recognises, symlinks included. Desktop requests also accept the app-owned
  floating-workspace folder; paired-client RPC never does.
- user-file: a single file the user named by absolute path, used in place.
  Only fs:readFile/stat/pathExists and saving (fs:writeFile) accept it.
- document-resource: an image or PDF a document references, limited to every
  project root when the document is in one, else to the document's folder,
  and refused by path text before any disk or network access.
Notebook kernels and AI Vault log tails check their open file as user-named.

* feat(editor): send each local file request's shape from the renderer

Problem: after a restart, a tab opened outside every project (a floating
workspace file, a file opened by absolute path, an OS-opened markdown) could
only be read if the renderer first re-granted its path, and readers that ran
before the editor loaded the tab had no grant at all.

The renderer now says what kind of request it is making, and main checks that:
- A persisted tab opened outside its owner's root (floating workspace, or an
  absolute stored path) whose owner is explicitly local reads and saves as a
  user-named file, from every reader: the editor loader, the restored-tab
  conflict scan, the change banner and compare dialog, the paired-phone
  markdown bridge and the save queue. Project tabs stay inside their root.
- Clicks, drops, typed paths and browser-opened notebooks stat as user-named.
- Markdown preview and rich-editor images are document resources, limited to
  the document's roots or folder. Images the user pasted or attached into a
  chat show as user-named; agent images stay inside the project.
- The image cache keys on the shape, so one shape's image never answers
  another's request.
A ratchet test lists every renderer file allowed to create a user-named
request.

* refactor(fs): delete the in-memory path grant system

Problem: main kept a set of paths the renderer had asked it to allow
(fs:authorizeExternalPath). The set lived only in memory, so a file the user
opened outside every project could be read until Orca restarted and was then
denied, and every new reader of a restored tab had to remember to recreate
the grant first. Three rounds of re-deriving grants at restore each found
another reader or path spelling it missed.

Now that every desktop request declares its shape, nothing needs a grant:
- delete the grant set, authorizeExternalPath, the restore re-grant from the
  earlier commits on this branch, the fs:authorizeExternalPath channel and its
  preload and web-client entries;
- delete every renderer grant call (terminal and markdown link clicks, drops,
  typed paths, the file explorer, AI Vault logs, chat attachments, browser
  notebooks) and every main one (floating markdown picker and folder, OS-opened
  markdown, keybindings.json, pasted images, import and upload sources);
- the floating workspace's picker-approved folders stay a terminal-cwd
  allowlist only.
Main now holds no per-path permission, so a restart can't change any answer.

* feat(editor): open project links that lead outside the project as named files

Problem: a file inside a project that is a symlink to something outside it
opened fine from the file explorer or a terminal Cmd-click, then showed
"Access denied" after a restart: its tab was stored as a project file, and a
project request is refused when it resolves out of the project. A folder link
out of the project expanded in the explorer until restart and then failed with
a raw access error.

Now the click decides and the tab keeps that decision. Both gestures stat the
path inside the project first; if only the user-named check passes, the path
leads out of the project:
- a file opens by its absolute path, so it reads and saves as a file the user
  named, the same before and after a restart;
- the explorer does not follow a folder link out of the project and says so
  ("This folder links outside the project, so it can't be opened here.").
Paths that stay inside the project still open as contained project tabs. Also
drops the AI Vault "path not authorized" message, which nothing shows now.

* chore: drop the casts the changed-code quality gate flags on this branch

The FileContent casts in the editor loader and the paired-phone markdown
bridge were never needed (the read result is already assignable). Tests stub
window.api through vi.stubGlobal and pass narrow stores without casting; the
one test store that still needs a cast states why.

* fix(fs): load chat images by type, and keep escaping project links readable

Problems found in review:
- Chat transcript images were trusted by message role: any user-role
  "[Image: source: <path>]" (an injected Claude record, `orca terminal send`,
  a paired client's image-ref) became an automatic user-named read as the row
  scrolled into view, of any file type, and on Windows a network-share path
  would have opened an SMB connection to that host.
- A document image named like an image but linking to a text file
  (logo.png -> .env) was read as text.
- Windows device names (NUL.png, COM1.jpg) passed the path-text check of the
  automatic image loads.
- A project symlink leading out of the project, opened by a typed path, a
  tab-strip drop or a browser file:// notebook, was stored as a project tab
  and immediately refused.

Fix:
- New chat-image request shape for every transcript image and the composer
  preview, whoever's turn named it: an absolute local path whose requested and
  real targets are image files, a regular file, size-capped; network-share and
  device-namespace paths and Windows device names are refused by path text
  before any filesystem call. Pasted screenshots still show after a restart,
  and agent images outside the project now render.
- Document resources check the real target's type too, and refuse Windows
  device names by path text.
- Typed paths, tab-strip drops and browser notebooks stat through the same
  check as the explorer and terminal, and open an escaping link by its
  absolute path.
- Tests pin the shape at the change banner, compare dialog, markdown preview
  and image prewarm; a second ratchet lists every file that can open a tab the
  tab rule reads as user-named, and its comment says what it can't see.
- Stale grant wording removed.

* fix(fs): tighten automatic image loads and the project-link check

Problems found in review:
- Two unit tests went red on this branch: the browser-share test still
  expected reads without a shape, and the rename test's electron mock had no
  app, which the desktop root check now needs.
- The device-name check ran on the raw path, so `NUL.png\.` or
  `COM1.png\x\..` (reachable from markdown `![](NUL.png%2F.)`) reached the
  filesystem; a document image whose real target was a device name passed.
- Chat images in a project that lives on a Windows network share no longer
  rendered, though the markdown preview showed them.
- Any failed project check (a missing file, a dropped connection) was taken
  as "this link leads out of the project" and opened as an absolute tab.
- Every local read allocated about 2 MiB, even for a tiny image.

Fix:
- Device names and device-namespace paths are checked on the resolved path
  and on the real target, for chat images and document resources alike.
- A network-share path in an automatic load is read only inside a project
  root (the user chose that share when adding the project); anywhere else it
  is still refused by path text before any filesystem call.
- Only main's "outside allowed directories" refusal marks a project path as
  leading out of the project; other errors surface as before. The message now
  lives in shared code so both sides agree on it.
- Reads size their first buffer from fstat and confirm EOF with a 1-byte
  probe; a file that grows past its reported size is still read in bounded
  chunks up to the cap.
- Fixed the two red tests.

* refactor(fs): name file access by its role, not its structure

Problem: the static-analysis anti-slop check failed the PR because the new
code named the request's file access a "shape" (`shape`, `RequestShape`,
`TabShape`), which describes structure rather than the role.

Rename the main-process module filesystem-request-shape.ts (and its tests) to
local-file-access-resolution.ts, rename the symbols to fileAccess,
FileAccessResolution and TabFileAccessFields, and say "file access" or
"access kind" in the comments and test names. No behaviour change.

* fix(fs): refuse every Windows device-name spelling in automatic image loads

Problem: the device-name check split a file name only on '.', so names such
as NUL:.png, COM1:.png, NUL:stream.png (an alternate data stream) slipped
through, and CONIN$, CONOUT$, CLOCK$, COM0 and LPT0 were not listed. Those
reached the filesystem from a document or chat image before being refused.

Split on ':' as well, list the missing device names, and test each with
Windows path rules and zero filesystem calls. Also cover the case of a local
link that leads onto a network share outside every project (refused for chat
images), and correct the shared comment on chat-image access.

* fix(editor): let users rename and insert images into files opened outside projects

Renaming a file opened outside every project (tab double-click, editor
header) and inserting an image into such a markdown document failed with
"Access denied", even before a restart: both writes only passed the
project-root check. Document resources and chat images were also limited
by file type more strictly than users expect.

- Add a "document-folder" access kind for writes beside a document the
  user opened: main allows renaming only that document, to a name inside
  its own folder, and importing new files only into that folder, checked
  by path text and again by real path, with Windows device names refused.
  The renderer sends it only for local user-named, writable tabs (rename,
  its undo/rollback, image insert); SSH and runtime requests never carry it.
- Document resources: drop the image/PDF type allowlist; folder
  confinement, regular-file reads, the cap and path-text refusals remain.
- Chat images: judge only the real target's type, against every
  previewable image type (AVIF added).

* fix(fs): a declared file-access kind never refuses what the project check allows

A full-path tab for a file inside a project (for example a link that
leads out, opened by its absolute path) was renamed under the
document-folder rule, which limited the new name to the file's own
folder, although the same rename with no declared access could move it
anywhere in the project. Any declared kind could be stricter than the
default in the same way.

Every desktop local file request now goes through one resolver,
resolveLocalRequestPath: it runs the default project check first (roots,
Orca's floating folder, symlink containment, outside-root path text
refused before any filesystem call) and only on a refusal applies the
declared kind's rule, which adds paths outside projects. Reads, saves,
rename source and target, and import destinations all use it, so a new
kind gets the rule for free. Automatic loads (document and chat) still
refuse Windows device paths and names by text first, even inside a
project; a device is never a file to show.

The document-resource rule no longer needs its own project branch, and
chat images no longer re-run the roots check for shares.

* fix(fs): symmetric outside-project renames, notebook real folder, same-share images

- Renaming a file opened outside every project accepted a name in a
  subfolder (`archive/todo.md`), but the Undo and the rollback rename,
  declared from the moved file, were then refused and the file stayed
  moved. A rename under document-folder access must now land directly in
  the document's own folder (checked by path text before any filesystem
  call), so rename, Undo and rollback are symmetric. Image import still
  accepts the folder or a folder under it. Inside projects the default
  check still allows any in-project target.
- A notebook opened through a link inside a project started its kernel in
  the link's folder instead of the real file's folder (main's behaviour),
  because notebook and AI Vault log-tail paths skipped the project check.
  Both now resolve through resolveLocalRequestPath (project check first,
  then the user-file rule).
- A markdown file opened from a Windows share outside every project could
  not show the images beside it. Document images on a share are now
  allowed inside the document's own folder; the folder text check refuses
  every other host and share before any filesystem call.
- resolveDesktopAuthorizedPath is async, so a synchronous failure in the
  default check rejects like any other refusal.

* fix(fs): refuse share images outside projects again; keep renames and kernels as on main

- Reverts the same-share document image rule from the previous commit.
  Its folder check compared hosts case-insensitively, so a host spelled
  with U+212A KELVIN SIGN (or a decomposed accent) passed as the
  document's own share and was contacted, reopening the network
  credential leak. Document and chat images on a share outside every
  project are again refused by path text before any filesystem call;
  tests now cover the look-alike hosts with zero filesystem calls.
- Renaming a file opened outside every project into a project folder
  passed the project check, but its Undo (declared from the new path)
  was refused and the file stayed moved. When the rename source is
  allowed only as the opened document, the new name must now land
  directly in the document's folder even if a project would accept it
  (resolveLocalRenamePaths).
- A notebook opened through a link outside every project started its
  kernel in the link's folder; main used the real file's folder. The
  kernel cwd is now the real file's folder in every case.

* fix(fs): a file opened outside every project renames to any path, and keeps its access

Renaming a document the user opened (floating workspace or full-path tab) now
follows the user-file rule: the source must be the opened document, and the
new path can be any absolute path, so a rename into another folder, a
subfolder or a project works, and its Undo (declared from the moved file)
comes back from there. Any other rename keeps the project check only. Remove
the same-folder rename rule and its tests; image import stays in the
document's own folder.

After a move, a tab stored by its full path keeps its full path instead of
being recomputed project-relative, so it keeps user-file access for save,
the next rename, image insert and restore after restart. Folder moves go
through the same remap.

Also un-export unused resolver exports and avoid a copy for single-chunk reads.
2026-10-04 16:55:32 -07:00
Neil 955dce5a5a Keep workspace deletion dialogs steady while changes load (#25321)
* Keep workspace deletion warnings from shifting the dialog

* Tighten spacing in workspace deletion confirmations

* Address deletion dialog review and synchronize localization catalogs
2026-10-04 16:49:12 -07:00
+2 8e5080c132 Validate OpenCode worker model preferences on the execution host (#24624)
* feat(orchestration): support OpenCode worker model selection

Allow supervised OpenCode workers to use per-launch model overrides through the existing launch-preference and receipt path.
Preserve existing OpenCode agent arguments while replacing only model flags, and reject unsupported effort values explicitly.
Keep Native Chat option exposure unchanged and update the worker CLI and orchestration guidance.

* fix: wait for OpenCode worker composer before first dispatch

Reuse captured composer readiness on local and paired execution hosts and revoke launching-shell paste anchors.

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>

* feat(opencode): probe execution-host CLI capabilities

* fix(opencode): select plugin default for execution host loader

* fix(opencode): limit prompt prefill capability to verified release

* feat(orchestration): gate OpenCode worker model preferences by host capability

Co-authored-by: user141514 <user141514@users.noreply.github.com>

* feat(opencode): probe launch capabilities on the execution host

* feat(opencode): probe execution-host CLI capabilities

* feat(opencode): probe launch capabilities on the execution host

* feat(orchestration): resolve explicitly configured command aliases

* fix(opencode): select plugin loader for the launched host binary

* fix(opencode): match WSL probe cwd and declared guest environment

* fix(orchestration): verify available OpenCode model on execution host

* fix(opencode): preserve launch environment deletion boundaries

* wip(opencode): authorize native startup prompt intent at execution owner

* fix(opencode): atomically replace status plugin entrypoints

* fix(opencode): retain plugin permissions across restrictive umasks

* test(opencode): resolve permission fixture from primary cwd

* feat(opencode): install startup prompt plugin independently of status hooks

* fix(opencode): wait for admitted startup intent and preserve failed-launch briefs

* fix(opencode): confine overlay manifest cleanup to owned directories

Co-authored-by: Adnan Khan <adnank11427@gmail.com>

* fix: wait for OpenCode worker composer before first dispatch

Reuse captured composer readiness on local and paired execution hosts and revoke launching-shell paste anchors.

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>

* feat(opencode): probe execution-host CLI capabilities

* fix(opencode): select plugin default for execution host loader

* fix(opencode): limit prompt prefill capability to verified release

* feat(opencode): probe launch capabilities on the execution host

* fix(opencode): select plugin loader for the launched host binary

* fix(opencode): match WSL probe cwd and declared guest environment

* fix(opencode): preserve launch environment deletion boundaries

* wip(opencode): authorize native startup prompt intent at execution owner

* fix(opencode): atomically replace status plugin entrypoints

* fix(opencode): retain plugin permissions across restrictive umasks

* test(opencode): resolve permission fixture from primary cwd

* feat(opencode): install startup prompt plugin independently of status hooks

* fix(opencode): wait for admitted startup intent and preserve failed-launch briefs

* fix(opencode): unsubscribe hook settings during async host shutdown

* fix(opencode): confine overlay manifest cleanup to owned directories

Co-authored-by: Adnan Khan <adnank11427@gmail.com>

* test(readiness): census recorded OpenCode composer boots

* fix(opencode): reject redirected overlay parents before cleanup

* fix(orcad): retain runtime cleanup when subscribing to hook settings

* refactor(launch): extract OpenCode config and attachment authority

* fix(opencode): retain host version selection across relay restarts

* fix(opencode): pass run prompts as positional messages

Preserve run flags and use the existing shell quoting and run-command detector
to append the initial message after --, reusing an existing separator.
TUI launches retain their version-selected prompt transport and draft behavior.

Original run-order work: @coelho-doti (#13065, tracked in #17551).

* fix(opencode): keep wrapped run tasks positional

Recognize supported environment prefixes and PowerShell call operators without
mistaking prompt arguments for executables. Keep environment and run separators
separate, preserve the task text and exclude run commands from native submission.

Source-parent: 23fc08b4e9
Related-to: stablya/orca#17551
Credits: @coelho-doti (stablya/orca#13065)

* Prepare complete private OpenCode launch validation source

Integrate the complete reviewed readiness, capability, native prompt, overlay and positional-run source onto frozen main. Preserve canonical atomic ACL retry, status generator/disposal, restrictive-umask fixtures and unowned source. Keep supported wrapped run commands positional.

Private-validation-source: a44345ce49
Original-full-source: 23fc08b4e9
Original-core-base: 8186ded0bd
Frozen-main: 08ee7ba9ef
Owned-source-paths: 111
Publication-policy: private validation only; preserve the six separate PR boundaries and held model/provider drafts

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Adnan Khan <adnank11427@gmail.com>
Credits: juli-gonzalez readiness contribution; Ahmed Nagy atomic plugin writer; coelho-doti positional run contribution

* Prepare private complete 111-path launch validation on current main

Private validation only. Preserve main credential additions and original launch ownership. Held model and provider topics remain excluded.

* Recognize env options before positional OpenCode run messages

* STRICT launch CI contract correction

* CAPS launch CI contract correction

* INTENT launch CI contract correction

* test(opencode): wait for malformed claim retries before expiring intent

Observe real endpoint I/O completion under fake timers before forcing expiry.

* test: initialize Claude prompt state in output retention fixture

* Verify OpenCode catalog model launches and preserve current launch behavior

* Verify OpenCode catalog model launches and preserve current launch behavior

* Wait for OpenCode location hydration in intent startup

* Refuse unverified new-worktree OpenCode model launches and record startup attribution

* fix(opencode): bind startup readiness to the composer location

* Bind OpenCode startup readiness to the current location in intent startup

* Restore the owning Orca CLI path after shell profiles

* Use a literal marker for the Bash lookup regression

* Preserve plain panes and initialize zsh after prompt hook replacement

* Preserve user line-editor dispatchers during deferred startup

* fix: retain CLI startup when global Zsh replaces prompt hooks

* test: replay global Zsh hook replacement after host startup

* test: isolate controlled Zsh widgets from distro keyboard setup

* fix(shell): preserve user hooks during deferred zsh initialization

* Retry interrupted OpenCode startup prompt claims

* Keep completed Zsh startup hooks retired when the wrapper is sourced again

* Reject truncated OpenCode catalogs and explain worktree model limits

* Use a template literal in truncated-catalog coverage

* Refuse unfinished OpenCode model catalogs

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: user141514 <user141514@users.noreply.github.com>
Co-authored-by: Ahmed Nagy <ahmednagy25t@gmail.com>
Co-authored-by: Adnan Khan <adnank11427@gmail.com>
Co-authored-by: Orca startup hydration review <agents@stably.ai>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Orca maintenance <orca-maintenance@users.noreply.github.com>
Co-authored-by: Orca <dev@stably.ai>
Co-authored-by: Orca campaign <orca-campaign@local.invalid>
Co-authored-by: Orca OpenCode issue campaign <opencode-campaign@stably.ai>
Co-authored-by: OpenCode issue campaign <codex@localhost>
2026-10-04 16:44:38 -07:00
Neil e2460907c3 Preserve image clipboard targets and content across editor changes (#25176)
* Preserve image insertion targets with one captured destination

* Set image paste test caret through the editor selection
2026-10-04 16:39:12 -07:00
Neil d3afb5c5a9 Preserve large paste destinations and native Undo boundaries (#25177) 2026-10-04 16:22:23 -07:00
keiandsetodeve cecb62158a fix(ui): restore IME Enter protection in workspace details (#24099)
Restore IME Enter protection in workspace details by reusing the existing composition tracker. Reset Notes ownership at textarea detachment and preserve sizing behavior. Repair isolated native test-window delivery without changing the production foreground policy or original native input assertions.

Fixes #24097

Related contributor history: #10711, #11067, #13128, #13282.
Original implementation and macOS recordings: @setodeve, commit b30f095.
Verified on required stock Linux X11/Wayland checks and independent frozen-source review.

Co-authored-by: setodeve <keinick11@outlook.com>
2026-10-04 16:21:04 -07:00
PM 75344e5850 docs: align contributor guidance with the PR template (#25034) 2026-10-04 16:19:53 -07:00