* perf(relay): release canceled AI Vault startup requests
* test: pin the historical hourly package version fixture
* test: provide the historical package version through build options
* test(ai-vault): cover delayed readiness rejection across restart
* fix(ci): preserve focused Playwright file selection
* test: isolate historical hourly build version inputs
* test: align historical package input with shared CI repair
* test: inject hourly package version without module mocking
* test: share the hourly package input contract across CI repairs
* test(ci): require focused commands on every platform
Assert each golden platform step and its focused arguments before deduplicating Playwright discovery probes.
---------
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: Neil <neil@stably.ai>
* fix: close abandoned PDF.js development asset streams
* test: keep casting safety comments attached after formatting
* fix(i18n): restore diff note draft catalog entries
* fix: guard closed PDF streams and preserve foreign listeners
* fix(i18n): make AI the recipient of diff notes
* test(browser): report the phase of WebRTC probe timeouts
* test: pin the historical hourly package version fixture
* test: provide the historical package version through build options
---------
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
* fix: close abandoned static web readers and align AI note labels
* test: pin the historical hourly package version fixture
* test: provide the historical package version through build options
* fix(windows): ship the process-table addon to the relocated daemon host
The Windows terminal daemon runs from a copy of the app under
%LOCALAPPDATA%\Orca\daemon-host\<version>. That copy took node-pty but not
@vscode/windows-process-tree, so the daemon's bare require of the addon found
nothing and every process-table read (foreground tracking, descendant sweeps)
fell back to a powershell.exe Get-CimInstance scan (#16905).
- Copy the addon's runtime files (package.json, lib/, the .node binary) into
the host; the ~25MB of gyp intermediates beside them are filtered out.
- Treat a host missing those files as unmaterialized, so hosts built before
this are rebuilt, and skip relocation if the install itself lacks them.
- Log the daemon's native/CIM capability at startup and warn once when the
process table falls back to CIM.
Revives #19525 on current main.
* test(windows): locate update-survival loss before relaunch
* test(windows): preserve daemon tree before update-survival proof
* test(windows): distinguish Electron exit from launcher close timeout
* test(windows): verify process exit when inherited pipes delay close
* test(windows): trace installer process checks in isolated survival runs
* fix(windows): probe process-query capability before installer sweep
* fix(windows): match installer probe and process-check profile behavior
* fix(windows): use NSIS separators for the process-check include
* test(windows): dismiss session-search overlay in survival harness
---------
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
* perf: scope relay capacity checks to the requested cell
* fix: restore catalog entries required by the current CI baseline
* fix(i18n): make AI the recipient of diff notes
* fix(persistence): identify reused Windows profile-owner processes
* fix(persistence): preserve absent-owner recovery without native registry
* ci: build Windows registry before native profile identity checks
* fix(cli): include native profile-owner dependencies in typecheck
* test: register native profile owner test in Windows PR lane
* test: use resilient Windows profile-owner cleanup
---------
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
* Reduce redundant CI runs, store uploads, and fixture processes
* Avoid repeating draft-independent mobile checks on readiness
---------
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Bundle a pinned, verified Bun runtime for headless Orca so existing Node launch commands can hand off before opening a profile. Keep desktop execution on Electron.
Add the Bun SQLite adapter and terminal backend, bounded shutdown, process inspection and cross-platform artifact qualification. Keep future managed SSH deployment separate from current production launch paths.
Migrate profile state to SQLite and move writes and backups into a background worker. Acknowledge terminal, SSH and automation changes only after durable saves. Preserve JSON import, recovery, rollback and compatibility exports.
Validate migration, worker failures, maintenance, cross-profile moves and terminal lifetime races with unit, integration and end-to-end coverage.
* Add translations for chat resume, Git toolchain, and notebook support
* Fix translation terminology in French and Korean locales
- Standardize Korean terminology from "restart" to "resume" for chat
resume functionality
- Clarify French error message for conflicting Orca windows/terminals
- Fix Korean context translation (문맥 → 컨텍스트)
* fix jupyter notebook translation
* chore(i18n): translate 113 new keys to es/fr/ja/ko/zh
Delta since last scan (80e0bee23b): 113 new en keys (session search,
resume-modal activity, ipynb venv, Cursor accounts, feature tips) plus 4
changed values (mid-turn -> working resume copy, screenshotsHint +maxSize).
es +111, fr/ja/ko/zh +100 each; strictly additive, 0 keys removed.
Three subagent review rounds: R1 fixed 20, R2 fixed 5 (incl. reverting an
NBSP convention misread), R3 signed off clean with full placeholder parity.
Gates: catalog verify, coverage --check, extraction, and locale vitest
suites all pass; fr/ja/ko/zh at 100% coverage.
* fix(native-chat): every lease latch has a way to die
A failed exit settlement no longer leaves the lease in recovery: the release
writes no stage and keeps the exit in its death evidence, and whatever the dead
generation left running is settled from that evidence at the next acquire or
read restore. The settlement retry flag, its disposition and every branch that
read it are gone. A reservation that recorded no process is released at
startup and after a failed start, the never-written conflicted status and the
processless proof are deleted, recovery resolution always concludes, and Codex
records its child's identity at spawn, before the handshake.
* test(native-chat): a re-create needs a release proven by death evidence
* test(codex): the child's pid is reported before the handshake
* test(native-chat): type the crash and exit fixtures without casts
* fix(native-chat): wait out a terminal owner an older build recorded, in recovery rather than manual recovery
* test(native-chat): a chat mid-turn at quit reopens idle, and an older build reads an unproven release
* test(native-chat): explain the baseline store cast
* fix(native-chat): a terminal owner's refusal names the process instead of recursing
Opening a chat whose terminal owner an older build recorded threw a stack
overflow instead of the refusal that names the process to quit.
* fix(native-chat): wait out a terminal owner recovery cannot verify instead of releasing it
A terminal agent an older build recorded keeps its PTY across an Orca
restart, so a probe that cannot answer (a start-time read that fails on a
loaded host) is not evidence its transport is gone. Releasing it let a
native child resume the same conversation beside the live terminal agent.
Only proof of its exit now ends the claim.
* ci(cross-version): run the unproven-release downgrade test
The sharded unit job excludes tests/e2e/cross-version-wire, and the
cross-version job runs an explicit list that did not name the new test,
so it never ran in CI. A change to the record validator now also starts
the job.
* refactor(native-chat): map the retired manual-recovery stage to recovering at decode
Nothing in this build writes manual-recovery, and restart reconciliation
already rewrites it. Mapping it where the other retired handoff stages are
mapped removes it from the in-memory lease type and deletes the branches
that could only see it: the acquisition refusal, the renewer skip, the
unproven-release stage check, and the handoff-status 'manual recovery is
required' answer. Older builds accept recovering, so a record written back
still loads after a downgrade.
* docs(native-chat): say what happens to a live child an ownerless reservation leaves
The reaper runs once at store open, while the unreconciled lease still
claims the child's token, so it does not stop that child on this launch.
The comment claimed it did.
* test(native-chat): name the each-case label for its role
* fix(native-chat): continue a create retried after recovery released its reservation
The client retries a create it never heard back from under the same operation id.
Recovery had released that create's reservation, so the retry was refused
agent_session_ownership_unknown while its row was pending, and
agent_session_operation_expired once the row aged out, and the chat never started.
A retry whose lease nothing holds now continues as a fresh reservation at the next
fence, which also stops the old reservation's spawn from committing.
* test(native-chat): name the refusal a replayed create used to get
* fix(native-chat): one quit-the-terminal-agent message for a chat a terminal agent holds
A chat held by a terminal agent an older build recorded frees only when that agent
exits. Sending said to reopen the chat and opening it said two runtimes claimed
it; both now say the chat is open in a terminal agent, name its process, and say
to quit it. Error codes are unchanged.
* ci: run PR checks on the rebased head
* fix(native-chat): name a terminal owner's process only when its start time can tell it from a reused pid
* test(native-chat): relaunch from the dying host's durable state, so its still-pending attach cannot race the new host
* feat(rate-limits): add Cursor usage tracking
## ELI5
If you use Cursor, Orca now shows how much of your monthly Cursor plan you
have used, next to the Claude, Codex and Grok meters, and in Settings →
Accounts. It reads the sign-in Cursor already saved on this computer and never
changes it.
## What changed
Cursor becomes a rate-limit provider like Grok: a status-bar meter (default-on,
with its own toggle), a row in the usage roster, and a Settings → Accounts
section naming the signed-in account.
The credential is read from whichever of three stores has it, first match wins,
all read-only:
- the macOS login keychain item `cursor-access-token` / `cursor-user`, which is
where `cursor-agent` 2026.06+ keeps the session;
- `~/.cursor/auth.json` and its platform variants, used by older CLIs;
- the Cursor IDE's `state.vscdb` (`cursorAuth/accessToken`), for people who
never run the CLI.
The keychain entry is the one current CLIs use, and reading only `auth.json`
finds nothing on an up-to-date macOS install. A locked keychain cannot mask a
readable `auth.json`, and a locked `state.vscdb` cannot mask either.
`~/.cursor/cli-config.json` supplies the account's email and display name; it
never holds a token.
Usage comes from the dashboard route the Cursor web dashboard itself reads,
because Cursor documents no individual-user usage API — every documented API is
team- or Enterprise-scoped. Per Cursor's pricing docs an individual plan has two
pools, Cursor Models and Other Models, both resetting with the billing cycle,
plus optional on-demand spend; each becomes a named bucket. The headline
percentage prefers `used / limit` over the sibling percentage fields, which are
pre-rounded for the dashboard's own copy. Because the route is undocumented the
mapping is defensive: an unrecognised payload resolves to `unavailable` and
hides the bar rather than publishing a zero that reads as "no usage".
Orca never runs `cursor-agent login` and never writes, refreshes or rotates a
Cursor credential. An expired token short-circuits to an actionable
"run cursor-agent login" instead of spending a request that can only 401 — not a
rare case, since `cursor-agent status` still reports `isAuthenticated: true`
against a token that expired months ago.
## Why this shape
Six open PRs implement this feature and none reads the keychain, so each finds
nothing for a large share of users; this takes the auth layer further and keeps
what those PRs verified live. The bar is not gated on `cursor-agent` being on
PATH, unlike other CLI providers, because an IDE-only session is real usage with
no CLI to detect.
`readKeychainPassword` moved out of the Claude keychain reader into
`src/main/macos-keychain/generic-password.ts` so both providers share one
`security(1)` wrapper. It is a byte-for-byte relocation, so Claude's credential
path is unchanged; the two child_process allowlists move the entry with it and
neither ratchet count changes.
Co-authored-by: Preschian Febryantara <preschian@users.noreply.github.com>
Co-authored-by: Qwesdy <qwezdi@proton.me>
Co-authored-by: ivo922 <github.concur614@passmail.net>
Co-authored-by: Mihail Vratchanski <mivrkiki@gmail.com>
Co-authored-by: Tauri-EPO <enrico.pin@gmail.com>
Co-authored-by: Raajik <44516546+Raajik@users.noreply.github.com>
* test(rate-limits): name the JWT helper's segment type in the Cursor tests
The anti-slop gate rejects a bare `object` parameter; the fixtures build a
claims record, so say that.
* fix(rate-limits): render Cursor's pools and keep its plan total visible
Review of the first commit found the meter effectively blank for a healthy
account, which the screenshots missed because the only Cursor session on hand
had expired and never reached the success path.
- The verbose status-bar segment filtered buckets through an allowlist written
for Gemini's experimental models, so both Cursor pools were dropped and the
fallback needed a `session` window Cursor never reports. A signed-in account
rendered an icon and no number. The allowlist now admits Cursor's pools, and
the fallback accepts a monthly window.
- `getWindowSections` dropped `monthly` whenever buckets existed. Cursor puts
the plan total there and its sub-pools in buckets, so a plan at 92% showed as
50% in the roster, the tooltip, and the tightest-usage pick.
- A plan reporting `enabled: false` still published its 0% pools, painting a
healthy meter for a pool the account does not own and skipping the
request-quota fallback.
- `redirect: 'error'` turned the dashboard's bounce to /login into a generic
network failure, hiding the actionable sign-in message.
- A busy `state.vscdb` (the IDE holds it open) surfaced as a provider error,
which would pin an alert bar on Cursor IDE users who never set Cursor up in
Orca. It falls through to "no credential" instead.
- Refreshing the Accounts section read the keychain twice for one update.
* fix(rate-limits): pin the platform in the Cursor keychain tests
Review caught three cases that assumed macOS: the keychain source is behind an
explicit `process.platform` check, so on the Linux CI runner the mocked read was
never reached and the tests read the CLI file instead. They now set the platform
they mean, and two new cases assert the off-macOS fall-through.
Also track the credentials reference doc (docs/** is ignored by default, so a
new reference needs its own allowlist entry) and give the visibility fixtures
their own provider id instead of Grok's.
* fix(rate-limits): prefer a live Cursor session and report a failed refresh
Review round two, from CodeRabbit and Pullfrog.
- Credential precedence returned the first token that parsed, so an expired
keychain token in front of a fresh Cursor IDE session reported "sign-in
expired" on every poll while a usable session sat one source below. A live
session now wins; the expired one is returned only when nothing live exists,
so the actionable message still appears in that case.
- The usage schema took `.optional()` where the route sends `null` for an absent
sub-object, so one null pool failed the parse for the whole body and threw
away valid pools and the billing cycle with it.
- Cursor usage could survive an account switch: a failed refresh for account B
kept account A's figures beside B's name in Accounts. The snapshot now carries
a hashed account fingerprint, and a known-and-changed identity clears the
previous reading. A refresh that names no account still keeps its own.
- The Accounts section rendered nothing at all when a signed-in account's fetch
failed, and could repaint an older account when two status reads overlapped.
It now states the failure — beside the numbers when a stale snapshot remains —
and ignores superseded reads.
- A web client claimed "not signed in" for a host it cannot read, contradicting
the meter beside it; it now says the detail is host-only.
- Signed-out copy named `cursor-agent login` as the only way in, though an IDE
sign-in works just as well.
- The census comment ended at 4219 after the pacer squash without naming the two
modules #22616 added; recorded them, re-measured on a clean origin/main.
- Narrowed the docs claim: Cursor documents all-plan APIs, but no individual
usage endpoint.
* fix(i18n): localize the web client's Cursor host-only notice
It reaches the Accounts pane like any other string, so the coverage gate is
right to want it in the catalog rather than allowlisted.
* fix(rate-limits): name the Cursor account on failed refreshes, and ship the reworded copy
Review round three. Both findings say an earlier fix did not actually take.
- The account-switch guard reads `authProvenance` off the fresh result, but the
fetcher stamped it only on success and network failures. The `stale-token`,
429, 5xx and parse results omitted it, and so did the expired-session branch —
so a switch whose first refresh failed, which is precisely the case the guard
exists for, still rendered the previous account's figures under the new name.
Every failure holding a readable session now names its account; a missing or
unreadable credential still names none. The service test also fed a result
shape the fetcher never produces, so it proved nothing; it now uses the real
stale-token shape, and the fetcher test asserts provenance across 401/429/5xx
and expiry.
- The reworded signed-out copy never rendered: a present catalog value beats the
`translate()` fallback, and `sync:localization-catalog` only adds missing keys
rather than updating changed defaults. Updated both strings in en.json, which
also prunes them from the runtime-required catalog now that they match.
* docs: keep the Cursor credentials reference out of the tree
Its content lives in the PR description instead; docs/** stays ignored rather
than gaining an allowlist entry for this branch.
* test(mobile): drop the census note main no longer pins
main removed `SESSION_ROUTE_MODULES` and re-pinned this lane on a different
count, so the paragraph this branch added documents a number series that is
gone. The branch touches nothing in this file now.
---------
Co-authored-by: Preschian Febryantara <preschian@users.noreply.github.com>
Co-authored-by: Qwesdy <qwezdi@proton.me>
Co-authored-by: ivo922 <github.concur614@passmail.net>
Co-authored-by: Mihail Vratchanski <mivrkiki@gmail.com>
Co-authored-by: Tauri-EPO <enrico.pin@gmail.com>
Co-authored-by: Raajik <44516546+Raajik@users.noreply.github.com>
* test(mobile): the page's live input sends a composed word's letters as they are typed
The reported shape, in a browser: `/tui` on an Android keyboard, where the `/` reached the
terminal and `tui` did not until Enter. Android keyboards hold a composing region over the Latin
word being typed, so on the page every input event mid-word carries `isComposing: true`, and the
preedit mirror holds reported preedit with no settle timer. Native Android reports no range, so
there each ASCII keystroke is sent as it is typed.
Driven through Chromium's own IME path with an Android WebView user agent: the letters after the
slash must arrive one by one, and a correction on commit must still erase and retype. The iPhone
case is the guard that a composition off Android stays held. The hook case says the same thing
without a browser.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): the page on Android reports no composing range, as native Android does
`handleLiveInputChange` passed `nativeEvent.isComposing` straight to the preedit mirror. On the page
that event is the DOM's, and an Android WebView marks the keyboard's composing region, which Samsung
and other Latin keyboards keep over every word. The mirror treats a reported range as preedit that
is not text yet, holds all of it with no settle timer, and Chromium fires no input event after
compositionend, so the word sat in the field until the next keystroke or Enter.
Native Android reports no range at all, and its fallback holds only a trailing non-ASCII run. The
page on Android now reports the same: undefined. ASCII echoes on each key, a Hangul or kana run
still settles on the timer, and a correction on commit still erases and retypes. iOS, where the
range is the text system's marked text, is unchanged.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* docs(mobile): name the Android page among the platforms that report no composing range
The mirror's fallback note listed only React Native Android; the page on Android now reports no
range too. The reader's docblock becomes the one line it needs.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(codex): turn off Codex daemon auto-start in homes whose socket path exceeds sun_path
Codex >= 0.157 auto-starts a background app-server daemon and connects to
<CODEX_HOME>/app-server-control/app-server-control.sock. Orca's managed homes
under userData make that path longer than sun_path (104 bytes on macOS, 108 on
Linux/Windows), so every interactive codex in an Orca terminal failed with
'path must be shorter than SUN_LEN'. The config mirror now writes a marked
[features] daemon_auto_start = false into only those homes, removes it when the
home fits, and never promotes it into ~/.codex.
* fix(codex): address review of the daemon socket guard
- A runtime config.toml holding only Orca's daemon override no longer reads as a
config-sync stall, so users without ~/.codex/config.toml get no false
"missing" warning in the accounts pane.
- The legacy shared-home refresh re-applies the guard, so retained pre-rollout
panes keep daemon auto-start off after a system-default launch.
- Warn once when an inline `features = {...}` or `[[features]]` blocks the
override instead of failing silently.
- Rename the upsert's TUI-specific internals now that it serves any table.
* fix(codex): apply the daemon socket guard even when the settings mirror stalls
When the settings write-back or mirror refused (unreadable baseline, failed
write to ~/.codex, unreadable source), the whole pass returned before the
daemon guard was applied. A home whose config.toml predates the guard then
kept failing with SUN_LEN on every launch for as long as the stall lasted.
The guard now lands on those paths too; the mirror itself is unchanged.
* fix(codex): guard managed account homes when ~/.codex/config.toml is missing
* test(codex): keep reset-credit ownership checks scoped to the retry, not service construction
* test(codex): build the account mirror test without a type cast
* fix(codex): keep blocking WSL ownership checks off the no-config guard pass
Guarding account homes with no ~/.codex/config.toml ran the WSL ownership
check, a synchronous wsl.exe call per account, at startup before the window
opens and on every account switch. WSL homes are guarded by WSL launch prep,
so that pass now covers host homes only.
---------
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* refactor(native-chat): remove the unused terminal handoff
No client ever called agentSession.requestHandoff or mounted the handoff
chrome. Delete the handoff coordinator, the terminal-owner runtime, the
proof write path and the unmounted UI. Keep agentSession.handoffStatus,
which released desktop clients read for worktree activation, and let
records an older build left mid handoff reconcile through the ordinary
restart and recovery paths.
* fix(native-chat): never let the pre-stop snapshot hold a chat's stop
Eviction now drains delivered events before quit's resume-offer snapshot. An
unbounded wait there sits ahead of the provider stop, so a sink whose journal
write stalls kept the child running until the step deadline aborted the
eviction. The offer is advisory: bound the drain and stop the child regardless.
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(native-chat): drop helpers only the terminal handoff called
`claudeAuthEnvCarriedForward`, `isPathWithinDirectory` and
`queryWindowsProcessRowsFresh` lost their last caller with the handoff. The
fresh-scan tests now go through `queryWindowsProcessDescendants({ fresh: true })`,
the teardown path that still depends on that contract.
Co-Authored-By: Claude <noreply@anthropic.com>
* docs(native-chat): stop citing the removed handoff in lifecycle comments
Six comments still named the handoff coordinator, a handoff suspend, or a
terminal-owned session as live participants in the flows they describe.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(native-chat): type the stalled snapshot drain without a cast
Co-Authored-By: Claude <noreply@anthropic.com>
* test(native-chat): pin that a start dead before proving owes no settlement
The removed restart handoff test pinned this branch; nothing else did.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(native-chat): keep the owner-status read behind an in-flight attach
The handoff removal dropped the per-session queue from `handoffStatus`, so a
read landing mid-start reported the reservation (no owner) instead of the
settled chat owner, and shipped desktop clients blocked worktree activation on
it. The read is queued again, as it was before the removal.
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(terminal): remove the agent-session PTY write gate
The gate only refused a write when a PTY had been bound to a chat session, and the
only code that ever bound one was the terminal handoff this branch removes. With it
gone, every admit/readmit returned "admitted" unconditionally, so the checks on the
renderer write path, the runtime controller backstop, terminal.send, agent prompts,
preview input and orchestration pointers, the refusal fields on terminal.send and
worker-start receipts, the plugin and CLI refusal copy, and the adopted-pane
orchestration routing could no longer run. Ordinary writes take the same path in
the same order as before.
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(native-chat): drop the transcript helpers only the handoff called
appendLegacyTranscriptMessages fed the terminal transcript catch-up and
proveClaudeTranscriptBranch backed the terminal owner's exit proof. Both lost
their last caller with the handoff. Their tests now go through the live entry
points instead: the roster bounds through the legacy import, the pinned-read and
growth tests through the ancestry replay the history window uses, and the marker
rules through the string proof in their own file rather than the session-file
resolver's.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(native-chat): stop calling a starting chat "mid-handoff"
A send refused because the chat's owner is not settled showed "The session is
mid-handoff (<stage>)." in the composer. With the handoff gone, the stages that
reach it are a chat that is still starting, or one whose previous agent process
has not yet been confirmed stopped. The message now says which of the two it is.
The refusal code is unchanged.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(native-chat): type the stand-in roster decoder without a cast
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(codex): name the pinned rollout lookup for what it does
With the terminal handoff gone, the module named codex-tui-rollout-proof holds
only the pinned rollout lookup that structured Codex launches use to resume a
thread, so the name described code that no longer exists. Rename the module and
its options type. Also drop a mobile allowlist assertion that pinned the
removed agentSession.requestHandoff method, which no longer exists to allow.
* refactor(native-chat): type the owner-status reply as the host sends it
The handoffStatus reply type still listed the terminal handoff's fields and
states (terminal placement, host label, proof retry, queued and waiting phases,
the to-terminal direction). No host writes them any more and the only client
reader parses the reply as unknown, so they described nothing. The reply on the
wire is unchanged.
* refactor(native-chat): normalize terminal-handoff lease values once at decode
Nothing in this build writes a terminal owner (`runtimeKind: 'tui'`) or the
handoff's `preparing` / `old-owner-stopped` stages, but the in-memory types
still admitted them, so readers across the host kept branches for values no
path produces and the compiler could not point at them.
The store now validates the on-disk shape, which still accepts those values so
an older record is not quarantined, and maps them once while parsing:
- `preparing` and `old-owner-stopped` become `recovering`
- a `tui` lease becomes `native`; when it records a process it also becomes
`conflicted`, the claim every build probes but never stops. A plain native
owner would be stopped by restart recovery, here and in older builds.
Revisions are taken over the normalized state on both sides of every compare,
and the mapped record reaches disk with the store's first transaction, the
same way the tab-id backfill does.
The in-memory types narrow to what this build writes, and the branches that
existed only for the removed values go. Structured-worker identity keeps its
verdict for a former terminal owner by refusing a conflicted claim rather
than a non-native kind.
* refactor(native-chat): stop threading the owner kind through a reservation
A reservation only ever names a native owner now, so the request no longer
carries a kind and the reserved lease records `native` directly. The attach
params keep `runtimeKind`: agentSession.ensure and create accept it, and the
operation fingerprint stored in the ledger covers it.
* test(native-chat): pin the legacy-lease rewrite with a transaction that changes nothing else
Hiding a tab also committed the visibility index, so the no-op transaction
wrote the file even when its open-time revision was wrong. Committing the index
first leaves the pending rewrite as the only reason to write.
* test(native-chat): give the legacy-lease store test a tab id so the backfill cannot supply its rewrite
The seeded record had no surface tab id, so the next open backfilled one and
that rewrite alone made the no-op transaction write. The test passed with the
legacy-lease rewrite signal removed.
* test(worktree-activation): restore the OMP surfaced-agent resume test
The handoff removal deleted it alongside the terminal-owner tests, but it
covers the surfaced-PTY block that still guards resume, including an agent
whose ownership is unknown.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(mobile): mount the page's terminal frame with its onLayout
On the page every branch of the session content is a bare View in one
slot, so the terminal frame reused the loading View. react-native-web
observes onLayout only on a View that mounts with it, so the frame never
reported its height: every measure fell back to the page's
window.innerHeight and the PTY got rows the frame cannot show (63 vs 47
on a 1280x2856 screen), with the last rows under the command dock.
Keying the frame makes it mount fresh. The parity pin moves for the key
string.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): size the terminal document to its host, not the window
Inside the WebView the window is the terminal frame; on the page it is
the whole page, header and dock included. Every fit, pan, scroll and
overlay bound in the document read window.innerWidth/innerHeight, so a
measure without a container height sized the PTY to the page.
The document now reads one seam, viewportSize: the window by default
(native unchanged), the host element's box on the page. A census keeps
raw window size reads out of document/.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): put the terminal document's coordinates in its host
Round one moved the page document's size to its host but left its
origin at the window, so a tap below the session header mapped rows low
(row 19 for row 11), and the selection overlay and scroll indicator,
position: fixed, drew over the page.
- viewportRect replaces viewportSize: the host's box on the page, the
window at 0,0 in the WebView. viewportPoint is the one place a client
point meets that origin; cell mapping, mouse reports, the pinch anchor
and edge scroll go through it, and a census holds the rest to deltas.
- observeViewport: the fit refits on a host ResizeObserver on the page,
a window resize in the WebView.
- A hidden host measures 0x0; the fit keeps scale 1 rather than 0.
- On the page the host is the overlays' containing block.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): state what native measured in the parity note
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): read the page terminal's overlay and refit off its host
The overlay is absolute inside the host now, and the page's refit
follows the host's box, so the owed-frame case pulses the host rather
than the window.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): hold the terminal fit while its host has no width
computeFitScale answered 1 for a 0-wide host, but applyFitScale still
committed that fit as soon as the cell width was known, so a display:none
screen got a fit for no box. The attempt now leaves the fit pending until
the host reports a positive width, and observeViewport starts it again
once it does: one commit, never at scale 0.
A regression test pins edge scroll to the host's edges: with the host at
top 100 and height 600, the down band starts at client Y 660, not 560.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): skip the terminal refit while its host has no box
react-native-screens hides the session with display:none when another
screen covers it, so the host's observer reports 0x0. The refit still
clamped pan and repainted, so coming back to the session lost the pan
the user left; native never refits on navigation. The refit now does
nothing until the host has a positive width, and the next real box
refits once.
Also untangles the seam-count sentence in document-host-seams.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): refit the page terminal only when its box changes
Coming back to a session takes the host from 0x0 to its old box. The
observer fired, the refit committed, and the user's pan and zoom were
reset, where native keeps them because navigation never resizes its
WebView. The fit now remembers the box it was committed for, and the
refit acts only on a positive box different from that one.
The frame tests wait on the document's own frames instead of a timer,
and the hidden-host case asserts the zero-width read happened.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): refit a shown terminal when a fit was asked for while hidden
The zero-width gate drops a fit requested while the host is hidden, and
the same-box skip then kept the stale fit when the host came back at its
old size: 80 columns at 390 px stayed at the 55-column scale, 0.945
instead of 0.65. Any fit request now forgets the last fitted box, and so
does a text-scale change that could not resize a hidden grid. A plain
hide and show asks for no fit, so it still keeps pan and zoom.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(agents): add first-class ZCode harness
Add ZCode (Z.ai's `zcode` CLI) as a supervised Orca agent: managed lifecycle
hooks on local, SSH and Windows hosts; status, question and approval reporting;
synthetic status titles; session resume; orchestration worker launch options;
and desktop + mobile agent-picker registration.
Written against the newly open-sourced `zai-org/ZCode` (agent CLI 0.16.9), not
against a remembered screen:
- ZCode's hook runner writes a Claude-compatible stdin alias set, so it routes
through the existing Claude-compatible vendor path while keeping its own
identity in the sidebar.
- `PermissionRequest` fires only once the approval card is on screen and racing
the user's answer, so it is proof the pane is blocked, not an auto-approval.
- ZCode's clarification tool is literally `AskUserQuestion` with Claude's
questions/options shape, so Orca's question card renders it unchanged.
- ZCode's `hooks.enabled` defaults to false, which is why configured hooks were
reported as never firing; the installer sets it.
- ZCode renames its own process to `zcode-cli`, so the expected foreground
process cannot be the launch command or dispatch refuses the pane.
- ZCode emits no OSC title in any state and repaints its ASCII banner forever,
so readiness comes from Orca's synthetic hook title and launch drafts wait on
the composer box rather than on a quiet render window.
Three files crossed their max-lines limit, so each is split along a real seam:
command-line entrypoint parsing out of agent process recognition, skill
classification out of skill root discovery, and registry coverage out of the
remote hook installer tests.
Refs #10564
* fix(zcode): drop the session-option catalog and pin the orchestration contract
ZCode's CLI exposes no `--model` flag at all, and the session-option launch path
refuses to apply any option until a model id is chosen. A catalog therefore could
not deliver `--mode` per worker, and would have accepted `--model` only to drop
it silently. Take opencode's position instead: no catalog, so `worker-start
--model` is refused with a clear message and ZCode launches with the model from
its own config. `--mode` stays reachable through agent args, which is also how
the yolo default is applied.
Add a contract test covering the parts that make ZCode a usable worker:
dispatchable foreground process, stdin prompt delivery, the prompt staying out
of the launch command, and the composer-gated draft paste.
* refactor(zcode): reuse shared helpers and cut the harness down
No behaviour change; every ZCode test still passes.
- Use installer-utils' own `hookDefinitionHasManagedCommand` instead of
re-walking a hook definition by hand, which also drops a local string reader.
- Share one `readZCodeEventMap` instead of keeping the same narrowing in both
hook-settings and hook-config-json.
- Collapse five identical error returns into one `zcodeHookError` builder, and
return early from the status branches instead of assigning through `let`.
- Split the event-to-status decision out of `normalizeZCodeEvent` into a pure
`readZCodeTurn`, so the normalizer reads as decide-then-build and stops
computing the tool name for events that never look at it.
- Take a script file name in `readManagedZCodeHookEvents` like its siblings,
which removes a `Parameters<typeof …>` indirection at the call site.
- Drop the unused `ZCodeHookEvent` export and inline a single-use path helper.
- Correct a stale comment: ZCode's loader is a strict `JSON.parse`, so the
in-place edit preserves key order and indentation, not comments.
* fix(zcode): address review — keep unmanaged event keys, correct comment, de-dupe README
- `removeZCodeManagedHooks` deleted any event key whose list ended up empty, so an
unrelated `"Notification": []` the user wrote was removed as collateral whenever a
managed hook elsewhere made the write happen. Only touch an event Orca actually
owned something in; covered by a new regression test.
- The `isNewTurnEvent` comment claimed UserPromptSubmit was ZCode's only turn
boundary while the expression below it also returned true for SessionStart. Say
what the code does: SessionStart lands the idle boundary, UserPromptSubmit is the
turn boundary (the Codex/Claude shape).
- ZCode appeared twice in the README's single agent-badge block; keep the
local-icon entry the link checker validates and drop the favicon duplicate.
* docs(zcode): call out that the desktop bundle's CLI cannot open a session
From live testing on #22464: pointing `zcode` at the desktop app's bundled
`glm/zcode.cjs` installs Orca's hooks fine but then fails with
`Cannot find package '@zcode/tui'`, so the pane never opens a session. The
symptom reads as a broken harness when the CLI simply has no TUI. Say which
build to use and how to check before reporting a problem.
Reported-by: JWu527
The skill-sharing release gate checks out the release tag, then restores
main's copy of skill-freshness-inventory.test.ts. That file holds behaviour
tests, so a behaviour test added on main runs against a tag that predates the
behaviour: #22606 added one and failed the v1.4.211 gate on macOS and Linux.
Keep restoring skill-provider-runtime-roots.test.ts from the workflow ref.
* fix(terminal): serialize only the visible width after a column shrink
xterm does not reflow the alternate buffer (or a normal buffer under
pre-21376 ConPTY), so after a shrink each line keeps its old length.
SerializeAddon walked every non-final row to line.length, so any snapshot
taken after a shrink carried stale right-hand cells that wrapped into extra
rows on replay; restores repainted that garbage and a differential TUI such
as OpenCode never cleared it.
Clamp the row walk and the wrap-boundary lookups to the terminal's columns
in Orca's addon-serialize source patch, and regenerate the bundles, maps and
lockfile hash per docs/reference/xterm-patch-regeneration.md.
* test(terminal): read shrink-snapshot fixtures through public APIs
Drops the private-terminal casts the casting gate flags; the normal-buffer case
now drives a plain pre-21376 ConPTY terminal and its SerializeAddon directly.
* fix(terminal): blank a wide glyph clipped by a column shrink when serializing
After a non-reflowing shrink a width-2 glyph can have its lead half in the last
column and its trailing half past the grid. Serializing the lead half makes the
replay wrap it to the next row and shift every row below, so serialize that
cell as a blank and keep the row exactly the grid's width. A glyph ending
exactly at the edge is unchanged.
* test(mobile): move the session closure pin past the main agent status modules
#22452 added src/shared/main-agent-status.ts and src/shared/agent-turn-outcome.ts,
which agent-status-types.ts imports, so the session route's closure grew by two
local modules (4218 -> 4220). That change was src/shared-only, so its own CI never
ran this suite; main has been at 4220 since, and any PR that fires the mobile web
app job fails on the stale pin. Measured on 4064653740 and on origin/main 3ea15dd0a2.
* test(terminal): differential serialize round-trip fuzz and transcript replay
Seeded VT streams (text, CJK/emoji/combining, SGR, cursor/edit ops, scroll
regions, DECAWM/IRM, alt-screen variants, DECSC, and shrink-heavy resizes)
drive a source terminal in three modes: reflowing normal buffer, alternate
buffer, and a non-reflowing pre-21376 ConPTY normal buffer. At each checkpoint
every SerializeAddon build under test serializes it, and each output is
replayed into a fresh terminal of the same size and compared cell by cell,
plus cursor, active buffer and modes.
CI runs 25 seeds per mode against a pinned list of pre-existing divergences,
and replays the committed PTY transcripts (the existing agent fixtures plus new
vim, less, pico, and OpenCode captures) under four resize schedules. Point
ORCA_OLD_SERIALIZE_ADDON at a previous patched build to also check byte
identity when no line is wider than the grid, and that no checkpoint regresses.
* test(terminal): build the differential serialize baseline from any git ref
config/scripts/build-serialize-addon-at-ref.mjs reverse-applies the patch that
produced the installed @xterm/addon-serialize dist, applies the ref's patch,
and verifies each step against the patches' blob hashes, so the fuzz can use
origin/main (or any fix commit) as its baseline without a second install.
ORCA_NEW_SERIALIZE_ADDON swaps in a built dist for the build under test, and a
seed-pinned test replays the nine I3 regressions found against origin/main.
* fix(terminal): serialize a clipped wide glyph as a width-1 blank
The stand-in for a wide glyph clipped by a column shrink came from getNullCell(),
whose width is 0. _nextCell skipped it as a wide trailer, and the row-end wrap
check counted the width-0 _backgroundCell as content, so a soft wrap after the
clipped column was taken as natural and replayed one column early
(conpty seed 1149: `abcdefghi中WRAPPED` at 12 -> 10 cols replayed as
`abcdefghiR`/`APPED`). Blank the cell in place instead: width 1, no codepoint,
its own attributes, so it counts as one empty column and forces the wrap.
Differential sweep vs origin/main, 7000 cases per mode: I1 0 byte diffs, seed
1149 fixed; the remaining I3 regressions are the trailing background-row seeds.
* test(terminal): neutral paths in the serialize fixtures and usage comment
The OpenCode transcript carried this machine's lane paths in its footer; replace
them with same-length neutral paths so the recorded cursor layout is unchanged.
* fix(terminal): keep trailing background-only rows when serializing
Without scrollback, _serializeString trims rows after the last content cursor.
A row made only of background-colored blanks emits its erase in _rowEnd but
never moved that cursor, so two or more such rows at the bottom were dropped
(4x3 `r1\r\n\e[48;5;157m\e[J\e[0m\e[3;1H` replayed with the last row blank).
Track the erase separately and extend the kept rows to it, except when the
cursor is wrap-pending: relative moves back from those rows cannot re-create
that state, and doing so regressed normal 508, alt 1425/6647, conpty 4699.
Harness: I1 now exempts checkpoints with a background row after the last text
row, the one place this fix changes bytes on purpose (scope helpers move to
serialize-grid-variant-scope.ts); conpty seed 5 leaves the pinned pre-existing
list. Sweep vs origin/main, 7000 cases per mode: I1 0, I3 regressions 0;
fixed/both-fail normal 967/3914, alt 2628/8316, conpty 6657/2937 (was
323/4558, 2296/8648, 5466/4128).
* test(terminal): narrow the I1 carve-out to where the serializer keeps background rows
Trailing background-only rows change bytes only when the serialized range has no
scrollback (the trimming path) and the cursor is not wrap-pending; checkpoints with
scrollback or a wrap-pending cursor are held to byte identity again. The 7000-per-mode
sweep against origin/main stays at I1 0 and I3 0.
* test(native-chat): pin that screen scrapers ignore kept background rows
The serializer now keeps trailing background-only rows, so a painted TUI's screen
read as text ends in \r\n\x1b[NX rows. Serialize the same frame with and without
them and check the Claude option scrape, the empty-prompt check and the fork
transcript read the same thing.
* test(terminal): read xterm core internals through a checked parser, not Reflect.get
The fuzz oracle reached xterm's private _core with Reflect.get, which the
low-evidence gate rejects. Narrow _core, writeSync and the DECSTBM bounds with
in/typeof checks into one named XtermCoreInternals shape instead.
* test(terminal): keep captured serialize transcripts byte-exact on Windows checkouts
* fix(terminal): prove an idle Git Bash prompt through its bin launcher
Git for Windows' bin\bash.exe is a launcher that runs usr\bin\bash.exe as a
child and waits, so an idle Git Bash pane's job always holds two pids and the
Windows shell proof never confirmed it. Accept exactly the launcher plus its
direct bash.exe child, checked against the identity process table.
* test(terminal): wait for the Git Bash prompt before asserting the hand-off job
* fix(terminal): prove a Git Bash prompt as one unbranched MSYS bash chain
Orca launches Git Bash as bin\bash.exe -c "chcp.com ...; exec \"$BASH\" ... -i",
and each MSYS exec leaves its pre-exec process alive as a stub, so an idle
pane's job is launcher -> stub -> interactive bash. Accept any job that is one
parent-to-child chain rooted at the launcher whose every later member is
bash.exe, instead of a fixed two-process shape.
* ci: register the Git Bash shell-proof win32 test in the package-test list
* fix(terminal): read the spawned shell as a path, and keep one shell map
A spawned shell path with a space (/Users/John Doe/bin/zsh) was split as a
command line, so the POSIX proof compared against "john" and never
confirmed. Local panes now keep only the spawned shell path and derive the
name from it; the Git Bash chain walk drops guards the member check already
covers.