The replay re-ran the whole accepted frame, so a surface the host retired
after accept (a phone close whose remote PTY is still exiting, or a closed
chat tab) came back. Every other surface now keeps the host's current
decision; the removal repair is extracted from the terminal retirement
helper so non-terminal tabs are removed the same way.
* fix(startup): read the install-dir package ACL as SDDL so a repaired tree reads clean on every Windows language
The Windows install-dir permission probe parsed icacls's display output and
recognized the well-known package grants by their English names. On
Chinese/Japanese/Korean Windows icacls keeps 'NT AUTHORITY' in English but
translates the package names, so a tree the repair had just fixed read as
poisoned AND reliable: the pre-window repair re-armed every launch, the dialog
blamed the install, and the safe-graphics fallback was withheld.
Read the DACL with 'icacls <target> /save <tmpfile>' instead, which writes
SDDL (SIDs, AC alias) on every locale, and drop the English-name heuristic and
its wellKnownNameCheckReliable flag.
* fix(startup): reject incomplete saved Windows permission records
* fix(windows): reject ACL exports from unsuccessful probes
---------
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
* fix(startup): hold desktop activations until the startup window exists
A second-instance, open-url, or open-file activation that landed after app
ready but before the startup window was created opened its own main window.
The startup open then built a second window, threw on the duplicate
'window:isMaximized' handler, and aborted runtime launch: no runtime RPC,
no orca-runtime.json, an orphan hidden window holding the trusted-renderer
id, and the visible window refused trusted IPC.
The desktop activation gate now starts 'initializing' for every launch mode.
The startup window opener releases it once that window exists, so queued
activations focus it, and a desktop launch that fails first releases it so
later activations can still open a window. Serve mode keeps settling the
gate after its RPC is ready.
* fix(startup): release desktop activation after failed ready prerequisites
---------
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
* fix(sidebar): "Hide default branch" hides a folder project's root workspace
A folder project's root is its main workspace but has no branch, so the
non-empty-branch check never matched it. Folder projects can now hold several
workspaces, so the root is the same noise the setting hides for git projects.
Decide by repo kind so detached-HEAD and offline-SSH git mains stay visible.
* test(sidebar): name the empty-branch git main case for what it tests and refresh the entry-point comment
* fix(sidebar): reveal imported folder roots with archived siblings
---------
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
* fix(terminal): a pane disposed mid-spawn no longer kills its successor's shell
A new terminal tab whose pane remounts while its first pty:spawn is in flight
is handed the SAME PTY by main's pane-spawn reservation. The disposed first
transport then killed that PTY as an orphan, so the tab closed on pty-exit
(focus fell back to tab 1) or stayed bound to a dead shell. Reported on SSH
worktrees (scan 22).
A transport destroyed mid-spawn now asks the pane surface first and keeps the
PTY while the tab exists, the worktree is not being deleted, and any layout
still names the leaf (leak-over-kill). A live transport refusing the id via
admitPtyId still kills unconditionally (#11003).
Adds rate-limited, id-hashed crash breadcrumbs for the next report:
terminal_fresh_spawn_retired (killed vs retained), terminal_tab_pty_exit
(host kind, ms since spawn, synthetic), terminal_active_tab_auto_move
(active-terminal repair, createTab orphan sweep). The two duplicated tab
pty-exit handlers now share handleTerminalTabPtyExit, and the FNV id hash
used by two crumbs moves to crash-breadcrumb-id-hash.ts.
Ports and supersedes #19386 (disposed-spawn-retention and its unit tests,
credit to its author); its Docker SSH e2e specs are not included.
* fix(terminal): scope spawn retention to its host and keep the PR focused
---------
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.
#23049 added a useRef, a useLayoutEffect and a useEffect to the terminal pane's
chat-state, layout-persistence and title-effects hooks and merged with the
parity shard red, so main fails 'preserves the recursively flattened render
hook order' (211 vs 214). Pin 214 hooks, 7 useMemo, and the new order hash.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
directPathForEndpoint hand-rolled Tailscale detection that called any 100.x
address a tailnet and missed Tailscale IPv6, so the same endpoint could be
labelled differently from the direct connection log. It now uses the shared
isTailscaleEndpoint. Also: a stale no-relay comment, a duplicate routing doc
line, and the overlay chain settling to Promise<void>.
Co-authored-by: mmarabel <166927047+mmarabel@users.noreply.github.com>
Co-authored-by: Neil <neil@stably.ai>
* Provide the Orca CLI automatically in managed WSL terminals
* Simplify managed WSL CLI provisioning
Never block a shell on CLI availability, keep the shared WSL login-shell
builder unchanged, provision from PTY env assembly only, drop the error
variable and command probing, and reuse the existing WSLENV helper.
* Scope the managed WSL CLI to WSL terminals
Provision only for WSL panes and publish the directory through
addOrcaWslInteropEnv, so daemon terminals keep inherited WSLENV entries and
non-WSL builds never see the variable. Write the bridge with a UTF-8 BOM so
Windows PowerShell 5.1 keeps non-ASCII user-data paths, give the dev bridge the
dev launcher's app-launch env, and drop the unused skill-setup wiring and
runtime capability.
* Tighten the managed WSL CLI bridge and setup
Launch the bridge child exactly like the registered bridge (no hidden
window or output relay; verified through WSL with Node and Electron), give
the dev bridge the dev launcher's NODE_OPTIONS stash, clear the guest-only
directory before starting Windows processes, collapse setup into one
function, warn once, and guard WSL env routing with tests.
* Harden managed WSL CLI quoting and inheritance
PowerShell also ends single-quoted strings at typographic quotes, so a
user-data path such as O'Brien with a curly apostrophe broke the managed
bridge. Fix the shared quotePowerShellLiteral and reuse it. Drop an inherited
ORCA_WSL_CLI_DIR on the daemon path, remove the unreachable PATH dedupe, and
cover failed setup with a stale caller value.
* Cover the managed WSL CLI in zsh and on POSIX CI
Add a live zsh case that reaches a real prompt, a POSIX test that runs the
PATH restore snippet in bash and zsh under set -u, and a null result for
unwritable user data. Say what a failed write actually costs, and document
per-spawn write logging and older-daemon behaviour.
* Keep system bashrc out of the PATH restore test
CI runners make bash -c read /etc/bash.bashrc, which fails under set -u.
---------
Co-authored-by: Orca Worker <orca-worker@localhost>
* 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
* fix(tasks): read a malformed saved Linear team selection as sticky-all instead of crashing the page
A persisted defaultLinearTeamSelection that is not a string array (a string
reached 1.4.207, report 0a2b6e7f) threw '(t ?? []).filter is not a function'
inside a commit-phase effect and tripped the page.tasks error boundary. The
value is now normalized where the page reads it and where a host projects it
to paired clients; anything but a string array means sticky-all.
* fix(mobile): read a malformed saved Linear team selection as sticky-all
A host that predates the desktop fix projects its raw store value, so the
mobile Linear list must tolerate the same string shape. Also trims the
desktop helper's comments to the why.
* fix(tasks): validate projected Linear team IDs and refresh parity contract
---------
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
A renderer frame published before a cold-restored terminal's PTY registered
was fenced to an empty tab list and recorded as accepted, and the renderer
never resends unchanged content. When registerPty binds a surface the
accepted frame fenced out, re-merge that frame so the fence reads current
state.
#22956 re-recorded with baseline = its own branch commit c5c1477ad4, which
the squash merge left unreachable from main, so the RPC recording pin guard
fails on main and every open PR. Repin to main's tip and re-record: only
baseline and lockfileSha256 (main's lockfile moved in #22961/#22964) change;
every recorded body is identical.
* fix: repair a bounded stale project-icon case
Based on the report and proposal by @mmarabel.
Co-authored-by: mmarabel <mmarabel@users.noreply.github.com>
* fix(projects): preserve peer-owned remote metadata
* fix(projects): keep enrichment within the owning host
---------
Co-authored-by: mmarabel <mmarabel@users.noreply.github.com>
Keep the caller distro across the Windows bridge, including drive-mounted
working directories, and pass it through the existing account imports.
Retain the contribution from PR #17093 and cover empty/space-containing
bridge arguments, platform boundaries, and ambient environment conflicts.
Co-authored-by: Joao Nicola <jgrnicola@gmail.com>
#22958 made the page on Android report no composing range with a user-agent check inside the shared
live-input hook. Host facts live in a `src/platform` pair, so the check moves to
`live-input-composing-range.web.ts`; the native file passes the event's range through. Behaviour is
unchanged. The hook test mocks the seam; the user-agent cases move to the seam's own test.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): give each host field one writer so relay routing can't revert edits
Relay learners (director re-resolution, credential rotation, direct-to-relay
upgrade) saved the whole HostProfile snapshot their connection opened with,
so a late relay write reverted an Edit Host endpoint and rewrote the device
token. The relay overlay also stored a copy of the paired address, which the
direct probe kept dialling after an edit.
Pairing is now the only full-profile writer (savePairedHost, fenced by a
census). Learners call setRelayRouting(hostId, relay), which never touches the
row or keychain, refuses a removed host, and skips unchanged routing. The
overlay is relay-only in memory; one serializer writes the v2 shape older
builds parse, without the direct entry. Saving a new endpoint rebuilds even a
Relay-active client from the saved row.
Co-authored-by: mmarabel <166927047+mmarabel@users.noreply.github.com>
Co-authored-by: Neil <neil@stably.ai>
* test(mobile): re-record RPC goldens for relay-only host routing
Baseline moves to the product commit. adapterSha256 moves for the pairing and
relay-credential adapters, which now read relay.relayHostId and inject
saveRelayRouting. Only the four direct-upgrade bodies change: the settled host
no longer carries the overlay's endpoints copy (direct-primary, relay-primary)
or the duplicate relayHostId; relay and every effect are unchanged.
Co-authored-by: mmarabel <166927047+mmarabel@users.noreply.github.com>
Co-authored-by: Neil <neil@stably.ai>
* refactor(mobile): relay learners own only relay routing
Follow-up to the field-ownership split. The supervisor keeps its host
read-only and holds the relay in a small owner that persists moves through
setRelayRouting; the direct upgrade returns { relay, bundle } and the
lifecycle composes the profile once. With one direct endpoint left, the probe
takes a bound openDirect and the lifecycle computes the direct path once.
One name per writer: setRelayRouting and savePairedHost are also the
dependency keys, and the removed-host error is RelayRoutingHostRemovedError.
The census now counts real value imports of savePairedHost, not mentions.
Co-authored-by: mmarabel <166927047+mmarabel@users.noreply.github.com>
Co-authored-by: Neil <neil@stably.ai>
* test(mobile): re-record RPC goldens for the { relay, bundle } upgrade result
Baseline moves to the refactor commit, and adapterSha256 moves for the
pairing and relay-credential adapters (dependency keys renamed to
savePairedHost and setRelayRouting). Only the four direct-upgrade bodies
change: the settled upgrade result is now { relay, bundle } instead of
{ host, bundle }, with identical relay, bundle, state and effects.
Co-authored-by: mmarabel <166927047+mmarabel@users.noreply.github.com>
Co-authored-by: Neil <neil@stably.ai>
* refactor(mobile): refresh edited hosts and let the establisher own relay
Edit Host now reuses refreshHostClient, which already rebuilds an owned
client (relay-active included) from the saved row after re-pairing, instead
of a savedAddressChanged flag on forceReconnect.
The session establisher, the only relay dialer, holds the relay and adopts
learned routing through setRelayRouting; the supervisor takes a host id and a
required relay, so the no-relay guards and the synthetic upgraded profile go.
enqueueHostListMutation returns its operation's value, and the overlay store
names its API after routing.
Co-authored-by: mmarabel <166927047+mmarabel@users.noreply.github.com>
Co-authored-by: Neil <neil@stably.ai>
---------
Co-authored-by: mmarabel <166927047+mmarabel@users.noreply.github.com>
Co-authored-by: Neil <neil@stably.ai>
* 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>
* 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.
* fix(native-chat): name a chat write by its target, not the owner generation
A write carried the fence of the last frame the pane read, and the host refused it
unless that fence was still current. An idle release and the restart after it each
move the fence, and the release publishes nothing, so a send after a release was
refused "Expected runtime fence 1; the session is at 3", and a Stop queued behind a
cold start was refused as stale.
Every write already names what it acts on: a send its conversation, a cancel its
turn, a prompt answer its item revision, a rewind its epoch; an option is
last-writer-wins. So admission stops comparing the client's fence, and the rebase
that papered over one restart (admitAtResumedFence, resumedFromFence) goes with it.
The writer-lease check stays, and so does the attach's compare-and-swap.
Frames now stamp the fence read when each frame is sent instead of a copy each
subscriber kept, which went stale on the same release.
* docs(native-chat): say mutation admission checks only the writer lease
* docs(native-chat): drop the send rebase from comments that still described it
* fix(native-chat): keep each pane's own fence on frames so a failed restart is not resent
* docs(native-chat): drop the fence from the admission the send effects run behind
* docs(native-chat): give the fence move on release the reason that still holds
* docs(native-chat): stop citing a write fence check in launch and mailbox comments
Three places still gave the removed fence check as a reason: the launch replay said admission puts the ledger ahead of the fence, the launch surface said a send must name the lease it was admitted against, and the direct-mailbox path said the lease fence decides whether delivery is safe. Admission now checks only the writer lease.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(native-chat): report a chat's owner from its record, not its running agent
Released desktop clients gate worktree activation on agentSession.handoffStatus
and count a chat tab as claimed only when the owner is `native`. The host
answered `native` only for a live lease, and threw not_attached for a chat idle
release had forgotten, so a chat at rest (idle-released, or restored after a
restart) blocked its whole worktree from activating.
The answer now comes from the record store for any record this host supports:
the owner is the lease's runtime kind whatever its liveness, and manual recovery
still answers `none`. With no map entry needed and nothing to wait for, the
serialized read that kept a mid-start chat's answer honest goes too.
* test(native-chat): pin the two owner answers that still refuse to vouch
With liveness gone, the manual-recovery branch and the unsupported-record
refusal are the only paths that keep a chat from answering native; neither
had a test.
* refactor(native-chat): say plainly why an unsupported chat reports no owner
* test(native-chat): say what a blocked activation gate actually skips
* 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.
* fix(native-chat): every journal append reaches the chats that are open
A journal write and its delivery to open readers were two calls, and some
writers made only the first. A failed start whose lease could not be handed
back, a provider revision with no frame behind it, and eviction's settlement
were all journaled without reaching an open chat.
A journal handle now reports every durable change, and the host's session map
binds that report to the session's readers when the handle is set. Writers no
longer publish what they append; the per-writer publish calls are deleted.
* test(native-chat): an epoch replacement reaches the open chat
* test(native-chat): each row reaches an open chat once, and a live handle enters only through the map
* perf(native-chat): a publish behind a delivered commit reads nothing
Each commit now delivers itself, so the publish a provider frame still sends
afterwards found every reader caught up but still read rows and rebuilt the
timeline for each one. A caught-up reader now skips the read.
* test(native-chat): state why the teardown test's fake journal is safe to cast
---------
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(mobile): extract the once-per-route terminal viewport measure
Behaviour-preserving move of measureViewportOnce into a pure module so the
first-subscribe ordering can be tested without mounting the session route.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): send phone dims on a terminal's first subscribe
A fresh terminal document has no xterm until its first init, so the viewport
measure could not answer before the first subscribe. The subscribe went out
without dims, the host serialized the snapshot at the desktop's size, the
phone painted it, measured, resubscribed and replayed the whole snapshot a
second time at the phone's size.
The first subscribe of each document now opens an empty terminal, measures,
and only then subscribes, so the host fits the PTY before serializing and the
phone paints once. Measure failure still subscribes without dims and the
existing fit pass takes over.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): converge the fit pass on the viewport the subscribe sent
A frame-height refit can clear the measured flag after the first subscribe
has already carried the phone's viewport. The fit pass then treated the
snapshot as dimensionless and resubscribed, replaying the whole snapshot a
second time at the same size. It now judges convergence against the
viewport that subscribe actually sent; the refit still owns real layout
changes through updateViewport.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): reject the failing measure without an async wrapper
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): keep the fit-pass decision on the measured flag
Converging on the viewport a subscribe carried accepted a stale one: after
native chat covered the terminal through a rotate or dock, the return
resubscribe sent the old dims, the host matched them and the pass never
re-measured (#4579). The decision is back on the measured flag; the sent
viewport only lets a matching fresh measure skip the round trip.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): forget a document's first-subscribe mark only on web-ready
The terminal handle re-attaches null then new on every theme or text-size
change, and clearing the mark there let the next resubscribe post an empty
init onto a live document. web-ready is the one signal that a new document
started, so the mark is dropped there instead.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): pin each held-subscribe teardown guard on its own
Splits the teardown test so dropping either the generation check or the
in-flight check alone fails a test, and shares one MutableRef type.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb