Commit Graph
11661 Commits
Author SHA1 Message Date
5802b54579 fix(rate-limits): read OpenCode Go usage with the Go API key (#22551)
* fix(rate-limits): read OpenCode Go usage with the account API key

Since OpenCode's console migration (upstream fe51b0b19a, "fix(console):
restrict legacy access to Black"), an account with no Black subscription
is redirected from the legacy console to /console/login, so Orca's
cookie-based workspace lookup returns nothing and the Go bar stays empty.

Fetch usage from GET https://opencode.ai/zen/go/v1/usage instead, which
authenticates with `Authorization: Bearer <key>` and needs no console
session. The key resolves in order: Orca settings override,
OPENCODE_API_KEY, then whatever OpenCode itself stored on /connect --
auth.json for 1.x, the credential table for 2.x. The cookie path stays
as the fallback so Black/legacy accounts keep working.

A 403 EntitlementError now reads as "no OpenCode Go subscription" in the
status bar instead of a generic refresh failure (#22257's reporter was
misled by exactly that).

* fix(rate-limits): prefer OpenCode's stored Go key over OPENCODE_API_KEY

OpenCode applies the key saved on /connect after the environment, so the
stored key is the one its own Go requests use. OPENCODE_API_KEY is also
the Zen provider's variable, so ranking it first could read a key that
OpenCode itself is not using for Go.

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

* fix(rate-limits): name the API key when OpenCode Go usage lands on sign-in

A redirected usage request arrives as a 200 sign-in page because Electron
follows redirects; report it as a rejected key instead of a parse failure.
The cookie path's empty workspace lookup is what non-Black accounts now
hit after the console migration, so its message points at the API key
rather than only the workspace override.

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

* chore(i18n): add the OpenCode Go API key strings to the English catalog

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

* docs(rate-limits): stop calling the credential table an OpenCode 2 marker

Verified on two real Windows hosts running OpenCode 1.18.16: the `credential`
table exists there too (empty, same columns), so its presence does not identify
a 2.x install. Neither host had an `auth.json` at all.

The resolution already probes both stores on every version, so only the comments
were wrong. Says so now, and records that a 2.x install which never ran the
legacy import has no `auth.json` either — which is why both tiers exist.

* refactor(shared): move GhosttyImportPreview out of global-settings-types

Adding `opencodeGoApiKey` pushed global-settings-types.ts one line past the
300-line ceiling, failing `oxlint` in CI. AGENTS.md forbids a max-lines
suppression, so split instead: the Ghostty import preview is a distinct concern
that never belonged in the settings-shape file.

Re-exported from the original module so no importer changes. 293 code lines now.

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-09-23 20:09:02 -07:00
Neil 57bf732a42 test(opencode): cover the opencode2 host-env branch and stop inheriting ORCA_OPENCODE_AGENT (#22547)
* test(opencode): pin per-major OpenCode overlay selection on WSL and the relay

OpenCode 2 dies with "Duplicate plugin ID" when two plugin files share an id,
so which overlay a guest or remote pane is pointed at decides whether the agent
starts. Nothing failed if that selection regressed:

- the WSL spawn path never asserted which major it asks the guest relay for,
  and the shared pty-ipc mock had no openCode2HookService at all, so no test
  could reach the opencode2 branch of buildPtyHostEnv;
- requestGuestOpenCodeOverlayDir had no coverage for the v2 guest dir;
- PluginOverlayManager had no case for a remote config root that still holds
  the other major's stale Orca plugin.

Tests only; no behavior change. Each new case was mutation-checked against the
production line it guards.

* test(opencode): stop the plugin contract test inheriting ORCA_OPENCODE_AGENT

The generated plugin self-disables when ORCA_OPENCODE_AGENT names a different
major, and the contract test saved and restored that variable without ever
setting it. Run from a shell that has it — which is any shell inside an Orca
OpenCode pane, i.e. how this repo is usually developed — the plugin returned an
empty hook set and the contract failed for the wrong reason.

Delete it in beforeEach, the way the opencode2 setup test already pins it.
Verified the file passes with the variable set to either major and unset;
before this it failed for two of the three.
2026-09-23 20:08:34 -07:00
Neil b0ae7d18a0 fix(opencode2): resolve subagent session lineage so child work stops taking over the pane (#22444)
OpenCode 2's plugin adapter unwraps a single-property `{ data }` success schema,
so `ctx.session.get` resolves to the bare session record. The shared lineage
lookup only accepts `result?.data?.id === sessionID`, and OpenCode 2 has no
`session.list` fallback, so `resolveRootSessionID` returned null for every
session and `childState` was permanently null.

With unknown lineage `canFailOpen` is true for attention events, so a subagent's
`permission.asked`/`question.asked` fell through and pinned an un-evictable
blocker keyed to the child's own session id — publishing a subagent as if it
were a root. Observed in hook posts: SessionBusy for a child session id whose
`session_v2` row carries a parent.

Envelope the result in the OC2 client shim so the shared lineage module works
unchanged; OpenCode 1 already receives enveloped results and is untouched.

Also adds `opencode2` to the double-Escape interrupt list, extracted into one
shared helper so the server inference and renderer gate cannot drift. A single
Escape was inferring an interrupt, and Escape is how the Subagents dock closes.

7 of 11 new lineage tests fail without the shim.
2026-09-23 20:08:01 -07:00
Brennan BensonandClaude c4dfd9deef fix(claude): resume a native chat from its real latest message (#22395)
* fix(claude): resume a native chat from its real latest message

Claude's last-prompt marker names the chain tip, which is often a stop-hook
summary or attachment row that --resume-session-at rejects. The branch proof now
resolves the marker to the latest main-chain message, and a plain resume
re-derives its point from the transcript instead of trusting the stored cursor,
resuming by session id alone when the transcript cannot vouch for one. An
acquisition release now reads the transcript tail like close and exit do.

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

* fix(claude): advance the durable resume point at every turn end

A completed turn now writes the live main-chain message uuid onto the owner's
head provider-handle link in place, so a host that dies before its close path
runs still resumes from its last completed turn and the chain does not grow per
turn. The write is serialized per session, only logged on failure, and close and
exit persist after it settles.

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

* fix(claude): retry a failed turn-end resume write and ignore results after exit

A turn-end write that failed was never retried when the next turn ended at the
same point, because the memo of the last attempted leaf outlived the failure.
A result frame delivered after the child's exit could also start a write that
landed behind the exit path's transcript-derived cursor, moving the durable
point backwards. The failed leaf is now forgotten so the next turn end retries
it, and a turn-end write only runs while the session is still the published
live owner.

The session-id fallback on a plain reopen and a failed durable write on the
unexpected-exit path now log why, instead of leaving no trail.

* fix(claude): resume a native chat by session id and stop predicting Claude's marker

A plain reopen now passes only the session id, so Claude continues from the
real end of its own conversation. Orca's saved leaf is its own record of the
last completed turn, taken from the live stream at each turn end. It is
bookkeeping (the reconciliation anchor), never a resume argument.

- Launch resolution resumes by id and checks only the session id; the prior
  head leaf is carried into the publication link.
- Remove the launch-time transcript re-derivation.
- Close, unexpected exit, and acquisition release no longer read the
  transcript. They wait for any in-flight turn-end write, then persist the
  last completed turn, so a crash mid-turn never saves a half-turn prompt.
- The delivery-reconciliation window walks from the file's last main-chain
  transcript row to the anchor instead of Claude's lagging marker, so a
  prompt Claude saved just before a crash reconciles as accepted.
- Revert the transcript branch graph to main; the terminal handoff readers
  keep their semantics.
- Report Claude rewind as unsupported. Its marker-based proof can never pass
  on the real binary, and no app screen calls it. Remove the Claude rewind
  launch, proof, and recovery path. A pending Claude rewind left by an older
  build is settled as refused on the next attach, which resumes by id; a
  failed settlement is logged and never blocks the chat.

* fix(claude): drop the stale resume-cursor wording from the restart-resume note

The restart path resumes Claude by session id alone now; the module comment
still described the old resume-at cursor.

* refactor(claude): prove the file-tail resume tip inside the branch graph

The reconciliation readers proved the transcript tip by re-parsing every
line and feeding a synthetic last-prompt row through the graph, doubling
parse cost on every reopen and coupling the tail path to the marker's
JSON shape. The graph now takes tip: 'file-tail' and tracks the last
main-chain row from its own parse; marker mode is unchanged and the
no-eligible-tail fallback keeps the exact marker semantics.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-23 20:00:13 -07:00
Neil b7a4fee700 fix(agents): stop claiming an unconfirmed OpenCode handoff succeeded (#22546)
* fix(opencode): stop claiming a handoff prompt was delivered when it was written blind

"Continue in New Session…" to OpenCode reported success even when the prompt
never reached the TUI (#22479). The paste-after-ready helper falls back to a
blind write when the composer-ready signal never arrives and only the agent
process is known to exist; that write was indistinguishable from a real
delivery, so the continuation showed its success toast.

- pasteDraftWhenAgentReady / pasteDraftToAgentPtyWhenReady report the blind
  fallback via onUnconfirmedDelivery, plumbed to launchAgentInNewTab as
  onPromptDeliveryUnconfirmed.
- The session continuation hedges instead of claiming success, and both the
  failure and the hedged notice offer "Copy prompt".
- OpenCode gets Codex's 20s composer budget. Both are quiet-window-less
  signals anchored on DECSET 2004, which ConPTY never forwards, so on Windows
  that budget is the settle delay before the blind paste.

* test(runtime): retarget the 8s startup budget test off OpenCode

The main-runtime startup-draft budget test used `opencode` as its stand-in for
"an agent without an override", which this branch invalidates by giving OpenCode
20s. It failed with "expected vi.fn() to not be called at all, but actually been
called 1 times" — the readiness signal now legitimately arrives inside budget.

Point it at `claude`, which still takes the 8s default, and add a companion
pinning OpenCode's 20s: a readiness signal at t+10s, past the old default, must
now deliver the draft. Removing the override makes that companion fail.
2026-09-23 19:47:47 -07:00
Jinwoo Hong 4064653740 refactor(browser): remove the unused screenshot-prep visibility helper (#22526) 2026-09-23 21:55:57 -04:00
Jinwoo Hong f2ac9f29b2 fix(browser): let pixel capture hold its own page drawn, without the desktop window (#22534)
* fix(browser): let pixel capture hold its own page drawn, without the desktop window

Screenshots were the last browser commands that still borrowed the desktop
window: they took the per-page automation-visibility lease, which waits for
two desktop-window animation frames (capped at 2 s) and never arrives when the
window is minimized or throttled. Only pixel capture actually needs a page
drawn — input, scripts, layout, the accessibility tree and PDF all work on a
hidden page.

Capture now takes a main-owned paint hold: a synchronous, per-page ref-count
that tells the renderer one way (no reply awaited) to keep the page drawn and
keeps the desktop renderer unthrottled while held. Both Orca's full-page
capture and the agent-browser helper's screenshots take it in
cdp-screenshot.ts and retry on a bounded schedule until the page answers with
a frame; a CDP error fails fast.

Deleted: the queue's needsPaint lease, the executeJavaScript acquire path and
its two racing 2 s timeouts and late-token cleanup, the renderer's rAF wait and
window bridge, the capture commands' own leases, the fixed 300/500 ms settle
waits, and the global one-screenshot-at-a-time lock.

Rebased onto main after #22528 landed; content identical to the reviewed
branch head 7b390ed6a8.

* fix(browser): probe for a frame instead of repeating the full capture

Retrying a capture resent the caller's full request, so on an already
drawn tall page (a full-page capture takes ~0.5 s) the 250 ms retry
started a second full beyond-viewport capture while the first was still
running. Measured on Electron 43: any later request makes a held page
produce a frame, and that frame answers every pending capture with a
full, correct image. So the capture is sent once and 1x1 probes follow
until it answers; their results are ignored.

Also report a detached debugger as detached rather than destroyed, and
give the layout-metrics timeout its own "did not respond" message, since
that request doesn't need a drawn page.
2026-09-23 21:17:46 -04:00
Neil 8352752e54 fix(opencode): give the opencode2 status plugin a distinct id (#22544)
* fix(opencode): fail-open plugin setup and distinct opencode2 plugin id

setup() threw when OpenCode 2 probed it without a full context, which the
TUI reported as an 'orca-opencode-status' plugin failure. Both plugin files
also shared one id while living in the same config dir.

* fix(opencode): drop unused oxlint-disable in setup fail-open test

* test(opencode): pin distinct plugin ids for the shared global config dir

Orca installs both family plugins into one global plugins dir, so a shared
plugin id makes OpenCode 2 fail the later one with 'Duplicate plugin ID'.
2026-09-23 17:51:00 -07:00
Brennan Benson 80f5aae0f9 feat(agent-status): publish the main agent's own state beside the combined row state (#22452)
* feat(agent-status): publish the lead agent's own state beside the combined row state

Every status producer folded the main agent's state together with live child
work into one `state`, so a lead that had finished while a subagent still ran
read `working` and its own state was lost. The row now also carries
`lead: { state, outcome?, stateStartedAt }`, admitted by the one payload
normalizer on the relay wire, IPC and disk, and published from the Claude hook
lane, the structured host ingest and renderer bridge, Grok (now on the shared
fold) and Codex (own combine kept). The persisted child-only boundary flag is
derived from `lead` plus child evidence and no longer written; old rows map
onto `lead` at hydrate. Combined `state` and `workingMode` are unchanged for
every reader; a cross-lane parity table pins that, with the cancelled-turn
watch-loop story recorded as a known divergence.

* fix(agent-status): make Orca's inferred interrupt the primary source of a Claude lead cancellation

Current Claude Code sends no hook at all on a cancel and no is_interrupt on
Stop, so the cancellation enters the lead record from the server's inferred
interrupt and rides into the next real Stop; is_interrupt on a turn boundary
stays as the secondary source for builds that send it. Comments, the store
reference and the parity table say so; no suppression changes.

* docs(agent-status): the child-only boundary comment now describes the persisted shell fact

The old sentence said a hydrated row no longer carries the shell fact, which is
the opposite of the mechanism: claudeRunningNonAgentTask is persisted precisely
so hydration can read it, and only a pre-lead row lacks it — reading as
shell-free, the same assertion its legacy flag made at write time.

* rename the lead fact to mainAgent: the main agent's own state

* docs(agent-status): the inferred cancel comes from Ctrl+C, not Esc

* fix(agent-status): an inferred interrupt keeps an already settled main agent, and the row verdict docs name its inferred source

* fix(agent-status): a child-induced wait publishes the main agent state it displaced

* fix(agent-status): decide child-held Claude rows from the saved main agent fact

Restart seeds the Claude main agent from the row's saved mainAgent whenever it
settled and no shell held the row, instead of re-deriving a child-only shape.
OSC cannot settle or repaint a row child agents hold open, including a row
waiting on a child's permission prompt. A sticky child permission prompt still
records the main agent's own progress, and OSC repaints and inferred answers
keep the shell fact beside the main agent they preserve.

* fix(agent-status): keep a finished turn's main agent verdict and clock with that turn

A Claude SessionStart restarts the main agent's clock instead of inheriting the
previous session's last Stop. A Grok idle prompt or session end, and a late
Codex root Stop after an inferred cancel, restate the same finished turn, so
they keep its recorded verdict; only a new turn clears it.

* test(agent-status): publish the Grok verdict restatement past the late-event window

* docs(agent-status): describe hydrate seeding and the OSC refusal from the saved main agent fact

* fix(agent-status): push a held child permission row when its main agent changes

* fix(agent-status): keep the shell fact on a held child permission row so restart does not settle it

* docs(agent-status): note the held child permission row carries the shell fact and is pushed

* fix(agent-status): pair the Claude shell fact with the main agent at the one row-build point

Every non-hook rewrite (terminal-title repaint, inferred answer, held child
permission) had to re-carry the shell fact beside `mainAgent`, and each one that
forgot let a restart settle a row while a shell still ran. The row builder now
pairs the fact once: a listener event restates it, any other write keeps it only
while `mainAgent` is unchanged. Restart seeds a settled main agent only when the
row says no shell ran, and legacy child-only rows map to that explicitly.

A held child permission now also accepts the main agent event's background
evidence, as it already accepts its `mainAgent`, so the child's drain no longer
settles a row a shell still holds. The renderer keeps a previous `mainAgent`
only for writers that never carry one, so a hook row without it matches the
host snapshot.

* test(agent-status): pin that restart never seeds a main agent from a row silent about its shell

* docs(agent-status): the row builder pairs the shell fact with the main agent, and restart seeds only on an explicit no-shell

* test(agent-status): name the legacy-row case parameter for what it holds

* docs(agent-status): name which rows carry the main agent fact
2026-09-23 17:45:50 -07:00
Brennan Benson 7216d5af25 test(runtime): isolate per-repo worktree scan expiry from the shared timer queue (#22575)
The TTL case was advancing every fake timer, so a scan an earlier test
had left scheduled was counted as this repo's rescan.
2026-09-23 17:40:46 -07:00
Neil d4386763d5 fix(opencode-usage): merge a migrated session's two rows per column (#22550)
A session that lived through OpenCode 2's V1 import has a row in both
`session` and `session_v2`, and neither is complete. #22391 resolved the
pair by ranking whole rows on one number — total token count, ties to
`session_v2` — which let that number decide everything else on the row.

Three consequences, each reproduced against that PR's own fixtures:

- A recorded cost could be zeroed. `session_v2` wins on tokens while
  carrying `cost = 0`, and row parsing maps a zero cost to `null`, so a
  legacy row's $12.50 disappeared. Cost is re-derived by the same lossy
  reduce as the tokens, but only the tokens were guarded.
- The token comparison decided metadata. A legacy row with more tokens
  supplied a stale pre-migration directory, and a legacy row with a NULL
  model erased the model `session_v2` had — 23 of 234 shared ids on a
  real migrated database have a model only on the v2 side.
- Winner-takes-all is per row, so a legacy row holding the input tokens
  and a v2 row holding the cache reads reported one of them as zero.

Metadata now comes from the generation OpenCode still writes, with older
generations filling only its NULLs; usage columns take a per-column MAX.
Both rows aggregate the same assistant messages, and the import can only
drop messages, never invent them, so each column's MAX is a tighter lower
bound on the truth than either row and can never exceed it.

The relation stays exactly one row per id — the highest-priority
generation holding it — every column stays `columnExists`-guarded with a
SQL fallback, and a database with a single session table builds the same
SQL it did before.

Cache schema version 4 -> 5 so existing caches rescan.
2026-09-23 17:39:26 -07:00
Brennan Benson 800d33e5c9 feat: name runtime machines (#22094)
* feat: name runtime machines

* fix: preserve pairing address optionality

* fix(cli): keep host and environment listings local

Listing paired servers read each one's machine name by dialing it, so both listings made a network
round trip per server and waited out a timeout on any that were offline. They answer from this
machine's own pairing store; `orca host name --environment <name>` reads one server's name.

* fix(settings): caption the machine name paired devices actually receive

The caption read the runtime's published name once, when the pane opened, so saving an override
left it naming the old computer while phones already showed the new one. It now re-reads whenever
the saved override changes; the settings write lands in the main process before the store publishes
it, so that read already sees the new name. The name is interpolated rather than baked into the
fallback, and the caption, label and placeholder are in the English catalog.

* refactor(settings): normalize the machine name in one place

The trim and length rules for `machineName` were spelled out separately at the
renderer IPC (trim + 255), the settings load path (trim only, no cap), the RPC
schema (zod trim + 255) and the runtime reader (trim). A hand-edited or legacy
profile could therefore load a longer name than any writer accepts.

`src/shared/machine-name.ts` now owns `MACHINE_NAME_MAX_LENGTH` and
`normalizeMachineName`, and every writer and the load path use it. The RPC
schema keeps rejecting over-long names but derives its cap from the constant,
and the runtime settings controller normalizes an RPC write before storing it.

* fix(runtime): detect the machine name once and label handoffs with it

Every runtime constructed in a process (the app, plus each one a test builds)
ran its own `scutil` lookup. The friendly name is a property of the host, so the
lookup is now a single shared promise; construction still never blocks on it,
and a rejected lookup can no longer surface as an unhandled rejection.

The structured-chat handoff banner ("Agent is open in terminal on X") named this
host with the bare `os.hostname()` while paired devices saw the published name.
The transport now reads the same `RuntimeMachineName`, through a getter so a
rename in Settings is reflected without rebuilding the transport.

* fix(cli): print the name the runtime publishes and keep its envelope

`orca host name --name X` printed `undefined`: `settings.update` replies with
`{ settings }`, but the handler read a bare `machineName` off the reply, and the
test fixture mirrored the wrong shape so it passed. After a write the command
now re-reads `status.get` and prints what the runtime publishes, so a blank
`--name` prints the detected name it returned to rather than an empty string.

The read path wrapped a possibly routed answer in a local envelope, stamping
`_meta.runtimeId: "local"` on a reply from another server. It now returns the
`status.get` envelope itself, and an unreachable runtime is reported as the
usual error instead of an invented "unknown" name.

`environment list` had gained machine-name and platform columns that no caller
populated, so every row printed "platform unknown"; the columns are removed.

* refactor(settings): give the machine name field its own component

The caption under the field re-read runtime status every time the saved value
changed, relying on a comment about write ordering to show the new name. A saved
override already is what paired devices see, so the hook now derives the caption
from it and asks the runtime only for the detected name; a stale status read can
no longer show the previous name.

`MobileMachineNameField` owns the store read, the published-name hook and the
debounced input, so `MobilePairingSetupSection` returns to its prop shape and
the pass-through `MobilePanePairingOutput` wrapper is gone. Paired-device
revocation moves into `useMobilePairedDeviceRevocation`, which keeps
`MobilePane` within its line budget with an extraction that carries behavior.

The web client mounts this pane too, but its settings store kept the name
locally where nothing published it. `machineName` now rides the existing
runtime-backed settings sync so the field renames the paired runtime.

* refactor(settings): normalize the machine name at the store boundary

Every writer (desktop IPC, web RPC, CLI) reaches the store through
updateSettings, which already normalizes the other free-text settings
there. Trim and bound the machine name in that one place instead of at
two upstream edges, so a future main-process writer is covered too.

* test(settings): pin machine-name routing and detection, and make the field searchable

The shared machine-name lookup test spawned the real `scutil` twice and compared the answers, so a
slow runner could time one spawn out to the hostname and fail. It now mocks the subprocess, proves
the hostname answers until the one shared lookup lands, and that a second runtime does not spawn
again.

`host name` is no longer pinned local, but only the explicit `--environment` route was covered; an
ambient `ORCA_ENVIRONMENT` now has its own test so the pin cannot silently grow back.

The Machine name field is added to the Mobile pane's search catalog at the tail, keeping every
existing row's tie-break index.

* fix(runtime): wait for the machine-name lookup before publishing status

A status read answered in the first few milliseconds after launch published the bare
hostname because the friendly-name lookup had not landed yet, and a caption fetched in
that window never corrected itself. RuntimeMachineName now exposes the settled lookup
as a promise, and both status publishers (the status.get RPC and the desktop
runtime:getStatus IPC) await it before reading. Construction, listen, and every other
method stay unblocked; the worst case is one wait of at most a second on the first read.

* fix(cli): refuse to rename a runtime that does not publish a machine name

An older Orca runtime rejects the unknown settings field with a bare invalid_params, so
'orca host name --name' routed at one failed with no explanation. The runtime that does
not publish machineName on status cannot store one either, so the CLI reads status first
and refuses with incompatible_runtime and a message that says to update that host,
before writing anything.

* fix(ipc): introduce this desktop to remote hosts by its machine name

When this desktop connected to a remote workspace host it announced itself under a
hostname captured once at module load, so a renamed machine kept its old name on every
other device's connected-clients list. The client name is now read at send time from
the runtime's machine name (the configured override, else the detected one), passed in
where the remote workspace handlers are registered, so a rename reaches the next
presence frame without a relaunch.

* fix(runtime): keep the machine-name lookup under the status probe budget

Status publishers now wait for the one-time name lookup, and `orca status`
probes them with a one-second budget. scutil answers in milliseconds, so a
half-second cap keeps a stalled lookup from making a healthy runtime read as
"starting" while still preferring the friendly name.

* refactor(web): drop the unreachable machine-name write path

The Mobile settings section is desktop-only, so the paired web client can
never render the field. Forwarding the name through the web settings sync was
dead code, and against an older host the strict update contract would have
rejected it while the local mirror kept the value. Remove it until a web
surface exists.

* chore(i18n): translate the machine-name strings and document paired-server rows

Add the Machine name field and its Settings search entry to the five non-English
catalogs, explain in the host list spec why paired-server rows report an unknown
platform, and drop a stale timeout figure from a test comment.

* refactor(settings): make the machine name a machine-wide setting with a General home

The name other devices and hosts list this computer under is not a mobile
setting. Rename MobileMachineNameField to MachineNameField, give it a per-mount
id, and put its primary home in Settings > General under "This computer". The
Mobile pane keeps the same field. One shared search entry feeds General, the
Mobile pane, and the copy now says "other devices and hosts" in all six locales.

The web client has no machine of its own to name and its settings mirror cannot
persist one, so the field renders nothing there and General omits the section.

* feat(mobile): name this computer in the Orca Mobile pairing step

The "Pair this computer" step now shows the same machine name field above the
connection choice and code, so a user pairing a phone from the sidebar page can
name the computer right there.

* feat(settings): name this host when sharing it with other devices

Share this host produces the access link other devices use to reach this
machine, so it mounts the machine name field first. The pane's search entry
takes the shared machine-name keywords so a search lands there.

* feat(sidebar): name this desktop when adding a remote host

This desktop introduces itself to a new SSH host or remote server under its
machine name, so the Add Remote Host dialog mounts the field once, between the
header and the host fields, in both modes. Submit logic is unchanged.

* feat(settings): name this computer in the SSH pane add form

The SSH pane's add form mounts the machine name field above the host fields.
Editing a saved host leaves it out; that host already met this computer.

* fix(mobile): drop the empty machine-name grid row on the web client

The pairing step wrapped MachineNameField in its own grid-area div. On the
web client the field renders nothing, so the wrapper left an empty row and
an extra row gap between the copy and the connection options. The field now
takes a className for its root, so the grid slot disappears with it.

* fix(settings): let Enter in the machine name field submit its form like sibling inputs

The field intercepted Enter to blur and commit instead of submitting the enclosing
SSH add form. The draft is already flushed on blur and on unmount, and the name is
read from the store whenever a peer asks, so nothing is lost when the form submits
first. Enter now behaves like the neighbouring inputs; the test proves the submit
fires and the name still commits when the form closes.

* fix(mobile): keep the machine name inside the pairing copy cell

A dedicated grid row stayed in the template on the web client, where the field
renders nothing, adding an empty track and a second row gap between the copy and
the connection options. The field now sits at the end of the copy cell with the
same 18px rhythm, so an absent field leaves nothing behind.

* fix(runtime): retry a failed machine-name lookup instead of latching the hostname

On a loaded Mac the scutil lookup missed its 500 ms cap during app boot, and
because the fallback was memoized for the process, every status read and the
Settings caption showed the bare hostname for the rest of the session.

The lookup now gets a 5 s timeout, a failed attempt (timeout, spawn error,
non-zero exit, empty output) clears the shared memo so a later ready() retries
after a 30 s interval, and status publishers wait only up to a 750 ms publish
budget before answering with what read() has now. A friendly name and the
non-darwin hostname stay final.

* refactor(settings): show the machine name only where other devices join this computer

The Add Remote Host dialog, the SSH pane add form, and General all describe
another machine, so a field about this computer's own name read as a third
kind of label there. The field now mounts only where other devices pair with
or connect to this computer: the Mobile pane, the Orca Mobile pairing step,
and Remote Servers > Share this host.
2026-09-23 17:30:14 -07:00
Brennan Benson 98e5ea3d5f refactor(tabs): delete the terminal tab's dead adopted-session field (#22557)
* refactor(tabs): delete the terminal tab's dead adopted-session field and every branch that read it

* refactor(tabs): drop the stale adopted-agent comment and pin legacy load on a chat terminal

The comment above the terminal chat-eligibility agent fallback described the
removed adopted-session agent fallback. The legacy load test now puts the
retired key on a chat-mode terminal tab, the only shape that ever carried it.
2026-09-23 17:29:11 -07:00
Jinwoo Hong 89817ad2b4 test(mobile): repin the recording corpus to main's tip after #22570 (#22576)
#22570 pinned its own branch commit, which the squash left off main; the corpus now pins main at 37820f9683, the tree its fenced paths match. Every golden changes only its baseline line.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-23 20:25:42 -04:00
FumingPower adc0c67f75 docs: remove duplicate Muse badge from README (#22497) 2026-09-23 17:23:29 -07:00
Jinwoo Hong 37820f9683 feat(mobile): the page owns its safe area, like a native screen (OTA phase C follow-up) (#22570)
* feat(mobile): the page owns its safe area, like a native screen

The shell reserved both system-bar strips outside the WebView and painted
them bgBase, so every page screen showed a flat band above its header,
sheet scrims stopped short of the status bar, and the dock floated above
the gesture bar.

For a page that declares `safe-area-insets` in `ready.accepts`, the shell
now draws the WebView edge-to-edge and keeps only the keyboard strip off
it. `init` carries the insets the view sits under (bottom 0 while the
keyboard ends the view, top 0 under the update banner), and a move is
re-sent over the existing route-update `init`. An older page keeps the
reserved strips, since it has no reader for the insets.

On the page, a root layout (`app/_layout.web.tsx`) feeds those insets to
react-native-safe-area-context below ExpoRoot's env()-measuring provider.
It also replaces expo-router's DefaultNavigator, an all-edges SafeAreaView
that padded a second time.

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

* test(mobile-web): re-measure the page pins for the root layout

The page's route tree gained `./_layout.tsx` (its web sibling of the
native root), so every pin that counts the tree moved:

- Script sweep re-measured by building `routes.slice(0, n)` for each n.
  It reads 69 scripts at 16 routes, which matches the real build. The
  asset-ceiling crossing moves from 31 routes to 32.
- Route closures now enter through both layouts. `entryNames` gains
  `[dir]` because `app/_layout` and `app/h/_layout` share a name.
- Session closure pin 4216 -> 4219. The added modules are
  bridge-safe-area-insets, page-safe-area-provider and _layout.web.
- The web-overrides allowlist names `app/_layout.web.tsx`.

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

* fix(mobile): read safe-area ownership from the page-document state

Review fixes on the page-owns-safe-area change.

- Ownership is page-document state now. `page-ready` carries `accepts`
  beside `reports`, the patch sets `pageOwnsSafeArea` from
  `safe-area-insets`, and the session hook projects it like
  `backClaimed`. The screen's own per-session copy is gone.
- Insets moves re-send `init` only to a page that declared
  `safe-area-insets`. A page that took route updates but not insets was
  sent a useless `init` on every keyboard show and hide.
- The banner wrapper is gone. The root pads the status bar strip while
  the banner shows.
- The shell session defaults the insets inline, with no predicate that
  mutated its argument.
- The provider is folded into its single caller, `app/_layout.web.tsx`.
  The session closure pin reads 4218 (local 1032).
- The screen tests share their module mocks, and the safe-area cases
  move to a suite of their own: owned page, banner, iOS and Android
  keyboard, and an older page that gets no re-init.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-23 20:08:00 -04:00
Brennan Benson 4bab736f90 fix(native-chat): dock the task strip on the goal tab (#22530)
* fix(native-chat): dock the task strip on the goal tab

The background-task strip was the full width of the message box, so it
did not share an edge with the narrower goal tab underneath it. When a
goal is showing, the strip now uses that tab's width and keeps a square
bottom, and the goal tab's top stays square so the two sit on each other.

* fix(native-chat): derive the task strip and goal tab seam from adjacency

The strip and goal tab were each told by the chat session whether the
other was showing, through two flags that had to match what actually
rendered. The session also re-derived the goal tab's own visibility rule
to compute one of them.

Now each bar styles its side of the seam from the DOM: the strip takes the
goal tab's width and drops its bottom corners and shadow when the goal tab
is its next sibling, and the goal tab drops its top border and corners
when the strip comes right before it. The flags and the duplicated goal
visibility check are gone, so the seam cannot disagree with what renders,
and anything placed between the two bars falls back to the separate look.
2026-09-23 16:49:31 -07:00
Jinwoo Hong 63866c1e27 fix(mobile-web): page inputs lose the browser focus ring and hairlines draw one device pixel (OTA phase C follow-up) (#22569)
* fix(mobile-web): drop the UA focus ring from page text inputs

Chromium rings every focused text field (:focus-visible); no native TextInput
paints one. A zero-specificity rule in its own inline block beside the Expo
root reset removes it for every page input; buttons keep the browser's ring.

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

* fix(mobile-web): draw page hairlines one device pixel thick

react-native-web pins StyleSheet.hairlineWidth to 1 CSS px, three device
pixels on a 480 dpi phone; native draws one. A build shim replaces that one
assignment with React Native's own formula (roundToNearestPixel(0.4), else
1/ratio), so every page hairline matches native without touching components.

A rendered check at a real device scale (Playwright's emulated scale floors
borders to CSS px, which no phone does) measures both parity fixes.

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

* fix(mobile-web): apply the hairline shim to react-native-web's CommonJS build

The page's dependencies require react-native, so esbuild resolves every
importer to react-native-web's dist/cjs build, which the previous filter did
not match: the shipped bundle still assigned hairlineWidth=1. The filter now
matches both builds, the rendered check requires the package the way the page
does, and a builder test reads every hairlineWidth assignment in the bundle.

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

* fix(mobile-web): draw page hairlines at a width WebKit paints too

1/ratio is exactly one device pixel, and WebKit floors that to 0 and paints
nothing (0.3333px at a scale of 3), so the iOS shell would have lost every
hairline. The shim now uses native's device-pixel count plus half a pixel;
both engines floor a border to whole device pixels, so each paints what React
Native paints at ratios 1, 2, 3, 3.5 and 4, measured per engine. The rendered
parity check now runs in WebKit at a device scale of 3 as well as Chromium.

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

* test(mobile-web): check the parity style in the existing root-reset build

Drops a second full build that read one HTML string, plus two assertions
that tested the constant against itself.

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

* fix(mobile-web): round the page hairline up to the 1/64 px layout step

Half a pixel over native's count kept borders at one device pixel but made
the separators drawn as `height: StyleSheet.hairlineWidth` straddle two rows
at about half of all offsets. Both engines lay out in 1/64 CSS px, and WebKit
stores an exact 1/3 as 21/64 and paints nothing, so the width is now native's
device-pixel count over the ratio, rounded up to the next 1/64 (22/64 at 3).
The rendered check adds a separator at a 10.1 px offset in Chromium and WebKit.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-23 19:48:45 -04:00
Jinwoo Hong 5b6a857e41 fix(mobile): route the bottom drawer's keyboard through the platform seam (OTA phase C follow-up) (#22556)
* fix(mobile): route the bottom drawer's keyboard through the platform seam

Fill-mode sheets called Keyboard.metrics() directly, which react-native-web
does not implement, so opening one on the page threw and the shell
re-downloaded the workspace. The drawer now reads useSoftKeyboard, whose
native half seeds from metrics() and carries the event duration, and whose
web half answers from the window (duration 0). The fill/content-sized seed
rule and resolveBottomDrawerKeyboardInset are unchanged. A census keeps
Keyboard.metrics/addListener inside the seam plus the tab-sheet hide wait.

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

* test(config): retire the drawer's exemption from the page keyboard census

The bottom drawer now reads the keyboard seam, so no module in the
source-control or review closures names react-native-web's Keyboard stub.
The census also flags Keyboard.metrics, which the stub lacks.

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

* refactor(mobile): give the drawer an imperative keyboard pair from the seam

The seam now exports subscribeSoftKeyboard and currentSoftKeyboardHeight
beside its hooks. The drawer's effect is back to its original shape with
only its Keyboard calls swapped for the pair, and useSoftKeyboard is back
to {height, visible} with no metrics() seed. Seeding every consumer opened
an iOS window between willHide and didHide where metrics() still reads
open. The web pair answers from visualViewport, so it stays silent inside
the shell and lifts sheets in a plain mobile browser.

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

* fix(mobile): start the web keyboard subscription from the current strip

A keyboard already covering the page when subscribeSoftKeyboard attached
never produced onHide when it closed, so the occlusion hook and a seeded
fill sheet stayed lifted. Outside the shell only.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-23 19:46:52 -04:00
Brennan Benson 069dc8a1d8 feat(agent-launch): let a caller reserve the chat session, and start terminal launches with the session picks (#22523)
* feat(agent-launch): let a caller reserve the chat session and carry session picks to a terminal launch

* fix(agent-launch): keep a caller-minted session id named for its agent, and mint the fallback the same way

* test(mobile): model the older host from the launch fields, not the refined schema

* docs(agent-launch): describe the reserved session id as conversation identity, not placement

The caller mints the session id so it knows which conversation it
started; tab placement is not keyed on it. Also puts the terminal
surface's doc comment back on createTerminalSurface.

* docs(agent-launch): say a terminal launch reads the session picks on the wire contract

The `sessionOptions` field doc still said a terminal launch ignores them, which this branch changed.

* fix(agent-launch): check a reserved session id's token after the agent name, not the whole id

A hyphenated agent name failed the one-token check, so any session id for such an agent was
refused at the wire, while every other agent without a chat has its id ignored on the terminal.
2026-09-23 16:45:28 -07:00
Neil 12a7ffdc64 test(runtime): stop worker-recovery retries from scanning inside later tests (#22567)
The legacy worker terminal recovery retry re-arms itself on a 1s..30s backoff
for as long as a dispatch stays deferred. Nothing in the runtime suite ever
resolves one, so a single test armed a loop that kept re-running recovery --
and the worktree scans it issues -- through the shared `listWorktrees` stub for
the rest of the file's run, landing inside whichever test was executing when the
timer fired. That is what made `lineage-and-scan-cache-part-03`'s per-repo TTL
test see 4 scans instead of 3 only under CI load.

Give the controller a `cancelAllRetries`, track controllers while they have a
timer armed, and cancel them from the shared runtime test lifecycle reset.

Measured over the whole 1299-test file with an afterEach probe: 25 stray
post-test `listWorktrees` calls before, 0 after.
2026-09-23 16:44:01 -07:00
Neilandfancivez 6847390c0a fix(ai-vault-search): own buffered transcript rows before they pin the parent (#22563)
Capped search rows are slices of the transcript. Own each row with the shared copier before buffering it, and keep the heap regression from #22378.

The sidebar test harness now includes the worktree listing map the toolchain banner reads, so that rerender no longer crashes.

Co-authored-by: fancivez <fancivez@gmail.com>
2026-09-23 16:02:53 -07:00
Jinwoo Hong 7f203ad5aa fix(browser): only pixel-capturing commands wait for the page to be drawn (#22528)
* WIP

* WIP2

* fix(browser): only pixel-capturing commands wait for the page to be drawn

Every targeted browser command used to take an automation-visibility lease,
which waits for two desktop-window animation frames (capped at 2 s). When the
desktop window is minimized or throttled, that wait always hits the cap, so
each phone tap or agent command stalled for up to 2 s (STA-8024).

Input, page JavaScript, layout and accessibility snapshots all work on a
hidden page; only pixel capture needs it drawn. The lease is now opt-in via
`needsPaint` on the two commands that can capture pixels through this path
(`exec` passthrough and `pdf`); `ensureVisible` is removed. Screenshots keep
managing their own lease.

(Commits 24821ae and 07b4a09 carry this change under WIP messages.)

* refactor(browser): drop the session-recreate option only no-lease commands used

Only exec and pdf take the automation-visibility lease now, and both create an
agent-browser session, so the lease's re-registration path always recreates
the session. Remove the `recreate` option from restartSessionForTarget and the
options passthrough that fed it. Also fix the keypress test comment that still
counted the lease's extra page lookup, and note that screenshots lease inside
the screenshot lock.

* fix(browser): resolve a command's page when it runs, not when it is queued

Enqueue resolved the page's guest webContents and created its session up
front. Before this PR every command re-resolved after its lease, which hid
that; now a command queued behind exec/pdf, whose lease can re-register the
page with a new guest webContents, would run against the old one.

Enqueue now only picks the page (and so the queue). The webContents lookup
and session setup move into the queued job and run when the command starts,
after the lease for exec/pdf.

The lease path's refresh collapses into that step. Resolving after the lease
already sees a re-registration that happens during the lease, so the only
thing left from the refresh is the case where the page's existing session is
bound to an older guest: that session is restarted on the new one (keeping
intercept routes) and the active-tab pointers move with it. That check now
covers every session-backed command, not only leased ones.

Admission is still checked at enqueue and again when the job starts.

* refactor(browser): drop the session-restart reconcile no page can reach

Every renderer re-registration of a page with a different guest goes through
browser:registerGuest, which calls onProcessSwap in the same tick. That
destroys the page's session and rejects its queued commands, so by the time a
queued command runs, no session can still be bound to an older guest. The
reconcile in the queue and restartSessionForTarget, its only caller, are
removed along with the two tests that called it directly.

A queued command now resolves the page's current guest, ensures its session,
and runs. The re-registration tests now report the swap through
onProcessSwap, as production does: commands queued behind the swapping lease
are rejected with browser_tab_closed, the leased pdf runs on the new guest,
and intercept routes survive the swap.
2026-09-23 17:32:04 -04:00
Brennan Benson 60bd1dfdea feat(native-chat): one shell-environment setting for every structured chat (#22387)
* feat(native-chat): one shell-environment setting for every structured chat

Structured Codex chats started from the login-shell environment, while
structured Claude chats started from Orca's own process environment, so a
variable exported in .zshrc reached one and not the other. Both now start
from the same base, chosen by a new setting:

- on (default): the whole login-shell environment, as a terminal gets
- off: Orca's environment plus PATH, locale, SSH_AUTH_SOCK, and the
  variable names the user lists

The setting is re-read each time a chat starts or resumes. It is shown
only when Chat UI, the Chat UI default view, and structured native chat
are all on. Terminal-backed chat is unchanged.

* fix(native-chat): normalize the shell-environment settings when a profile loads

A hand-edited settings file could store the variable list as something other
than an array, and the structured runtime called `.filter` on it per launch, so
a malformed value failed every structured chat create and resume, and the
settings pane render. Normalize both keys where the profile loads, the same way
the other array settings are, through one shared normalizer the runtime policy
also uses. Also pin that an uncommitted name draft survives an unrelated
settings re-render.

* fix(native-chat): keep the pinned account as the only source of a structured chat's Claude home

The session record owns which Claude home a structured chat uses, and the
acquisition pin (claudeConfigDirEnvPatch) is the only emitter of
CLAUDE_CONFIG_DIR, compared against what the child would otherwise inherit.
With the login-shell snapshot as the inherited base, a CLAUDE_CONFIG_DIR
exported only in a shell rc flipped that comparison and produced an explicit
pin to the CLI default home, which moves the CLI off its default Keychain item.

Drop the inherited CLAUDE_CONFIG_DIR in the Claude launch resolver before the
pin runs, as Codex already does for an inherited CODEX_HOME. A configured
per-agent overlay still passes through, since the record already honors it.

* fix(native-chat): drop Orca's own CLAUDE_CONFIG_DIR from a structured Claude child too

The process spawner merges Orca's process env under the launch env, so a
CLAUDE_CONFIG_DIR exported to Orca itself reached the child around the launch
resolver's drop and unseen by the account pin. One helper now strips it from
both inherited bases. Also declare the two shell-environment settings on the
runtime store contract and add the six new strings to every locale catalog.

* feat(native-chat): add shell variables one at a time with a removable list

* fix(native-chat): return focus to the name input after removing a shell variable

* fix(native-chat): use a neutral placeholder for the shell variable input

The empty input showed a grey HTTPS_PROXY as its placeholder, which reads as a
saved value, especially right after that exact entry is removed from the list.
Use "Variable name" instead, in every locale catalog.
2026-09-23 14:29:20 -07:00
Jinwoo Hong 9cdbc0c128 fix(mobile): keep the shell's window insets out of the page WebView (OTA phase C follow-up) (#22549)
* fix(mobile-web): stop the page declaring viewport-fit=cover

The shell already pads the WebView out of the status and navigation bars. With
viewport-fit=cover, Android's edge-to-edge WebView still reports the window's
bar insets through env(safe-area-inset-*), which react-native-safe-area-context
on web reads, so every page-side SafeAreaView padded a full bar a second time.
Without it env() reads 0 and the shell's pad is the only one.

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

* fix(mobile): keep the shell's window insets out of the page WebView

WebView M144+ forwards the window's systemBars and displayCutout insets to
CSS env(safe-area-inset-*) for every WebView, and Chromium applies them
regardless of viewport-fit. The shell already pads the WebView out of both
bars, so every page-side SafeAreaView (expo-router's DefaultNavigator and the
session header) padded a bar a second time. M139+ likewise resizes the visual
viewport for ime(), which the shell has already done by shortening the WebView.

The WebView now sees those three types zeroed, per Android's "zeroing" approach
(not CONSUMED, so later changes still reach it). A listener replaces the
WebView's own onApplyWindowInsets, so the zeroed set is passed back into it.

iOS needs nothing: the WKWebView uses contentInsetAdjustmentBehavior = .never
inside the padded shell and reports zero insets.

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

* docs(mobile-web): say why the page declares no viewport-fit

The earlier comment claimed dropping viewport-fit=cover makes env() read 0 on
Android; Chromium's WebView applies the safe area regardless of viewport-fit.
The page simply never asks to extend under the bars, and the shell owns the
safe area.

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

* refactor(mobile): keep the page inset zeroing private to the shell view

The transformation has no honest JVM test (the builder runs as SDK 0 there
and drops every inset type), so it moves into MobileWebShellView.kt as
private members instead of standing alone. The listener comment now covers
both the P-R listener and the S+ onApplyWindowInsets path it replaces, and
the page document's comment says the env() zeroing is Android's; on iOS the
padded WKWebView reports none.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-23 17:13:23 -04:00
Neil f1eb1913a6 fix(opencode2): block the pane on every session-owned form (#22548)
#22399 admitted an OpenCode 2 form.created as a pane blocker only when
metadata.kind === "question". On v2.0.15 that is an allow-list on a field
with no contract: packages/schema/src/form.ts declares Metadata as an open
Schema.Record and metadata itself as optional, and the public
POST /api/session/:sessionID/form endpoint lets any client raise a real
blocking form on a real session with no metadata. Orca dropped those, so
the pane painted no blocker while OpenCode waited forever.

Invert the default. Every form whose owner is a real session blocks;
only a form owned by the "global" MCP-elicitation sentinel is dropped,
because that owner is not a session and never goes idle, so its blocker
could not be retired. That also restores websearch.provider as a blocker:
it carries the real context.sessionID, session idle retires it, and while
it is pending the agent is genuinely stalled on the user.

Resolution is unchanged: clearAttentionForResolution keys on the exact
form id plus source session, so a resolution for a dropped form matches
nothing and cannot retire a live blocker.
2026-09-23 13:52:41 -07:00
Jinwoo Hong 1866a796fd fix: explain Xcode-blocked Git once in the sidebar and rescan on return (#22552)
* fix: explain Xcode-blocked Git once in the sidebar and rescan on return

* refactor: simplify Xcode toolchain banner to one classifier and a stateless rescan

* refactor: fold banner copy into one table and cover SSH/runtime gating
2026-09-23 16:51:23 -04:00
Jinjing 90f0c5c8ae test(file-search): pin request-key listings against the real runtime shape (#22312)
* test(file-search): pin request-key listings against the real runtime shape

* test(file-search): pin intermediate renders and late remote answers

Strengthen the stale-answer guard to assert every intermediate render reads as loading (null), add a late-answer drop case, make the tab-entry loading pin non-vacuous, and correct the e2e comment for local listings.

* test(file-search): keep only the non-duplicate runtime-listing pins

Drop the remote projection cases already covered at the hook level, collapse
the classifier integration to the loading pin, drop the local-only rapid-edit
e2e, and fix the brittle README absent-file assertion that failed CI.
2026-09-23 13:35:47 -07:00
Brennan Benson 845db9e5e2 fix(native-chat): underline only file links a click can act on (#22370)
* fix(native-chat): underline only file links a click can act on

A chat message could underline a bare file name such as `deck.md` that
resolved nowhere, and clicking it did nothing, so it read as a broken link.

- Inline code and quoted text become file links only when they name a path
  (contain a `/` or `\`), matching plain prose; a bare file name stays plain code.
- Every file link click now answers: it opens, or says the file was not found,
  that the host could not be checked, or that the path could not be resolved.
- Explicit links like [x](README.md:5) route as files, and linked text keeps
  `#`, `?` and `%XX` literally instead of re-parsing them as URL syntax.

* fix(native-chat): wrap the parsed file location so file URIs in chat text still open

Linkified prose, quoted text and inline code wrapped their display text, which the
literal wrapped-href route no longer URL-parses, so file:///... resolved as a relative
path under the worktree. Wrap pathText[:line[:col]] from the parsed link instead.
2026-09-23 13:29:24 -07:00
19e8bf319d fix(ai-vault-search): detach buffered transcript rows (#22545)
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: Andre Ambrósio <56239028+sirambrosio@users.noreply.github.com>
2026-09-23 13:28:17 -07:00
Jinwoo Hongandmaxidiazbattan 519bde81df feat(ipynb): render notebooks like a notebook, with seamless click-to-edit cells (#22519)
* feat(ipynb): parse ANSI SGR sequences in notebook output

Tracebacks and stream output carry terminal colour codes that the notebook
printed raw. Splits text into styled runs (16/256/truecolor, bold, italic,
underline) and drops non-SGR escapes via the shared stripper.

* feat(ipynb): render notebooks like a notebook, not a form

- Prose inherits the app UI font instead of a bare terminal font name, which
  Chromium could not resolve and fell back to Times (removes the now-dead
  resolveEditorFontFamilyOrInherit).
- Markdown cells render by default; double-click or Enter edits them in the
  same Monaco surface code cells use. Code cells activate on press so a
  collapsing neighbour cannot swallow the click, which also retires the
  root pointer-capture deactivation (Monaco blur already covers it).
- Code sits on its own tinted surface, the active cell gets a ring, and the
  editor sizes to its content so activating a cell no longer jumps.
- The always-on 8-button toolbar and native select become a hover/focus
  toolbar (move, delete, and a menu for insert and cell type); the Jupyter
  [n] prompt turns into the run button.
- Outputs show only the richest MIME representation, HTML renders in a
  script-less, no-network sandboxed frame sized to its content, and ANSI
  colours use the default terminal palettes.

Adopts the MarkdownPreviewBody reuse, richest-MIME selection, extra raster
MIME ranks, and CSP-sandboxed auto-height HTML frame from #18542.

Co-authored-by: maxidiazbattan <maxidiazbattan@gmail.com>

* chore(ipynb): drop the nbformat label and BETA badge from the notebook header

parseIpynb already rejects notebooks without a v4 cells array, so the label
carried no actionable information; the parsed nbformat field goes with it.
Removes those catalog keys and the stale lowercase code/markdown ones.

* fix(ipynb): keep cells pixel-stable when they switch to editing

The preview and the live editor disagreed on four things, measured over CDP:
- Font: the excerpt painted the bare "SF Mono" name (or --font-mono via its
  row class), while Monaco appended its own fallbacks and landed on Menlo.
  Both now use resolveEditorFontStack, the editor font plus the terminal
  fallback chain.
- Line height: 20px rows vs Monaco's 21px. Both read CODE_EXCERPT_LAYOUT.
- Gutter: a 48px line-number column plus 12px inset vs Monaco's 25px gutter.
  Notebook cells drop line numbers (the Jupyter and VS Code notebook
  default) and Monaco's decorations lane is the same 12px inset.
- Rows: colorized blank lines collapsed to 0px, and a trailing newline had
  no preview row. Rows are fixed-height and a trailing newline opens an
  empty last line, matching the Monaco model.

The [n] prompt and run icon now share one grid cell, so the hover swap keeps
the label's box and centre. The commented-line tint moves to a theme token.

* feat(ipynb): VS Code-style run gutter above a fixed execution count

Replaces the in-place [n]/play swap from 172b4f7937. The [n] count is always
shown and never moves; the run button sits above it in the same gutter
column, revealed on hover or focus from an invisible slot that keeps its
box. While a cell runs, the slot shows a spinner and the count reads [*].

* refactor(ipynb): give notebook code its own preview, keep MonacoCodeExcerpt unchanged

172b4f7937 bent the shared MonacoCodeExcerpt (used by PR comment code
context) to the notebook's layout, changing that surface's font, tracking
and highlight tint. The excerpt's rendering is now byte-identical to main;
only its colorizer is extracted as useMonacoColorizedLines. The notebook
renders its own rows from CODE_LAYOUT, the same metrics its Monaco editor
uses, so activation stays pixel-stable.

* fix(ipynb): open the cell editor with the caret where the user pressed

Monaco mounted with its caret at 1:1 and focus ran occurrence highlighting
on that first token, so clicking any line lit up every 'import'. The press
on the preview now maps to a model position (its rows mirror model lines)
and Monaco opens there before focusing; keyboard and markdown activation
open at the end. Occurrence highlighting stays on, as in VS Code notebooks.

* feat(ipynb): put the execution count on top and the run button below it

The [n] count now shares the first code line's box (the gutter mirrors the
code surface's border and padding, and the count is one line tall); the
run button slot sits beneath it and stays reserved while hidden.

* fix(editor): re-colorize code excerpts when the theme changes

Monaco's colorizer emits theme-specific token classes, so excerpts colorized
under one theme kept that theme's token colours after a switch (dark-theme
keywords rendered green on the light theme). Affects notebook previews and
PR comment code context.

* fix(ipynb): mount cell editors before paint so a click swaps in seamlessly

@monaco-editor/react waits on an async loader, shows "Loading..." and builds
the editor inside a display:none container. Every click on a cell therefore
painted a placeholder, then an editor laid out at 0x0 (one uncoloured line),
and the caret guess ran against the static preview, falling back to the end
of the cell whenever the press missed a text row (padding, gaps).

The cell editor is now created with monaco.editor.create in a layout effect:
sized, tokenized (restoring its view state marks the visible lines stable)
and placed before the first paint. Monaco hit-tests the press itself with
getTargetAtClientPoint, so the caret lands on the nearest position for text,
trailing whitespace, blank lines and padding; keyboard and markdown
activation open at the start. Indent guides stay off to match the preview.

Word and selection occurrence highlighting are off in cell editors, since
clicking a word lit up every other occurrence like a search.

Also drops wiring the rewrite made dead: the per-editor save shortcut (the
notebook's capture handler already owns Cmd+S), the callback refs, the
onSaveRequest prop, and the toolbar's positioning wrapper.

* fix(editor): follow OS theme flips when colorizing code excerpts

useMonacoColorizedLines read the theme once from settings, so a 'system'
theme flipping with the OS never re-colorized. It now uses
useDocumentDarkTheme, like the rest of the notebook; rendering is unchanged.

* fix(ipynb): let Escape close an open widget before it leaves the cell

The cell's Escape command had no context, so Escape with suggest, find or
parameter hints open closed the whole cell editor. It now only fires when
none of those widgets is visible.

* fix(ipynb): keep HTML output frames at an opaque origin

The output frame had allow-same-origin only so the viewer could measure its
content height. Notebook HTML is untrusted, so the frame is now sandbox=""
(opaque origin, no scripts) at a fixed height that fits a pandas head() or
describe() table and scrolls anything larger.

* refactor(fonts): move buildFontFamily into lib/monospace-font-family

lib/editor-font-zoom imported it from a terminal-pane component module. The
monospace fallback chain now lives in lib, and every importer, including the
terminal, points there. Pure move; the tests move with it.

* fix(editor): pass comma-separated editor font stacks through unchanged

resolveEditorFontStack quoted a value like "JetBrains Mono, monospace" as
one family name, which no font matches. A comma list is now used as-is;
only a single name gets the quoted fallback chain.

* chore(i18n): drop the Korean override for a removed notebook key

---------

Co-authored-by: maxidiazbattan <maxidiazbattan@gmail.com>
2026-09-23 15:07:30 -04:00
Jinwoo Hong 1116c54230 fix(relay): accept production cells past c29 in the regional rehome operator (#22518)
The selector membership check capped cell ids at c29, so enable failed
closed with "selector membership is invalid" once c30 went general.
Accept c1-c99 with no leading zero.

Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010
2026-09-23 14:37:17 -04:00
JinjingandSeongho Bae a77f87ea16 feat(sidebar): copy workspace name from context menu (#22338)
* feat(sidebar): copy workspace name from context menu

Add Copy Name directly below Copy Path in the workspace context menu. It
copies the name through resolveWorktreeDisplayName, the renderer mirror of
main's mergeWorktree fallback (custom name, then branch, then folder), so the
copied text is what `name:` worktree selectors resolve against and a cleared
custom name no longer copies `undefined`.

The context-menu model now spreads the command handlers instead of listing
each one twice, which keeps it under the file-length limit.

Fixes #21980
Linear: STA-8068

* refactor(sidebar): rename Copy Name to Copy Worktree Name

- Clarify that this copies the worktree display name, not the path
- Update all locale strings and i18n keys
- Rename test file to match

* test(sidebar): cover folder workspace copy worktree name

---------

Co-authored-by: Seongho Bae <me@seonghobae.me>
2026-09-23 11:25:49 -07:00
Brennan Benson 641a7f36d9 fix(native-chat): keep one live tool-run header from a call's start to the turn's end (#22432)
* fix(native-chat): keep one live tool-run header from a call's start to the turn's end

The collapsed tool run's header was two elements, one for "a call is running"
and one for "nothing is", chosen call by call. Every call start and end
remounted it, the count disappeared while a call ran and came back one
higher, and a call that finished inside a frame still bought the whole swap.
That is the 42→43 flicker in the report.

The header is now one element whose live state belongs to the turn, not to
any call: it stays live from the run's first call until the agent moves past
it (prose, a further run, or the turn's end), and settles in place. While
live the sentence speaks in the present tense and counts the call in flight
("Running 3 commands"), with the latest call's command beside it as a muted
preview; once settled it reads as before ("Ran 3 commands ✓"). The category
glyph is the run's in both states, and the completion mark only appears once
settled, so nothing pops between calls.

Which run is live is derived where the transcript is sliced into rows: the
last row that speaks or acts is the trailing one. A reasoning aside after it
leaves it live; an answer or a further run settles it.

Present-tense forms for the ten sentence categories are added to the shared
copy and the English catalog. The transcript-file lane, which renders with
the structured activity UI off, is unchanged.

* fix(native-chat): settle a run blocked on the reader, keep it live past an approval

- A run whose question is awaiting the reader's answer no longer pulses
  "Reading 1 file" while the agent is blocked; it falls back to its calls.
- An approval's receipt no longer moves past the run above it, so the call
  it just approved reads as running while it runs.
- The header button is the live region, so the count is announced too.
- Drop the unused live option and record from the shared English sentence;
  nothing renders it yet.

* fix(native-chat): stop the settled run's check from fading in on every mount

Windowing remounts settled rows as the reader scrolls, and a restored transcript
mounts them all at once, so the fade replayed where nothing had changed. Also
pin that the live header counts the next call on the same element.
2026-09-23 10:49:04 -07:00
Brennan Benson 563dd5487f feat(native-chat): show a Codex chat's goal above the composer, and set it from goal mode (#22377)
* feat(native-chat): show a Codex chat's goal above the composer and set it from goal mode

Structured Codex chat now treats the thread goal as session state: a banner above the
composer shows the current goal (pursuing / paused) with clear, pause/resume and expand;
/goal enters a goal mode whose send calls thread/goal/set; the objective is journaled as a
user message marked as sent as a goal. The banner is derived from the journaled goal rows,
which Codex's resume snapshot refreshes, so a reopened or adopted chat shows its goal.

Fixes STA-8159

* fix(native-chat): replace a recorded goal by clearing first, and recover a lost goal-change response

- A set while the journal records a goal (any status) clears it before setting,
  so the new goal starts with its own time and token counters instead of
  rewriting the old goal's objective in place.
- The threadGoal plan answers an unknown outcome from the goal the journal
  records and reruns otherwise, so one request timeout no longer refuses every
  later Clear/Pause/Resume as unknown for the mounted session.
- The goal-mode chip says "Exit goal mode"; "Clear goal" stays the banner's
  action on the provider goal.
- A typed bare /goal on Enter enters goal mode, the same as picking it.
- The renderer reads the goal off the tail of its ordered snapshot; the host's
  unordered map keeps the by-sequence reader.
- Drop the composer's duplicate in-flight guard; the goal controller already
  serializes changes.
- Pin that a counter-only revision reaches a subscriber's live page under its
  original sequence.

* fix(native-chat): keep a bare /goal inside goal mode as the entrance, and pin goal delivery and serialization

- A bare `/goal` submitted while already in goal mode re-enters the mode instead
  of setting a goal whose objective is the literal text "/goal".
- The counter-only revision pin now drives the host's own event sink bound to a
  real journal, so it goes red when the publish after a lifecycle transition is
  dropped; the previous fake sink never published.
- Pin that a set which threw after journaling its objective puts that objective
  back exactly once when the ledger reruns the same operation id.
- Cover the goal controller hook: absent without host support, the loaded window
  wins over the host's answer, a second change while one is unsettled answers
  false without a request, and a refused change frees the next one.

* fix(native-chat): resume a blocked or usage-limited goal, and keep goal-mode drafts honest

- The goal bar offers Resume on a blocked or usage-limited goal, which the
  provider resumes exactly as it resumes a paused one; a goal whose token budget
  is spent still offers only Clear. The rule lives beside the other goal facts
  in shared code so every reader answers it the same way.
- A `/goal <text>` typed inside goal mode sets the objective `<text>`, as it
  does outside goal mode, instead of a goal whose objective is the literal
  command.
- Setting a goal is a host round trip; a draft edited while it was in flight is
  no longer wiped when the goal lands, matching every other host command.
- Pin that a lost status-change response is read as applied only when the
  recorded goal is in that status, that a cleared row in the loaded window
  outranks the host's earlier answer, and that the PTY lane is untouched.

* fix(native-chat): keep the load-older anchor on the loaded window when a live revision lands below it

A live revision of a row keeps that row's original sequence. When the row is
older than the client's loaded window, the shared reducer merged it in and it
became the load-older anchor, so paging `before` it skipped every row between.
A goal's counter-only revisions during a long goal turn reach any client that
attached after the goal row left its window, so a reopened chat lost rows on
scroll-back.

The reducer now admits live rows only at or above the window's oldest row
while older rows remain on the host; the journal keeps the revision and the
page reader serves it once the window reaches the row. With nothing older on
the host the window is the whole journal, so a row below the head is admitted
as before.

Also drain accepted provider events before a goal set reads the journal to
decide whether it replaces a recorded goal.
2026-09-23 10:34:06 -07:00
Brennan Benson a375936c04 feat(agent-launch): let a caller reserve the pane its terminal launch creates (#22291)
* feat(agent-launch): let a caller reserve the pane its terminal launch creates

* fix(agent-launch): refuse a launch whose reserved pane is already live

* fix(agent-launch): refuse a live reserved pane before it is revealed

The live-pane refusal used to fire in the executor, after createTerminal
had already issued a handle, published the mobile snapshot and revealed
the tab. The reveal re-registered a fresh launch config over the running
agent's. agent.launch now passes requireFreshPane with a reserved pane,
and createTerminal throws AgentLaunchPaneAlreadyLiveError as soon as
spawn reports it attached to a live pane. That is before any handle,
snapshot or reveal. The spawn reattach itself is the one terminal.create
already uses, so the live PTY is never killed, and the stable-pane
create claim is still released in finally. The isReattach plumbing
added to the launch factory for the old check is gone.

A replay-safe launch refused this way on an existing workspace now
records a failed ledger row, the same way a name collision does.
Before, the row stayed claimed, so every retry got
agent_session_operation_unknown. agent.launchReplay passes the code
through. On create-worktree the workspace already exists when the
terminal is refused, so the row stays unknown. The code is added to
the runtime passthrough list so callers can branch on it.

The pane key is now in the replay fingerprint, deliberately. It is not
placement: group, anchor and focus still stay out of the request and
out of the ledger. It is identity. It is written into the pane's PTY
environment and names the tab the caller has placed. A retry that
reserved a different pane is therefore a different request. Replaying
the first answer would return a key the new reservation can never
find. This matches terminal.createAgentSession, which also fingerprints
its tab and leaf ids. The key is only folded in when present, so every
existing digest is unchanged, and a test pins that.

The wire schema now refuses a tab id the runtime would not adopt as
sent: one with surrounding whitespace, which the runtime trims, and one
longer than 512 characters, which the spawn reservation does not key
on. It reuses the tab-id schema that Placement uses.

* test(agent-launch): pin that a refused live pane issues no handle

The refusal test named handle issuance but only asserted the reveal, so a
throw moved to just before the reveal would still pass. Assert no terminal
is registered, with the attach test as the positive control.
2026-09-23 10:18:16 -07:00
github-actions[bot] dac82f61bc Update README downloads badge 2026-09-23 12:37:55 +00:00
Jinjing 8d6fec597b Optimize cloud-verify workflow to scan HEAD instead of all history (#22457)
* fix(cloud-verify): scan HEAD instead of all history

- Gitleaks now verifies only the checked-out revision
- Reduces scan scope and improves verification workflow performance

* ci(cloud-verify): clarify that Gitleaks scans HEAD-reachable history
2026-09-23 01:36:24 -07:00
Brooks JohnsonandNurdaulet Bolat 942d993f1f fix(editor): map Salesforce Apex extensions to the apex language id (#14287)
Fixes #22049

Co-authored-by: Nurdaulet Bolat <204565446+nvimq@users.noreply.github.com>
2026-09-23 01:29:27 -07:00
Jinwoo Hong ae3d380b23 fix(relay): lock only the target cell row, last and NOWAIT, in the rehome commit (#22449)
* fix(relay): lock only the target cell row, last and NOWAIT, in the rehome commit

The idle-rehome commit runs on the source cell. From an Asia cell each
statement is a cross-region round trip, and the transaction locked every
relay_cells row plus every runtime, capability and safety row before about
twenty more statements, so each Asia-source rehome held the whole fleet's
cell rows for ~3.6 s and every reconnect, renewal and placement queued or
timed out behind it.

The commit now reads the cell inventory and the runtime, capability and
safety tables unlocked, keeps the control and worker rows locked (now
NOWAIT), and takes one cell lock: the target row, in a single statement
that locks it NOWAIT, re-checks enabled, general admission and capacity,
and reserves the units, issued as the last statement before COMMIT. A
target that changed admission, filled up, or is locked by another writer
rolls the whole commit back and answers deferred (candidate-ineligible).

The hold is sampled under a site label, so cellInventoryHoldMsMax still
sees rehome holds and rehomeTargetRowHoldMsMax reports them apart.

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

* fix(relay): fail closed on the rehome target-row lock clause

The target-row statement now carries FOR UPDATE ... NOWAIT unless the
dialect is explicitly SQLite, so a wrapper that omits the optional dialect
can no longer run the reservation unlocked. Test wrappers and the fault
injection entry forward the dialect they wrap.

The latency test also probes the admission and region tables at every
round trip; only the target's admission row may be locked, and only
before COMMIT. The runbook notes that an Asia-sourced commit holds the
rehome control row for about 6.5 s, so a pause that fails once is retried.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-23 03:45:47 -04:00
Jinwoo Hong a2a78ab335 feat(relay): alert on relay cell table lock convoys (#22446)
* feat(relay): alert on relay cell table lock convoys

Adds a log-based metric and alert for cell-inventory lock holds of at
least 1,000 ms, and a Cloud SQL log metric and alert for relay-only lock
timeout cancels at 20 or more per minute. NOWAIT refusals are excluded:
background sweeps produce about 160 per minute even with rehome paused.

Replayed over 2026-09-20 14:00 to 2026-09-22 15:00 UTC: the hold filter
matches all 93 asia-east2 rehome holds plus 9 director holds, and every
one of the 88 cancel burst minutes overlaps an asia-east2 hold.

Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010

* fix(relay): page only on cell lock holds; director holds stay visible

Director holds of 1-2.5 s recur several times a day with rehoming paused,
and pausing rehome does not stop them. The paging hold policy now selects
role=cell samples only; a separate policy with no notification channel
keeps director holds visible. The burst documentation no longer claims no
burst happens while paused, and the runbook points a burst with no cell
hold at the director policy.

Replayed cell-only: 93 of 93 asia-east2 holds, 0 director holds over
2026-09-20 14:00 to 2026-09-22 15:00 UTC; 0 from then to 2026-09-23 07:30.

Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010
2026-09-23 03:24:09 -04:00
Jinwoo Hong 1043dc5e1d fix(relay): stop rehoming hosts off Asia cells until the lock fix lands (#22443)
The source cell runs the rehome commit. An Asia source pays a cross-ocean
round trip per statement while holding relay_cells row locks every cell
needs, which convoys the fleet database. Selection now drops source cells
outside the director's region before building the decision window, so the
incumbent_region filter shrinks while Asia cells stay valid targets. The
preview counts the same hosts as source-outside-director-region and the
poll summary reports skippedOffRegionSourceCells. Temporary stopgap.

Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010
2026-09-23 03:15:52 -04:00
Jinwoo Hong 51c3434851 chore(relay): treat Asia cell c30 as a general cell now that it is promoted (#22439)
C30 was promoted to general on 2026-09-23 (selector generation 286). The
same-cap wave now rolls it as a general cell instead of handing it back
isolated, and the shadow gate reads its pool beside C27-C29. Follow-up to
#22375.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-23 02:55:27 -04:00
Jinwoo Hong 293c2508fc test(mobile): move the session closure pin past the structured tool-line module (#22430)
#22349 added `src/shared/structured-agent-session-tool-call-block.ts`, which the
projection and live turn the session route already reaches import. The PR was
src/shared-only, so its CI never ran the closure suite; main's pin stayed at 4215
while the closure measures 4216.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-23 02:34:45 -04:00
Neil 17ffbf3b31 fix(runtime): answer startup terminal queries for background-created terminals (#22384)
* fix(runtime): answer startup terminal queries for background-created terminals

A runtime-created terminal (orchestration worker-start, `orca terminal
create`) has no renderer pane until the user opens its tab. Main only
answers terminal queries for PTYs the renderer marked hidden, so on a
fresh app sitting on the landing screen nothing answered the agent's
startup cursor-position query. Muse waits ~2s per unanswered CPR and then
exits 0 with no output, which surfaced as `agent_readiness: timeout`.

Background runtime spawns now carry initiallyHidden, mirroring the
renderer's hidden-at-spawn path: fresh daemon sessions are marked before
byte zero, the committed id is marked and paced as backgrounded, and the
mark is released on failure, reattach, or adoption. A pane that later
mounts visible unmarks and restores from the model snapshot as before.

* fix(runtime): keep background PTYs hidden and paced across renderer reloads

A runtime background spawn has no renderer pane to report visibility or
re-mark it hidden, so it synced as foregrounded (no backpressure thinning)
and a reload/crash gate reset cleared its hidden mark, leaving startup
queries unanswered. Track runtime-owned hidden marks: they survive
renderer-scoped resets, count as known-hidden for backgrounded pacing
until a visible report, and are released by a renderer unmark or PTY
teardown.

* fix(runtime): don't re-hide a background PTY whose view mounted visible during spawn
2026-09-22 23:00:11 -07:00
Jinwoo Hong b864a1c775 test(mobile): repin the recording corpus to main's tip after #22381 (#22407)
#22381 pinned its own branch commit, which the squash left off main; the corpus now pins main at 996f9cc306, the tree its fenced paths match. Every golden changes only its baseline line.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-23 01:53:52 -04:00
Neil 0b16a31e6e fix(runtime): budget explicit terminal close for the daemon's immediate-kill verdict (#22385)
* fix(runtime): budget explicit terminal close for the daemon's immediate-kill verdict

Explicit terminal close (worker-release, worker-stop, `orca terminal close`)
gave the daemon kill RPC a fixed 2s deadline. The daemon's immediate kill
captures descendants, SIGTERMs them with a 2.5s verification window, then
waits up to 8s for the root's physical exit. An agent that runs exit hooks
after SIGTERM (Muse: ~3s) outlived main's 2s timeout, so close reported the
PTY unverifiable and worker-release returned release_unknown even though the
daemon confirmed the exit ~200ms later.

Derive the close budget from the daemon's own immediate-kill reply budget
(now in an import-free module) plus 2s for the post-kill inventory check. A
process that exits within the daemon's budget is released; a wedged process
or unreachable host still times out as unverifiable.

* fix(runtime): budget the force-kill retry and exercise an expired close deadline

* test(runtime): drop tautological deadline-expiry assertion
2026-09-22 22:44:14 -07:00
Neil eb18eaf2b6 feat(usage): add Muse Code local usage provider (#22379)
* feat(usage): add Muse Code local usage provider

Scan Muse session logs (including subagent logs, which hold usage the parent
log does not) for model_completed token events and surface them as a fourth
local usage provider: shared scan worker, persisted per-file cache reused by
mtime/size, cross-log dedupe, Stats tab, and Usage Overview integration.
Muse logs carry no price, so the provider reports tokens only.

* fix(usage): name Muse in Stats & Usage copy; skip partial-cost warning when nothing is priced

* fix(usage): surface unreadable Muse sessions root; name Muse in remaining Stats & Usage copy

* fix(usage): count distinct same-content Muse records within one log
2026-09-22 22:44:10 -07:00
Jinwoo Hong 9fef7a0f04 fix(cloud): gate the Asia canary on its own cell's SQL failures, not the directors' (#22405)
* fix(cloud): gate the Asia canary on its own cell's SQL failures, not the directors'

The production canary summed sqlFailuresDelta over every director and the
canary cell and required zero. Directors log a steady baseline of
relay_cells NOWAIT and lock-timeout refusals unrelated to the canary cell,
so a C30 canary failed most attempts on that noise. The canary now requires
zero SQL failures from the canary cell's own metrics and records the
director sum as directorSqlFailures without gating on it. Directors keep
every other rule (unavailable regions, fallbacks, pool waiting, transient
waiter and wait-time bounds). Staging keeps the combined zero rule and its
evidence shape unchanged.

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

* fix(cloud): gate the Asia canary's pool bounds on its own cell too

Directors also show a steady pool-wait baseline (waiting above zero and
waits over 50 ms in about 6 of every 60 minutes), so a five-minute canary
still failed about half the time on director pool pressure unrelated to
the canary cell. With gateDirectorDatabase off, the production canary now
applies databasePoolWaitingMax, databasePoolWaitersMax and
databasePoolWaitMsMax to the canary cell's metrics only and records the
director values under director-prefixed names. Directors still gate Asia
selections, region fallbacks and unavailable regions. The staging path
keeps its combined values, key order and validation order.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-23 01:43:26 -04:00
Brennan Benson 8757e40063 fix(native-chat): keep a structured agent's tool line between tool calls (#22349)
* fix(native-chat): keep a structured agent's tool line between tool calls

A structured session's status named a tool only while the call was still
running, so the sidebar's tool line went blank whenever the agent was
thinking or writing between calls. Terminal agents keep naming the finished
tool until the next one starts, and clear it after a failure. The structured
status projection now does the same: a running call wins, otherwise the
turn's newest root call if it completed.

* fix(native-chat): bound the structured tool line by the running turn, not the user row

A send made while a turn is running writes its user row into the journal
straight away, and the turn keeps going. Stopping the scan at that row
blanked the tool line while a tool was still running. The scan now runs to
the turn record and names a call only when that record is still running, so
a turn that already ended never lends its last tool to a pending follow-up.

This lookup was the running-only lookup's only production caller, so it
replaces that lookup instead of sitting beside it.

* fix(native-chat): keep naming a structured agent's failed tool until the next one

Clearing the tool line after a failed call brought the blank gap back for
much of a turn: Codex marks any nonzero exit as failed, so a search with no
match or a red test run is enough. The failure already shows on the tool's
own row in the transcript. The running turn's newest running call still
wins; otherwise its newest root call is named whatever it settled to.

* fix(native-chat): name a structured Codex edit on the tool line as the chat draws it

Once a Codex edit's changes exist, its apply_patch call becomes a diff row,
which the status lookup skipped, so the row named the command before the edit.
The chat's tool-call block for a journal row now comes from one shared builder,
and the status lookup reads the same definition: a diff is named as Diff with
its path, and counts as settled since it carries no lifecycle.

* docs(native-chat): describe the structured tool field as running-or-latest

The status summary's toolName/toolInput now name the running turn's
latest tool between calls, not only a running one. Update the wire type
and status bridge comments that still said "the running tool".
2026-09-22 22:42:31 -07:00