mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
76d1c68ab5c8b90eab7bfdf027f54e23df239461
2952
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
76d1c68ab5 |
Resume in New Native Chat for OpenCode (#28072)
* feat(chat): Resume in New Native Chat for OpenCode terminal conversations An OpenCode conversation started in the terminal now offers "Resume in New Native Chat" in its Session History row menu and its tab's right-click menu, as Claude, Codex, Grok and OMP do: a new OpenCode chat takes it over, showing its history and continuing it. OpenCode 2.x rows (`opencode2`) and 1.x rows both open an OpenCode chat. Two pieces kept OpenCode off the shared adoption path. Its account is a locator, not a folder, so the transcript import now works over the account the chat pins, whatever its kind; the folder agents wrap their lookups in directoryTranscriptImport, unchanged. Its conversations live in a database, so the journal import reads the found database with the reader the OpenCode transcript view already uses. OpenCode refuses to reopen a session from another folder, so the workspace rule covers it too. * test(chat): Pi is now the agent with no transcript import * fix(chat): find an OpenCode 2.x terminal tab's Session History row The pane reports `opencode` while Session History lists the conversation as `opencode2`, so the pane header's and tab menu's Resume in New Native Chat found no row. Match by the chat agent that takes the conversation over. * Fix OpenCode chat adoption environment and pane eligibility * test(chat): adapt parent adoption fixtures to account identities * test: keep only the tests that pin user-visible behavior * refactor(chat): map a Session History row to its chat agent where the chat starts Both Resume in New Native Chat callers repeated the row-to-chat agent lookup. The launch now takes the row agent and maps it once, so the Session History actions file stays unchanged. |
||
|
|
eb7f427426 |
Clear agent selections in workspace creation and settings (#28543)
* Replace workspace None agent option with a clear action * Limit agent clearing to new workspace creation * Let selected default agents toggle off in settings * Keep default tab templates out of setup-only creation * Remove duplicate clock property blocking CI |
||
|
|
a0de43aa1f |
AI-button and OpenCode-model tabs start their agent in their own terminal (#28210)
* fix(agent-launch): an AI button plans its agent inside its own pane's spawn, with the host's planner and no launch record The window queues the launch on the tab; the pane's reserved spawn plans it with the planner every host launch uses and starts it at the pane's size, with its launch token. Every way the spawn ends finishes the launch, and the window pastes the prompt only once this pane says the agent started. An ordinary spawn reaches its provider without waiting on a plan. An OpenCode tab with a model keeps main's host hop untouched (TEMPORARY, until a local producer of model options exists). * fix(agent-launch): an AI button's prompt waits for its pane to start before the spawn budget runs, so a pane that settles slowly still gets its prompt; stale comments * test(agent-launch): the late pane-start test's store double is typed without an assertion * test(agent-launch): the late pane-start test pins the prompt written once and Enter once, and nothing when the pane didn't start * fix(agent-launch): an AI button's prompt goes only to the PTY its pane started, never a split's shell that bound first; the pane's start answer names that PTY * fix(agent-launch): an AI button's launch watches each split's readiness as it binds, so the launch pane's own ready output is seen even when a split bound first and it drained at once * fix(agent-launch): an AI button's pane plans with the host's current planner and runs its runtime fact's shell, so its command is quoted for the shell it is typed into, as a host launch is; the tab's inferred shell is not sent as a request |
||
|
|
9b1e83a7d8 |
refactor(native-chat): rename the chat visuals folder to the chat scratch folder (#28389)
* fix(files): never write through a dangling symlink A path that doesn't exist yet was authorized by walking up to the nearest existing folder. A dangling symlink fails realpath with ENOENT, so the walk stepped over it and approved the link's own path; the write then followed the link and created its target outside the allowed folders. The walk now continues from the link's target, and returns that real destination. * refactor(native-chat): rename the chat visuals folder to the chat scratch folder The per-chat folder agents write into is the chat's scratch space, not only a visuals folder. Its root becomes <state>/chat-scratch/<hash> (the hash is unchanged) and the agent variable becomes ORCA_CHAT_SCRATCH_DIR. The old ORCA_CHAT_VISUALS_DIR is never set and is still stripped from what every agent child inherits, Pi included. The visuals skill names the new variable and says the folder is temporary. * test(native-chat): keep the delivery test to the rename The launch parity snapshot already pins that every agent drops the pre-rename variable. * fix(files): refuse any dangling symlink in the missing-path walk Following a dangling link by hand resolved relative targets against the lexical parent, not the link's real folder: through a folder link the walk could loop forever or authorize the wrong file. Refuse instead: realpath can't say where a dangling link leads, but a write would follow it. * test(files): drop the loop case that duplicated the dangling-link refusal |
||
|
|
0504670961 |
Fix CLI delivery and fresh launch environments for managed SSH agents (#26464)
* fix: refresh host environments before structured agent command resolution * fix: ship the owning server CLI for managed remote agents * test: include the shipped CLI in remote install fixtures * Bind packaged agent CLI calls to their execution host * Keep owning-server CLI binding scoped to managed agents * Serve the last shell environment to chat starts and refresh it in the background Only the first chat start waits for the login shell; later starts get the last snapshot and refresh it in the background once it is 10 s old. On Windows hosts without the registry addon (managed server slots), the saved PATH is reread with reg.exe. The Windows server CLI launcher stager now checks the target architecture, and the CLI help says ambient selectors are ignored inside a managed server. * Keep the last good shell environment when a refresh fails, and load the Windows profile once A login-shell capture now says whether the shell produced the env or Orca fell back to its own, and only a real capture replaces the snapshot. Windows loads the PowerShell profile once per process and refreshes only the saved PATH. The reg.exe fallback queries the whole key so a missing user Path no longer drops the saved PATH. The first capture starts when the resolvers are created. * Keep the last good login-shell env for other readers while a forced refresh runs * Mark the Claude launch resolver as a reviewed router-aware module It only types the chat command resolver it injects; main's launch-routing guard (#26801) matched the name. * Keep a settled login-shell env for readers during a refresh and share the first run - Any settled cached entry, a fallback included, now serves other readers while a forced capture runs; only a real capture replaces it. - Native chat's first capture shares a run a startup reader already began, so the login shell runs once per process at startup on macOS/Linux. - Type the injected Claude resolver without naming resolveClaudeCommand. - Cover the runtime's Claude command options, the owning-host --host check, and the PTY flag drop. * Drop an inherited owning-host flag from daemon terminal panes The persistent terminal daemon merges its own environment into every pane, so an inherited ORCA_CLI_OWNING_HOST reached desktop panes even though main strips it from the spawn env. Drop it with the other per-launch CLI variables. * Route agents to their own server through ordinary launch defaults; start the first chat after an install The native-chat agent environment now sets the owning server's ORCA_USER_DATA_PATH and empty ORCA_ENVIRONMENT / ORCA_PAIRING_CODE / ORCA_REMOTE_PAIRING, which the CLI already reads as unset; empty rather than deleted, because the spawn merges the launch overlay over the server's own env. The ORCA_CLI_OWNING_HOST flag, the CLI's skip of ambient selectors, the launchers' export of it, the terminal clean-up, the help-text exception and the WSL launcher guard are gone, so a person's terminal follows their own settings as on main. A launch that finds no program in the shell snapshot waits for one shared refresh (at most one shell timeout) and resolves again; create support rereads the shell once before refusing. On Windows a failed PowerShell profile load is retried on the next refresh instead of being kept until restart. * One shared shell reread when an agent's program is missing The pre-create check for OpenCode, OMP and Pi forced its own login-shell capture on every refusal: not shared with launches, one per concurrent check, triggered by an installed-but-too-old version too, and an older capture finishing last could overwrite a newer cached environment. On Windows, every launch miss also reran a failed PowerShell profile load, bypassing the 10-second limit. - One saved shell environment per host: chat launches, their misses, the pre-create check and AI commit messages read it and share one refresh. - The pre-create check resolves the agent's program the way a launch does, so only a missing program waits for the refresh; registrations declare how to resolve their launch and check its version. - A forced login-shell capture joins one already running for the same shell, so captures cannot land out of order. - On Windows, a miss within 10 s of a failed PowerShell load rereads only the saved PATH. * One owner for the cached shell environment Review found two more races in the reread-on-miss machinery, both from the host wrapper keeping its own snapshot beside the login-shell cache: - A launch that found no program could join a reread that started before the agent was installed, and spend its one retry on the old PATH. - On Windows, saved-PATH rereads renewed the snapshot's age, so a failed PowerShell profile was never retried while launches kept missing. The login-shell cache is now the only owner of the captured environment, the run in progress and which result wins (the newest run started; a fallback never replaces a captured env). It takes three options instead of `force`: - startedAfter: a lookup that found nothing waits for a run that started no earlier than the lookup did, joining one if it has, else starting one. - refreshAfterMs: serve the last result and rerun in the background once the last attempt is older. - keepCaptured: a captured env is kept for good; only a fallback is rerun (Windows' PowerShell profile). The host wrapper keeps only policy: the 10 s interval, on Windows the saved PATH laid over the profile env on every read (a miss forces a fresh registry read), and the settings overlays. Its snapshot, pending promise, staleness and Windows "loaded" bookkeeping are gone. A lookup's start time travels with the miss (resolveAgainAfterRefreshOnMissingProgram passes it). * Read an injected shell environment once per runtime, as main does The previous commit read an injected environment on every acquisition, which broke main's 'caches shell exports but re-reads configured overrides for a resume' contract. It is read once when the resolvers are made again. * Model lists reread the environment on a missing program, as launches do Live QA: the first new chat after installing Claude showed "Claude wasn't found on the computer running this chat". The chat launch itself reread the login shell and started the new binary, but Claude's model-list check ran first, with the environment read before the install, and never retried: catalog probes were deliberately built without the miss refresh. The retry now lives in each agent's one shared lookup of its binary and environment, which a launch and a model listing both call: resolveCodexStructuredInvocation, resolveAcpLaunchInvocation, a new shared resolvePiRpcInvocation, and resolveClaudeChildEnvSources (which already had it). Each catalog probe is given the runtime's refreshAfterMissingProgram, and the catalog context no longer strips it, so a listing and a launch reread the same single-owner environment the same way. * Pass optional launch lookups through directly instead of conditional spreads * Read the host shell env through resolveLoginShellEnvironment; drop the extra wait cap CI on the merge with main failed main's new codex-workspace-relative-path test (#26684), which mocks the login-shell module with only resolveLoginShellEnvironment: the wrapper called captureLoginShellEnvironment and imported the spawn-timeout constant. The wrapper only needs the env, so it reads through resolveLoginShellEnvironment again, as on main. The miss wait no longer races its own 5 s timer: the shell run it waits for is already killed at the spawn timeout, so the second cap was a guard over the same bound. The constant goes back to private, as on main. * Test that a hung login shell holds a launch miss for at most one shell timeout The bound lives in the one owner: the shell run is killed at the spawn timeout (5 s) and settles to a fallback, which never replaces a captured snapshot. A mutation raising that timeout fails this test. * A program found through a relative PATH entry is not missing Main's #26684 runs Codex through a workspace-relative PATH entry (`bin`, `node_modules/.bin`). The lookup searches such an entry against Orca's own directory and returns the bare name; the spawn then finds the program in the chat's directory. Counting that bare name as missing made every such launch wait for a login-shell reread. The retry now fires only when the lookup truly found nothing: a bare name back from a PATH whose entries are all absolute. Each agent's one lookup tells it which PATH it searched. * Count a relative PATH entry as found only when it holds the program; Windows miss waits for a saved PATH read started after it looked A bare name with a relative PATH entry is checked from the launch's working directory, so an agent in neither entry still gets its one reread. On Windows the miss no longer joins a registry read that began before the lookup. |
||
|
|
b91795ac33 |
fix(native-chat): store a chat's permission mode on its own, so a model pick can't reset it (#28268)
* fix(native-chat): picking a Codex model or effort keeps the chat's permission mode A model or effort pick on a running chat replaced the record's options with the provider's answer, which names only the provider's own options. The saved permission mode was dropped, and the next start read it as Ask. The pick now keeps the record's permission mode beside the provider's answer; everything the provider reset still resets. * chore: re-run CI against current main * chore: re-run CI against current main * test(native-chat): keep one permission-mode-survives-pick test * refactor(native-chat): store a chat's permission mode as its own record field A model or effort pick replaces the record's options with the provider's answer, which dropped the permission mode saved among them. The mode now lives beside the options, so no provider answer can touch it. Clients still pick it with setOption and read it from the same published fields. * fix(native-chat): keep a permission mode an earlier dev build saved among options Hourly builds before the separate field saved the mode in options.permissionMode. The startup pass now moves it into the field instead of re-deriving it from the agent's arguments. Temporary; removed after 1.4.226 ships. * chore: re-run CI against current main |
||
|
|
287f4306c9 |
fix(agent-launch): refuse an agent whose runtime needs repair under its own code, in the settings page's words (#28232)
A launch whose project runtime needs repair is now refused with project_runtime_repair_required and the project settings page's sentence, instead of "Project runtime requires repair before terminal spawn: <reason>"; a retry repeats it. |
||
|
|
3d8907178e |
feat(notifications): open a server agent's notification on its server (#26518)
* test(e2e): add a paired phone client helper and phone-mirror baseline specs
Later phone-mirror slices need one oracle that drives a real mobile-scoped device with the
phone's own capabilities against a serve host or a paired desktop. Pins #26022 across a serve
restart and today's gap: a desktop's phone does not list the desktop's server workspaces.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): show SSH host health on the workspace row host badge
Rows on an SSH host the desktop cannot reach looked healthy on the phone. The badge now
adds the desktop sidebar's own word ("devbox · Disconnected"), derived by the same shared
function from the connection status ssh.listTargetSummaries already sends; old desktops keep today's badge.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(e2e): compose the phone-mirror topology and check the cold snapshot
Slices need one composer for host, desktop and phone with a single teardown, so specs stop
copying setup. The serve-restart spec now reads the first tab-stream snapshot after the cold
start, which is what the phone builds its tab strip from.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): name an unhealthy SSH host even in a single-host list
The desktop card shows SSH status on any host count, so the phone now reads target summaries
whenever a repo is on SSH and badges an unhealthy host alone too. Labels and health share one
target-to-host keying, and a hook-level test pins the reply-to-row wiring.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(e2e): keep phone-mirror teardown errors from masking test failures
Teardown failures become a test attachment instead of replacing the real failure, and the
cold-snapshot poll reports stream errors together with the PTY list.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): clear stale SSH host badges and announce them without the dot
A catalog that no longer spans hosts or holds an SSH repo now clears the health and label maps,
so a host that left cannot keep badging rows. The badge gets a screen-reader label ("devbox,
Disconnected") instead of reading the middle dot.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(runtime): let a paired desktop register its phones as delegated mobile devices
A desktop paired to this runtime can now call pairing.delegatedMobileDevice.sync to make each of
its phones an ordinary mobile-scope device it owns, so the phone mirrors that host through the
desktop. Children are never pending, inherit the parent's reach, and die with the parent's grant.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(runtime): move the mobile device revoke primitive below dispatch
Dispatch binds the caller-bound delegated-device context, so the revoke path it needs now lives in
a layer under it as a pure move; this drops the abstract method seam. No behaviour change.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(runtime): keep a runtime grant until its delegated phones are revoked
A failed child cleanup now leaves the parent in place so a retry can still find the children, and
revokeRuntimeAccess lives beside the revoke it cascades to. Adds restart, rename and bounds coverage,
shares credential minting, and drops the timing-dependent census.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(runtime): fail a delegated sync whose dropped phone cannot be revoked
Matches the parent revoke: a child whose cleanup could not be saved is kept and the sync errors
before minting anything, so the desktop retries instead of believing the server matches its list.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): relay a paired phone's server-workspace requests through the desktop
A phone mirrors its desktop, including workspaces on the desktop's configured servers. The desktop
now forwards a request that names `executionHost: runtime:<env>` to that server over one socket per
(phone, server), signed in as the phone's delegated device; replies pass through untouched except
stream ids. Hidden until the phone client ships: the relay capability is not advertised yet.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): type the single-host health fixtures inline so the test tsc passes
Fixtures hoisted into consts widened their health maps to string, which the now-required
hostHealthById rejects under the tests-typecheck ratchet; passing them inline keeps the union.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* docs(mobile): state the desktop relay contract in its entry module
main dropped docs/reference, so the relay's contract lives beside the code. Also pins that a
server whose delegated sync fails (e.g. delegated_device_revoke_failed) is unavailable until a
later request syncs it again.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): pin the relay's configured-server adapter
The production hosts adapter was only typechecked; this pins that it resolves configured servers
by exact id only (never a name or a removed server), fences by pairing revision and runtime
identity, and stops reporting retirements once unsubscribed.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): re-record the repo-metadata goldens for SSH host health
The settled metadata state now carries hostHealthById, and an SSH-only catalog now reads the
target summaries so a lone unhealthy host can be badged. Scoped re-record via rpc-recording.mts.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): tighten the desktop relay after elegance review
Reuses the shared E2EE socket opener, folds relay state into fewer fields, never sends a request
whose link ended while opening, passes failure codes through, relays only this desktop's own
phones, and drops the unused host-state reader until S2 needs one.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): bound relay frames and stop sticky relay verdicts
Restores the 8 MiB frame cap on every remote runtime socket, re-checks an update-needed server on
each new link so an in-place upgrade relays, answers a phone the server just declined without
syncing again, and ends a link whose mirrored capability update cannot be queued.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): run targeted desktop-owned calls on the desktop
The phone tags every call on a workspace screen with its execution host, so a
paired-desktop method carrying `runtime:<env>` now runs locally instead of
failing 'forbidden'. The Mac stays the only reader of the routing table.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): list the desktop's configured servers and their last workspace rows for the phone
The phone needs the same host list the desktop shows, including servers that are offline or
too old to relay to. mobileRelay.hosts.list/worktrees answer from the desktop's own status and
its own worktree.ps fetches, cached per server and fenced by pairing, never from relayed replies.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): list server rows through one desktop fetch, with no side effect on listing
The phone now always lists a server's rows through mobileRelay.hosts.worktrees, so the
desktop's own fetch is both the live source and the cache, and stamping stays in main.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): fetch a server's full workspace list within a bound, and forget removed servers
The desktop now asks for the phone's full-catalog limit, answers within 5s with the last rows
when a server hangs, drops a server's rows when it is removed or re-paired, and checks the
server's identity after the fetch so a reply from before a re-pair is never served.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): name a server workspace's host on the phone's calls
A phone reaches a server's workspace through its desktop only if each call names the host; the
direct, cloud-relay and shell-bridge transports now carry `executionHost`, streams keep it for
replay and unsubscribe, and an older shell (no `executionHost` grant) refuses rather than strips it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): list managed servers as the desktop sidebar does, and hide rows mid-delete
A server Orca manages on an SSH host now lists once under that host's name, or not at all while
the host is reached over the SSH relay, using the sidebar's own folding. Rows the server is
deleting are left out at reply time, as a phone asking the server itself would see.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): keep a disconnected server's last rows, and never join a re-paired server's fetch
Retirement also fires on a manual disconnect or managed-server update, which wrongly emptied
the phone's list; rows are now pruned only once a server is no longer configured. Fetches are
shared per server identity, so a poll after a re-pair asks the new server right away.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): list the paired desktop's server workspaces on the phone
Fixes the phone missing a desktop's `orca serve` workspaces: rows come from the desktop's
mobileRelay.hosts.* with each server's health ("Update needed", "Disconnected"), open through the
relay, follow the desktop's hidden hosts, and the desktop now advertises the relay capability.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): pin that a desktop failing the hosts list leaves only its own rows
The page asks a relaying desktop for its servers; a refusal such as method_not_found must leave
today's list with no server rows and no catalog error.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): mirror the desktop's shown hosts exactly, one host included
Jinwoo's call: the phone follows visibleWorkspaceHostIds as the desktop sidebar does, so a
one-host list narrows the phone too instead of being read as a single-host focus.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): scope a server workspace at its route, never the desktop
Every screen under a server workspace's route now reads one context and
gets that server's client and a router that keeps pushes on it, refusing
to fall back to the desktop; pins write to the row's owner like the Mac.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): never open an unreachable server workspace, and prove the relay in e2e
A server workspace the phone can't reach now gets one explicit screen at its route
and no row tap or action-sheet entry opens it; native routes read the route's host,
a census pins every workspace route, and the e2e reads files and git on the server.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): gate a server workspace on that server's own features
A targeted status.get now relays to the server, so a server workspace's route
provides that server's status to every screen under it and its action sheet,
while whether the phone can reach the server stays the desktop's answer.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(ci): run the remote-runtime WebSocket opener test on Node
Under the bun vitest project, `ws` resolves to Bun's built-in client, which
ignores maxPayload, so the frame-cap test hung. The opener only ships on
Node/Electron, so run its test in the Node runtime project like its siblings.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(push): carry a notification's workspace server to the phone
A desktop relays alerts for agents on its paired servers, but the push named
only the server's worktree, so a tap opened it on the desktop and bounced.
The gateway accepts an optional runtime execution host and forwards it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(notifications): tell the phone which server runs a notified workspace
A server agent's alert reached the phone with the server's worktree id and no
host, so tapping it opened the desktop's workspace list and bounced. The
producers now name the runtime host, and main carries it into the push.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): open a notified server workspace on its server
A push that names a runtime host now routes its tap through that server, like
any other server workspace. Absent or non-runtime hosts open as before.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): keep viewing suppression to the viewed workspace's own host
A desktop workspace and a server workspace can share an id, so viewing one
must not silence the other's alerts. Also pins a runtime-stamped tab owner.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): name the push provider payload builder for its role
The anti-slop gate rejects "shape" as a symbol name; the loop variable builds
an APNs or FCM payload, so it is named for that.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): give the file preview the workspace router, back to 128 page assets
The preview was the one workspace screen on the plain router, so the scoped router
was reached by a route set of its own and esbuild split it into an extra chunk
(129 assets); every workspace screen now uses it and it shares the route's chunk.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile-relay): derive the phone allowlist from the routing table
Adding a phone method now takes one edit (its route). A server-targeted call on a
legacy non-E2EE socket is refused instead of running locally, and only
execution-host methods enter the relay.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile-relay): drop unread freshness fields from server workspace lists
The phone reads only the rows, so stale, fetchedAt, totalCount and truncated
were dead wire weight. The desktop still serves a server's last rows while
the server is unreachable.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): read only the rows of a server's workspace list
The desktop no longer sends a freshness flag, so the phone's schema drops it.
The relay capability list names the delegated-devices constant instead of
repeating its literal, which also retires a now-circular type check.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): show a server workspace as disconnected while its server is down
The phone read the desktop's connection state for a server workspace, so a stopped
server kept a green dot and "Loading tabs" spun forever. The workspace route now
narrows that state by the server's health from mobileRelay.hosts.list, reusing the
existing disconnected session UI and refetching tabs when the server returns.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): share one desktop server-list feed between the host list and workspaces
A server workspace polled mobileRelay.hosts.list beside the host list's own read, so
the phone could ask the desktop twice per tick and the row badge and the screen could
disagree. One feed per desktop client now owns that read; both readers join it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(e2e): read a server's reachability from the hosts list, not a dropped stale flag
|
||
|
|
29aa2a4f0f |
Refresh Source Control only for the worktree whose git index changed (#26946)
* Refresh Source Control only for the worktree whose git index changed Index and HEAD-log writes under <common>/worktrees/<entry>/ now carry the checkout they belong to (resolved through the head-identity memo), and the renderer skips the status nudge when that checkout is a known sibling of the active one. Unattributable changes (shared config, bound upstream ref, overflow, SSH watches, unknown admin dirs) still refresh as before. * Pass the stable worktree-path getter as an effect dependency instead of a render-time ref * Fan out sibling head moves to every checkout's Source Control A commit to a shared branch (e.g. main) from a sibling worktree can move the active checkout's local upstream, whose ref leaf is not polled. Its logs/HEAD append now stays unattributed so ahead/behind refreshes immediately; index-only churn keeps its per-worktree attribution. --------- Co-authored-by: Neil <neil@stably.ai> |
||
|
|
bf07132ac2 |
feat(mobile): view and drive server browser pages the desktop renders (#27749)
* test(e2e): add a paired phone client helper and phone-mirror baseline specs
Later phone-mirror slices need one oracle that drives a real mobile-scoped device with the
phone's own capabilities against a serve host or a paired desktop. Pins #26022 across a serve
restart and today's gap: a desktop's phone does not list the desktop's server workspaces.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): show SSH host health on the workspace row host badge
Rows on an SSH host the desktop cannot reach looked healthy on the phone. The badge now
adds the desktop sidebar's own word ("devbox · Disconnected"), derived by the same shared
function from the connection status ssh.listTargetSummaries already sends; old desktops keep today's badge.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(e2e): compose the phone-mirror topology and check the cold snapshot
Slices need one composer for host, desktop and phone with a single teardown, so specs stop
copying setup. The serve-restart spec now reads the first tab-stream snapshot after the cold
start, which is what the phone builds its tab strip from.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): name an unhealthy SSH host even in a single-host list
The desktop card shows SSH status on any host count, so the phone now reads target summaries
whenever a repo is on SSH and badges an unhealthy host alone too. Labels and health share one
target-to-host keying, and a hook-level test pins the reply-to-row wiring.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(e2e): keep phone-mirror teardown errors from masking test failures
Teardown failures become a test attachment instead of replacing the real failure, and the
cold-snapshot poll reports stream errors together with the PTY list.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): clear stale SSH host badges and announce them without the dot
A catalog that no longer spans hosts or holds an SSH repo now clears the health and label maps,
so a host that left cannot keep badging rows. The badge gets a screen-reader label ("devbox,
Disconnected") instead of reading the middle dot.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(runtime): let a paired desktop register its phones as delegated mobile devices
A desktop paired to this runtime can now call pairing.delegatedMobileDevice.sync to make each of
its phones an ordinary mobile-scope device it owns, so the phone mirrors that host through the
desktop. Children are never pending, inherit the parent's reach, and die with the parent's grant.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(runtime): move the mobile device revoke primitive below dispatch
Dispatch binds the caller-bound delegated-device context, so the revoke path it needs now lives in
a layer under it as a pure move; this drops the abstract method seam. No behaviour change.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(runtime): keep a runtime grant until its delegated phones are revoked
A failed child cleanup now leaves the parent in place so a retry can still find the children, and
revokeRuntimeAccess lives beside the revoke it cascades to. Adds restart, rename and bounds coverage,
shares credential minting, and drops the timing-dependent census.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(runtime): fail a delegated sync whose dropped phone cannot be revoked
Matches the parent revoke: a child whose cleanup could not be saved is kept and the sync errors
before minting anything, so the desktop retries instead of believing the server matches its list.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): relay a paired phone's server-workspace requests through the desktop
A phone mirrors its desktop, including workspaces on the desktop's configured servers. The desktop
now forwards a request that names `executionHost: runtime:<env>` to that server over one socket per
(phone, server), signed in as the phone's delegated device; replies pass through untouched except
stream ids. Hidden until the phone client ships: the relay capability is not advertised yet.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): type the single-host health fixtures inline so the test tsc passes
Fixtures hoisted into consts widened their health maps to string, which the now-required
hostHealthById rejects under the tests-typecheck ratchet; passing them inline keeps the union.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* docs(mobile): state the desktop relay contract in its entry module
main dropped docs/reference, so the relay's contract lives beside the code. Also pins that a
server whose delegated sync fails (e.g. delegated_device_revoke_failed) is unavailable until a
later request syncs it again.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): pin the relay's configured-server adapter
The production hosts adapter was only typechecked; this pins that it resolves configured servers
by exact id only (never a name or a removed server), fences by pairing revision and runtime
identity, and stops reporting retirements once unsubscribed.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): re-record the repo-metadata goldens for SSH host health
The settled metadata state now carries hostHealthById, and an SSH-only catalog now reads the
target summaries so a lone unhealthy host can be badged. Scoped re-record via rpc-recording.mts.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): tighten the desktop relay after elegance review
Reuses the shared E2EE socket opener, folds relay state into fewer fields, never sends a request
whose link ended while opening, passes failure codes through, relays only this desktop's own
phones, and drops the unused host-state reader until S2 needs one.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): bound relay frames and stop sticky relay verdicts
Restores the 8 MiB frame cap on every remote runtime socket, re-checks an update-needed server on
each new link so an in-place upgrade relays, answers a phone the server just declined without
syncing again, and ends a link whose mirrored capability update cannot be queued.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): run targeted desktop-owned calls on the desktop
The phone tags every call on a workspace screen with its execution host, so a
paired-desktop method carrying `runtime:<env>` now runs locally instead of
failing 'forbidden'. The Mac stays the only reader of the routing table.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): list the desktop's configured servers and their last workspace rows for the phone
The phone needs the same host list the desktop shows, including servers that are offline or
too old to relay to. mobileRelay.hosts.list/worktrees answer from the desktop's own status and
its own worktree.ps fetches, cached per server and fenced by pairing, never from relayed replies.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): list server rows through one desktop fetch, with no side effect on listing
The phone now always lists a server's rows through mobileRelay.hosts.worktrees, so the
desktop's own fetch is both the live source and the cache, and stamping stays in main.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): fetch a server's full workspace list within a bound, and forget removed servers
The desktop now asks for the phone's full-catalog limit, answers within 5s with the last rows
when a server hangs, drops a server's rows when it is removed or re-paired, and checks the
server's identity after the fetch so a reply from before a re-pair is never served.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): name a server workspace's host on the phone's calls
A phone reaches a server's workspace through its desktop only if each call names the host; the
direct, cloud-relay and shell-bridge transports now carry `executionHost`, streams keep it for
replay and unsubscribe, and an older shell (no `executionHost` grant) refuses rather than strips it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): list managed servers as the desktop sidebar does, and hide rows mid-delete
A server Orca manages on an SSH host now lists once under that host's name, or not at all while
the host is reached over the SSH relay, using the sidebar's own folding. Rows the server is
deleting are left out at reply time, as a phone asking the server itself would see.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): keep a disconnected server's last rows, and never join a re-paired server's fetch
Retirement also fires on a manual disconnect or managed-server update, which wrongly emptied
the phone's list; rows are now pruned only once a server is no longer configured. Fetches are
shared per server identity, so a poll after a re-pair asks the new server right away.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): list the paired desktop's server workspaces on the phone
Fixes the phone missing a desktop's `orca serve` workspaces: rows come from the desktop's
mobileRelay.hosts.* with each server's health ("Update needed", "Disconnected"), open through the
relay, follow the desktop's hidden hosts, and the desktop now advertises the relay capability.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): pin that a desktop failing the hosts list leaves only its own rows
The page asks a relaying desktop for its servers; a refusal such as method_not_found must leave
today's list with no server rows and no catalog error.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): mirror the desktop's shown hosts exactly, one host included
Jinwoo's call: the phone follows visibleWorkspaceHostIds as the desktop sidebar does, so a
one-host list narrows the phone too instead of being read as a single-host focus.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): scope a server workspace at its route, never the desktop
Every screen under a server workspace's route now reads one context and
gets that server's client and a router that keeps pushes on it, refusing
to fall back to the desktop; pins write to the row's owner like the Mac.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): never open an unreachable server workspace, and prove the relay in e2e
A server workspace the phone can't reach now gets one explicit screen at its route
and no row tap or action-sheet entry opens it; native routes read the route's host,
a census pins every workspace route, and the e2e reads files and git on the server.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): gate a server workspace on that server's own features
A targeted status.get now relays to the server, so a server workspace's route
provides that server's status to every screen under it and its action sheet,
while whether the phone can reach the server stays the desktop's answer.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(ci): run the remote-runtime WebSocket opener test on Node
Under the bun vitest project, `ws` resolves to Bun's built-in client, which
ignores maxPayload, so the frame-cap test hung. The opener only ships on
Node/Electron, so run its test in the Node runtime project like its siblings.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): give the file preview the workspace router, back to 128 page assets
The preview was the one workspace screen on the plain router, so the scoped router
was reached by a route set of its own and esbuild split it into an extra chunk
(129 assets); every workspace screen now uses it and it shares the route's chunk.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile-relay): derive the phone allowlist from the routing table
Adding a phone method now takes one edit (its route). A server-targeted call on a
legacy non-E2EE socket is refused instead of running locally, and only
execution-host methods enter the relay.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile-relay): drop unread freshness fields from server workspace lists
The phone reads only the rows, so stale, fetchedAt, totalCount and truncated
were dead wire weight. The desktop still serves a server's last rows while
the server is unreachable.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): read only the rows of a server's workspace list
The desktop no longer sends a freshness flag, so the phone's schema drops it.
The relay capability list names the delegated-devices constant instead of
repeating its literal, which also retires a now-circular type check.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): show a server workspace as disconnected while its server is down
The phone read the desktop's connection state for a server workspace, so a stopped
server kept a green dot and "Loading tabs" spun forever. The workspace route now
narrows that state by the server's health from mobileRelay.hosts.list, reusing the
existing disconnected session UI and refetching tabs when the server returns.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): share one desktop server-list feed between the host list and workspaces
A server workspace polled mobileRelay.hosts.list beside the host list's own read, so
the phone could ask the desktop twice per tick and the row badge and the screen could
disagree. One feed per desktop client now owns that read; both readers join it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(e2e): read a server's reachability from the hosts list, not a dropped stale flag
|
||
|
|
04b4e962a7 |
fix: a Claude model that's gone switches to the list's default when you send (#27826)
* fix: start a new Claude chat on the listed default when its remembered model is gone The remembered new-chat model was launched as-is even after Claude stopped offering it, so every new chat failed its first message. The host create and the desktop new-chat picker now derive the same replacement from the host's saved model list: the listed default, effort moved to the nearest level it offers. Explicit models (--model, held picks) run as given; with no saved list the remembered model is kept. * fix: read the host's model list only for a Claude create that needs it The check read host.deps before ruling out other agents and explicit models, so creates whose host carries no deps (and every non-Claude create) paid for a lookup they never use. * fix: picker derives the fallback only where the host would A held model runs as picked on the host, so the picker no longer derives an effort beside it. A cached list whose default marker was dropped for this workspace keeps the placeholder instead of naming its first row. * fix: the new-chat picker picks the replacement for a gone Claude model The host-side check read the saved list separately from the picker, so the two disagreed and a cold start never fired it. The desktop new-chat picker now picks the replacement it shows, once, through the existing pick path, so the first send runs it and the remembered selection heals. The host is back to main. * fix: decide a gone Claude model's replacement at send, from the list in hand The open-time auto-pick raced the chat's create and the list's arrival, so the first send could still launch the gone model. Now, like the common pattern, the picker shows the replacement for a model the host's loaded list lacks, and the send step applies it through the existing pick call, awaited, right before the message goes out. A failed pick never holds the send back. * fix: one verified list decides a gone model's replacement The picker, the send step and the option setter now all read the list the picker shows, less the row a session adds for its own unlisted model, so what is shown is what applies and an explicit pick from that list stays. A listing that is still running, failed, or absent keeps the model. The send step re-derives from the chat's state when it runs, so a second send behind one that already moved the model applies nothing. * fix: only rows a finished host listing names can replace a gone model A single verified flag outlived the catalog it described: live options built from the built-in fallback list, or a malformed or empty host answer, still authorized a replacement the host would refuse. The state now keeps the ids a finished, well-formed host listing names, and a replacement must be both shown in the picker and named there. * fix(native-chat): the send carries the composer's Claude model and effort Every composer send (desktop and phone) carries the model and effort the picker shows, as optional agentSession.send options, only to a host that advertises agent-session.send-options.v1. The host writes a carried value that differs from the chat's own through the existing pick path before accepting the message, so the turn starts on it and the chat records it. A queued card keeps its options and applies them when it goes out; a launch's create call carries the replacement for its first message. Replaces the separate awaited pick before each send. * chore: drop an unrelated lockfile change * refactor(native-chat): one helper names a send's control fields for client and host * ci: rerun against current main * fix(native-chat): a message runs on the model and effort it was sent with The carried selection is now stored with the message (its submission row, and a queued card's row) and applied through the shared option writer as the delivery loop hands the message over, or before the start it needs. A pick made after the message was accepted no longer changes the model it runs on. Direct sends, queue drains and Send now share that one apply point; the early apply before acceptance and the queue-only apply are gone. The picker and the send read one effective selection: the replacement for a model the host's finished listing lacks shows only for a host that takes a send's selection, the phone validates picks against what it shows, a send carries the default the composer shows, a resumed launch's first message gets the same resolution, and only the listing proves a row (no label heuristic). * docs(native-chat): a submission's options are the host's alone * fix(native-chat): an opening message keeps its model; a pending pick resolves like the picker The opening message a create sends now stores the model and effort the chat was created with, so a pick made before its handover no longer changes what it runs on, and a card kept after a restart keeps them. A pick on its way to the host resolves through the same option view the picker shows before a send carries it, so a gone model it names is replaced too; the send selection reads only the shown snapshot. The option writer builds its own record-and-publish context, so the message path no longer duplicates it, and the client and host share one function for a send's fingerprint fields. * ci: rerun against current main |
||
|
|
b9bd8dbe4a |
fix(agent-status): retire any restored agent row whose terminal died while Orca was closed (STA-10194) (#28475)
* wip * refactor(agent-status): tighten the restored-status sweep after review - reword preserveResumeIdentity to the real rule: the pane survives to resume into - one isLocalHookConnectionId predicate for the sweep, removed-worktree cleanup, and runtime PTY sources - the startup wrapper is file-private with a distinct name; probe catch simplified - stale 'liveness reap' wording in the Claude roster comment and test - tests: the Claude roster case is asserted through a later turn settling, and a hydrated resume remnant is pinned as never a candidate |
||
|
|
03eee256f1 |
fix(wsl-host): let an app update reach a WSL host orcad (E1) (#28458)
* fix(wsl-host): read the running orcad's census before an app update replaces it * fix(wsl-host): keep Force, the incumbent watch and its freshness through an update * fix(orcad): recount terminals just before an update stops the incumbent * refactor(wsl-host): move watch arming and pairing identity out of oversized modules |
||
|
|
5f134d8f01 |
feat(mobile): show and use the desktop's server workspaces on the phone (#26501)
* test(e2e): add a paired phone client helper and phone-mirror baseline specs
Later phone-mirror slices need one oracle that drives a real mobile-scoped device with the
phone's own capabilities against a serve host or a paired desktop. Pins #26022 across a serve
restart and today's gap: a desktop's phone does not list the desktop's server workspaces.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): show SSH host health on the workspace row host badge
Rows on an SSH host the desktop cannot reach looked healthy on the phone. The badge now
adds the desktop sidebar's own word ("devbox · Disconnected"), derived by the same shared
function from the connection status ssh.listTargetSummaries already sends; old desktops keep today's badge.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(e2e): compose the phone-mirror topology and check the cold snapshot
Slices need one composer for host, desktop and phone with a single teardown, so specs stop
copying setup. The serve-restart spec now reads the first tab-stream snapshot after the cold
start, which is what the phone builds its tab strip from.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): name an unhealthy SSH host even in a single-host list
The desktop card shows SSH status on any host count, so the phone now reads target summaries
whenever a repo is on SSH and badges an unhealthy host alone too. Labels and health share one
target-to-host keying, and a hook-level test pins the reply-to-row wiring.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(e2e): keep phone-mirror teardown errors from masking test failures
Teardown failures become a test attachment instead of replacing the real failure, and the
cold-snapshot poll reports stream errors together with the PTY list.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): clear stale SSH host badges and announce them without the dot
A catalog that no longer spans hosts or holds an SSH repo now clears the health and label maps,
so a host that left cannot keep badging rows. The badge gets a screen-reader label ("devbox,
Disconnected") instead of reading the middle dot.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(runtime): let a paired desktop register its phones as delegated mobile devices
A desktop paired to this runtime can now call pairing.delegatedMobileDevice.sync to make each of
its phones an ordinary mobile-scope device it owns, so the phone mirrors that host through the
desktop. Children are never pending, inherit the parent's reach, and die with the parent's grant.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(runtime): move the mobile device revoke primitive below dispatch
Dispatch binds the caller-bound delegated-device context, so the revoke path it needs now lives in
a layer under it as a pure move; this drops the abstract method seam. No behaviour change.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(runtime): keep a runtime grant until its delegated phones are revoked
A failed child cleanup now leaves the parent in place so a retry can still find the children, and
revokeRuntimeAccess lives beside the revoke it cascades to. Adds restart, rename and bounds coverage,
shares credential minting, and drops the timing-dependent census.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(runtime): fail a delegated sync whose dropped phone cannot be revoked
Matches the parent revoke: a child whose cleanup could not be saved is kept and the sync errors
before minting anything, so the desktop retries instead of believing the server matches its list.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): relay a paired phone's server-workspace requests through the desktop
A phone mirrors its desktop, including workspaces on the desktop's configured servers. The desktop
now forwards a request that names `executionHost: runtime:<env>` to that server over one socket per
(phone, server), signed in as the phone's delegated device; replies pass through untouched except
stream ids. Hidden until the phone client ships: the relay capability is not advertised yet.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): type the single-host health fixtures inline so the test tsc passes
Fixtures hoisted into consts widened their health maps to string, which the now-required
hostHealthById rejects under the tests-typecheck ratchet; passing them inline keeps the union.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* docs(mobile): state the desktop relay contract in its entry module
main dropped docs/reference, so the relay's contract lives beside the code. Also pins that a
server whose delegated sync fails (e.g. delegated_device_revoke_failed) is unavailable until a
later request syncs it again.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): pin the relay's configured-server adapter
The production hosts adapter was only typechecked; this pins that it resolves configured servers
by exact id only (never a name or a removed server), fences by pairing revision and runtime
identity, and stops reporting retirements once unsubscribed.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): re-record the repo-metadata goldens for SSH host health
The settled metadata state now carries hostHealthById, and an SSH-only catalog now reads the
target summaries so a lone unhealthy host can be badged. Scoped re-record via rpc-recording.mts.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): tighten the desktop relay after elegance review
Reuses the shared E2EE socket opener, folds relay state into fewer fields, never sends a request
whose link ended while opening, passes failure codes through, relays only this desktop's own
phones, and drops the unused host-state reader until S2 needs one.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): bound relay frames and stop sticky relay verdicts
Restores the 8 MiB frame cap on every remote runtime socket, re-checks an update-needed server on
each new link so an in-place upgrade relays, answers a phone the server just declined without
syncing again, and ends a link whose mirrored capability update cannot be queued.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): run targeted desktop-owned calls on the desktop
The phone tags every call on a workspace screen with its execution host, so a
paired-desktop method carrying `runtime:<env>` now runs locally instead of
failing 'forbidden'. The Mac stays the only reader of the routing table.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): list the desktop's configured servers and their last workspace rows for the phone
The phone needs the same host list the desktop shows, including servers that are offline or
too old to relay to. mobileRelay.hosts.list/worktrees answer from the desktop's own status and
its own worktree.ps fetches, cached per server and fenced by pairing, never from relayed replies.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): list server rows through one desktop fetch, with no side effect on listing
The phone now always lists a server's rows through mobileRelay.hosts.worktrees, so the
desktop's own fetch is both the live source and the cache, and stamping stays in main.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): fetch a server's full workspace list within a bound, and forget removed servers
The desktop now asks for the phone's full-catalog limit, answers within 5s with the last rows
when a server hangs, drops a server's rows when it is removed or re-paired, and checks the
server's identity after the fetch so a reply from before a re-pair is never served.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): name a server workspace's host on the phone's calls
A phone reaches a server's workspace through its desktop only if each call names the host; the
direct, cloud-relay and shell-bridge transports now carry `executionHost`, streams keep it for
replay and unsubscribe, and an older shell (no `executionHost` grant) refuses rather than strips it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): list managed servers as the desktop sidebar does, and hide rows mid-delete
A server Orca manages on an SSH host now lists once under that host's name, or not at all while
the host is reached over the SSH relay, using the sidebar's own folding. Rows the server is
deleting are left out at reply time, as a phone asking the server itself would see.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): keep a disconnected server's last rows, and never join a re-paired server's fetch
Retirement also fires on a manual disconnect or managed-server update, which wrongly emptied
the phone's list; rows are now pruned only once a server is no longer configured. Fetches are
shared per server identity, so a poll after a re-pair asks the new server right away.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): list the paired desktop's server workspaces on the phone
Fixes the phone missing a desktop's `orca serve` workspaces: rows come from the desktop's
mobileRelay.hosts.* with each server's health ("Update needed", "Disconnected"), open through the
relay, follow the desktop's hidden hosts, and the desktop now advertises the relay capability.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): pin that a desktop failing the hosts list leaves only its own rows
The page asks a relaying desktop for its servers; a refusal such as method_not_found must leave
today's list with no server rows and no catalog error.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): mirror the desktop's shown hosts exactly, one host included
Jinwoo's call: the phone follows visibleWorkspaceHostIds as the desktop sidebar does, so a
one-host list narrows the phone too instead of being read as a single-host focus.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): scope a server workspace at its route, never the desktop
Every screen under a server workspace's route now reads one context and
gets that server's client and a router that keeps pushes on it, refusing
to fall back to the desktop; pins write to the row's owner like the Mac.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): never open an unreachable server workspace, and prove the relay in e2e
A server workspace the phone can't reach now gets one explicit screen at its route
and no row tap or action-sheet entry opens it; native routes read the route's host,
a census pins every workspace route, and the e2e reads files and git on the server.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): gate a server workspace on that server's own features
A targeted status.get now relays to the server, so a server workspace's route
provides that server's status to every screen under it and its action sheet,
while whether the phone can reach the server stays the desktop's answer.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(ci): run the remote-runtime WebSocket opener test on Node
Under the bun vitest project, `ws` resolves to Bun's built-in client, which
ignores maxPayload, so the frame-cap test hung. The opener only ships on
Node/Electron, so run its test in the Node runtime project like its siblings.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): give the file preview the workspace router, back to 128 page assets
The preview was the one workspace screen on the plain router, so the scoped router
was reached by a route set of its own and esbuild split it into an extra chunk
(129 assets); every workspace screen now uses it and it shares the route's chunk.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile-relay): derive the phone allowlist from the routing table
Adding a phone method now takes one edit (its route). A server-targeted call on a
legacy non-E2EE socket is refused instead of running locally, and only
execution-host methods enter the relay.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile-relay): drop unread freshness fields from server workspace lists
The phone reads only the rows, so stale, fetchedAt, totalCount and truncated
were dead wire weight. The desktop still serves a server's last rows while
the server is unreachable.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): read only the rows of a server's workspace list
The desktop no longer sends a freshness flag, so the phone's schema drops it.
The relay capability list names the delegated-devices constant instead of
repeating its literal, which also retires a now-circular type check.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): show a server workspace as disconnected while its server is down
The phone read the desktop's connection state for a server workspace, so a stopped
server kept a green dot and "Loading tabs" spun forever. The workspace route now
narrows that state by the server's health from mobileRelay.hosts.list, reusing the
existing disconnected session UI and refetching tabs when the server returns.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): share one desktop server-list feed between the host list and workspaces
A server workspace polled mobileRelay.hosts.list beside the host list's own read, so
the phone could ask the desktop twice per tick and the row badge and the screen could
disagree. One feed per desktop client now owns that read; both readers join it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(e2e): read a server's reachability from the hosts list, not a dropped stale flag
|
||
|
|
701e9f2bdf |
feat(new-workspace): add "None" agent option that opens only the setup script (#25617)
* feat(new-workspace): add "None" agent option that opens only the setup script New workspaces previously required choosing an agent or a blank terminal, so a setup script that launches its own agent always ran next to an extra shell. The quick-create agent picker and Settings > Agents now offer "None (setup script only)": when setup runs, the workspace opens with just the Setup tab focused; without a setup script it falls back to a blank terminal. The default is stored as defaultTuiAgent 'blank' plus a new optional newWorkspaceSetupOnly flag, so clients and servers that only know 'blank' degrade to a blank terminal instead of rejecting an unknown value. Closes stablyai/orca#25247 * fix(new-workspace): honor the "None" default for CLI-created workspaces "None (setup script only)" only took effect in the new-workspace dialog. A workspace created with `orca worktree create` still opened a blank terminal first and left it selected, because the main process builds those tabs and never read the setting. The main-process tab provisioning now skips the blank terminal when the default is "None" and a setup script runs in its own tab, so Setup is the only tab. The same rule applies to CLI creates that activate the workspace, and to dialog creates the user navigated away from before they finished. A split setup or repo default tabs still get their shell. The setup-only check moved to src/shared so main and renderer share it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(new-workspace): register the CLI activation caller and stay under the line limit The CLI activation path now passes providesInitialSurface for the "None" default, so it has to appear in the test that lists every caller using that option; the full test run failed without it. Merging main pushed worktree-creation-flow-execute.ts to 301 lines against the 300-line limit. Naming the background-create condition brings it back to 300 with no behavior change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(new-workspace): open a blank terminal when the setup-only Setup tab fails to spawn The "None" default skips the blank shell because the Setup tab will be the only surface. If that spawn fails, the workspace was left with no terminal. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * style: format the setup-only fallback terminal call Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(workspace): save None through host settings and honor per-create choices * fix(settings): clear None when legacy clients choose a blank terminal * fix(settings): clarify None with split or skipped setup --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Neil <neil@stably.ai> |
||
|
|
31e5003572 |
fix(accounts): launches a WSL host starts itself use the account selected for its distro (#28464)
* fix(accounts): launches a WSL host starts itself use the selected account The desktop now pushes the account env it selected for a WSL host's distro to that host's orcad (accounts.setHostLaunchEnv, capability-gated). orcad lays it over every terminal create (CLI, orchestration, automations, bare commands and presets), structured chats and AI commit text, as the desktop's own launches into the distro do. `orca account` on the host now points at the desktop that owns the accounts. * fix(accounts): cover splits, child agents and restarts; skip SSH workspaces Review follow-ups: terminal splits and structured chats' child agents take the host account env too; an SSH workspace's terminals never get distro paths; orcad saves the pushed env so a launch before the desktop reconnects keeps the account; a failed push retries unless a newer one supersedes it. * fix(accounts): retry a host account push when the host's capabilities are unknown * test(orcad): the startup mock restores host account env * refactor(accounts): serialize host account pushes with the shared keyed queue |
||
|
|
b525a3ba91 |
refactor(wsl): reuse the WSL-host predicate, error helper and a shared owner lookup (#28447)
* refactor(wsl): reuse the WSL-host predicate, error-message helper and a shared owner lookup - isWslHostRuntimeEnvironment becomes a type guard and replaces inline orcadDeployment?.kind === 'wsl' checks in main and renderer. - WSL modules use shared/error-message instead of two local copies and three inlined ones. - The workspace row-owner lookup moves out of the IPC bridge into lib/, since the terminal pane and host removal import it too. - The per-terminal WSL unavailable selector returns early when no WSL host exists. * test(wsl): the unavailable selector is quiet when no WSL host exists |
||
|
|
f7235f6e2e |
feat(wsl-mover): accept local WSL projects on the migration wire, only in their own distro (D1b-2) (#28466)
* wip(wsl-mover): local-wsl manifest source on the wire (D1b-2) * wip(wsl-mover): narrow SSH-only steps, cover WSL source parsing * feat(wsl-mover): WSL host orcad accepts its own distro's projects and advertises it (D1b-2) * fix(wsl-mover): bind a WSL source to its distro registration * fix(wsl-mover): keep the manifest parser under the line limit |
||
|
|
4f5ffcb429 |
feat(wsl-mover): decide every piece of project state and add a losslessness check (D1b-1) (#28459)
* feat(wsl-mover): decide every piece of project state and add a losslessness projection (D1b-1) * fix(wsl-mover): replace casts with checked reads * fix(wsl-mover): tighten the projection after review * fix(wsl-mover): second review round on the projection * refactor(wsl-mover): keep the disposition tables in shared, out of the layout-writer scan |
||
|
|
7a5495493b |
fix: distinguish unreadable cookies from related import skips (#28269)
Count unreadable cookies separately from related cookies withheld by existing import safeguards. Keep import and clearing decisions unchanged and retain the legacy warning for older or invalid summaries. Co-authored-by: oliver-mee <102673257+oliver-mee@users.noreply.github.com> |
||
|
|
624a095993 |
fix(cli): warn when a workspace status has no board column (#28264)
Return optional warnings from the owning host when a requested workspace status does not match its configured columns. Preserve the requested write and older-peer behavior; show warnings separately from normal CLI output. Co-authored-by: Kalvin Mizzi <kalvin@mizzi.com> |
||
|
|
0d1e367d05 |
fix(automations): format the jobs-file limit consistently (#28220)
fix(automations): format the jobs-file limit consistently Use English number formatting in the English jobs-file limit error so non-English host locales still report 10,000 jobs. Co-authored-by: Matheus Breguêz <matbrgz@gmail.com> |
||
|
|
4d65146f2d |
fix(native-chat): Stop is saved before it acts, and a late or repeated Stop does nothing (#27076)
* feat(native-chat): add a chat-owned command receipt store
Adds the agent_session_command_receipts table and an insert-if-absent
receipt write that runs inside a command's own journal transaction. A
receipt records the command's identity, canonical hash, status and a
pointer to the row holding its result; it dies with its chat record.
Nothing uses it yet: the existing operation ledger stays authoritative.
* fix(native-chat): key command receipts like today's ledger ids
Rows are keyed by caller and operation id; a send looks across callers and
other methods look up only their caller's row, matching the existing ledger.
Bad rows and newer table shapes read as unreadable instead of throwing.
* fix(native-chat): order receipt lookups by insertion and key by operation id
Global lookups now take rows in insertion order, so a read and an insert
agree on which receipt answers an id regardless of the host clock. The
primary key leads with the operation id, so its own index serves both
lookups.
* fix(native-chat): accept sends through their own receipt
Send (direct, queued, steer) and /compact no longer commit a separate
ledger reservation before the message is saved. Their command receipt is
inserted-if-absent in the same transaction as the submission or queued
draft; a repeated id replays from it, a changed request conflicts.
Operation ids are checked for syntax only, so age, clock skew, pruning
and the reservation's own storage can no longer refuse a send.
The ledger success row they already co-committed stays as a temporary
insert-if-absent write so families not yet switched keep seeing these
ids; it is never read on this path.
* fix(native-chat): answer a send's receipt one way, share /compact's id space with Send, wake delivery once
- After its commit, a send whose publication fails answers through the same receipt replay a retry takes, so one id gets one answer; the in-memory copy and journal reload are gone.
- /compact ids are global like Send's: a retry under a new connection replays instead of needing a second acknowledgment write.
- The receipt route calls its runner directly; the runner wakes delivery when a new message row commits.
- The compatibility ledger row expires from host time; each temporary site is labelled.
- Receipt read errors and post-commit failures are logged.
* test(native-chat): expect receipt answers where tests assumed the send ledger
Restart-continuation tests now model the case Send cannot vouch for (saved but
not loaded into memory) and keep it unconfirmed; a later throw after the message
is in the chat answers continued, as a resend would. Drop /compact's unreachable
legacy replay branch and fix stale comments.
* test: run the queued tests that open the journal database in the Node runtime project
* fix(native-chat): queued-message actions and prompt answers accept through their own receipt
Send Now, Delete, Resume and prompt answers move onto the command-receipt route
Send uses: the receipt is inserted with the command's own write. A no-op answers
only once its receipt commits (otherwise outcome unknown), and a prompt answer's
replay reads the exact revision it was accepted at.
* fix(native-chat): replay a queued action from what its receipt recorded
Send Now's acknowledgement points at the submission it handed off; a question
answer's receipt keeps the accepted resolution; a no-op that cannot save its
receipt answers outcome unknown and holds nothing in memory; classified refusals
pass through.
* docs(native-chat): receipt comments say what a replay answers from
* fix(native-chat): Stop saves first, captures its target, and holds queued messages
Stop accepts through its own receipt: one transaction saves the Stop record
(target turn, agent generation, cutoff), returns unsent messages to the queue as
paused cards, and writes the receipt; only then does it interrupt the captured
target. A failed save leaves the agent running and says so. A late Stop is a
recorded no-op, and a retry is answered from the receipt.
* fix(native-chat): Stop round 1: late Stop is a no-op, saved-then-failed refuses, hold only what the Stop did not stop
- A Stop naming a turn that already ended is an accepted no-op.
- A card's Cancel saves its receipt with the dismissal row.
- A Stop saved but not carried out is refused as stopFailed ("Couldn't stop <agent>. Try again.") and its receipt is rewritten as rejected.
- A Stop during a start is saved first, then aborts that start; the abort-before-admission path is removed.
- The message the Stop stops is withdrawn as before; messages behind it are held as paused cards. Non-text messages are still withdrawn (TEMPORARY).
- Cleanups: one receipt commit path, dead fields and helpers removed, the returned-to-queue text names the agent.
* fix(native-chat): Stop round 2: narrow to receipts and save-first; message fate as on main
- Stop's withdrawal of queued sends is main's own rows (rejected, cancelled), written in the same
transaction as the Stop event and its receipt; a failed save withdraws nothing.
- A Stop arriving during a start aborts that wait on arrival, as on main (admits-now guard), then
captures its target once in the session lane. The out-of-lane admission is removed.
- A committed receipt is never rewritten: a failure after the save answers from the receipt and
writes the existing 'Cancellation was not confirmed.' note. 'Couldn't stop <agent>' only when
the save fails; a card Cancel that can't be saved says the question or approval wasn't cancelled.
- A Stop naming a turn a rewind removed is late (an accepted no-op).
- Removes the held-card settlement and returnedToQueue: what happens to a stopped message stays as
on main.
* test(native-chat): name the Stop row comparison for what it drops
* fix(native-chat): Stop round 3: a committed Stop always acts; notes only for unconfirmed interrupts
- A Stop whose write throws after its receipt committed still interrupts (never 'Couldn't stop').
- An interrupt that worked answers stopped even when only its note fails to write.
- A strict card interrupt that throws after its receipt committed gets the 'Cancellation was not
confirmed.' note, through one shared helper with the turn path.
- A target-less Stop stands while the main agent works, so a turn that opened while its save waited
(a send made before the Stop) is interrupted, as on main.
- A card Cancel that can't be saved says 'Couldn't cancel this question or approval. Try again.'
* fix(native-chat): Stop round 4: name the turn the Stop actually stops
- The Stop event's turn is resolved when its write runs (the named turn, else the live turn), so a
turn that opened while the save waited is the one marked interrupted and shown stopping.
- The unconfirmed note is keyed by that turn; tests cover it and its own write failing.
- Whether a Stop withdrew anything comes from its capture, so a throw after the commit can't flip it.
- A Stop counts as saved anyway only when its own write committed the receipt.
* fix(native-chat): a new chat's first send gets the Stop-aware operation receipt
Main's create path (from #26332) hands performSend the founding receipt; the
Stop changes widened the turn context's receipt to track its commit. Wrap it
with the existing observedReceipt.
* fix(native-chat): a background task the agent won't stop still says not done
A background-task Stop now saves its receipt before it asks the agent. A
refusal after that save (task still running, or a timeout / lost connection)
was being replaced by the receipt's replay, so the person never heard the Stop
failed. That refusal is now the first answer, as on main; the receipt stays
saved and a retry of the same id is answered from it without asking again.
* test(acp): a Grok Stop naming an ended turn leaves the unechoed follow-up alone
The late-Stop rule makes a Stop naming a turn that has ended an accepted no-op
for every agent. The Claude case was rewritten with it; this is Grok's.
* test(native-chat): Stop tests exercise what they name after the late-Stop rule
- The "leaves the child alone" Stop now reaches the provider (handed over, a
turn with no row yet); the at-rest refusal case was a duplicate of the late
no-op test and is removed.
- The adapter comment and two test names say the gap rule covers a named turn
with no journal row yet.
- The Grok late-Stop test checks no status row and that the follow-up completes.
- Name the two refusals refusalAnswers passes through.
* fix(native-chat): a background-task Stop answers its acceptance and notes what happened
A background-task Stop is saved before it asks the agent. The answer is
that saved acceptance: a stopped task answers cancelled:true as before
(the phone keeps the row "stopping" on it), and anything else answers
cancelled:false. A retry answers from the receipt without asking again.
What the agent then says comes back as a note keyed by the Stop's own id,
written through the existing Stop-note path, and never as a refusal:
- still running: "The background task wasn't stopped." (or "The
background tasks weren't stopped." for Stop all), the new status-row
failure kind backgroundTaskNotStopped;
- timed out or lost the agent: "Cancellation was not confirmed."
The note is the thread's, since tasks outlive their turn. A note that
cannot be written is logged and changes no answer.
The background-task body moves out of performCancel into the targeted
cancel, and the refusalAnswers flag and its receipt are deleted.
* test(runtime): the Codex strip's Stop notes a command it could not stop
A background-task Stop now answers its saved acceptance and says what happened in a note: "The background task wasn't stopped." when the command still runs, "Cancellation was not confirmed." when Codex was lost.
* test(native-chat): keep the Stop tests that pin behaviour, cut the rest
Merges the receipt, save-first, failed-after-save and background-task Stop
tests into one flow file; drops unit and per-branch tests already covered
by a flow test.
|
||
|
|
b70f13ff89 |
fix(orchestration): stop retried failed worker starts from piling up live agents (#27795)
* fix(orchestration): stop retried failed worker starts from piling up live agents A failed worker-start keeps the terminal it created (a pane the user typed into is theirs), so a coordinator retrying a start that can never succeed left one more live agent behind on every retry, with nothing to stop it. - A --retry-of start now releases the failed attempt's still-owned terminal through the normal worker-release path (output archived, user takeover kept, recovery finishes it after a restart). - A failed start now counts toward the Task's existing three-failure circuit breaker, carried across retries; a retry of a circuit-broken Dispatch is refused with a plain error. Linear: STA-7113 * test(orchestration): type the failed-start retry fixtures without casts * fix(orchestration): carry failed-start count only along --retry-of; plain breaker refusal - Only a --retry-of start inherits its predecessor's failure count, so worker exits across separate starts keep the old behaviour. - The circuit-broken refusal carries failureCount and next steps that say to stop retrying, read the last attempt, and that its pane stays open. - A repeated failed report on a circuit-broken Dispatch is a duplicate. Linear: STA-7113 * fix(orchestration): breaker refusal only points at a terminal that is still open Linear: STA-7113 * fix(orchestration): log a retry's release that could not prove the old terminal closed Linear: STA-7113 * Preserve worker diagnostics before terminal release and mask wrapped tokens |
||
|
|
26a265d704 |
Host plans agent launches with the window's runtime rule and one runtime fact (C1) (#27692)
* feat(agent-launch): host plans agent launches with the window's runtime rule and one runtime fact
Host-started agent launches now pick their platform with the same rule the
window uses (project runtime, else a \\wsl$ path, else the host), and resolve
one runtime fact that supplies both the command quoting and the PTY shell.
Adds the agent.launch.terminal-inputs.v1 capability and its optional
title/extraAgentArgs/requestKind/terminalOnly fields; no sender yet.
OpenCode's createAgentSession stays on today's rules until C2 (temporary).
* fix(agent-launch): one shared runtime rule, a derived fact, and only quoted commands carry it
Round-1 review fixes for C1:
- Window and host read a workspace's runtime through one shared resolver
(local-workspace-execution-runtime.ts); a repo-less \\wsl$ folder gets no
runtime, as in the window.
- The fact is { platform, remote, pty } with the spawn's own PTY type; the
quoting shell is derived from it, never stored.
- createAgentSession uses the same fact (its pre-C1 exception is deleted).
- Worktree startups quote from a fact for the repo and pass it on; a runtime
that needs repair fails the startup, not the create. `shell` is required
when building a launch command.
- The host planner stamps a fact only on commands quoted with it; a
caller-quoted command keeps the spawn's own rule.
- Qoder probes where the process runs (\\wsl$ cwd, then a WSL shell).
- wslShellRule 'host-legacy' keeps today's WSL-shell outcome where no
project chose a runtime (temporary, removed by C8).
- terminalOnly reaches the early-tab decision with an accurate receipt
sentence; the unused client gate is removed.
- Adds one host C0 row for a \\wsl$ repo with no saved runtime.
* fix(agent-launch): round 2: keep request fields for C2, extend main's resolver, one answer per workspace
Problem: the first round still carried request fields and window-parity
switches that only the next step sends, added a new shared resolver that
also changed window code, and left some background launches using a
different runtime answer than the one they spawn with.
- Move the agent.launch request fields, their capability and fingerprint
handling, and the prompt/Codex parity switches out to the next step;
today's host behaviour is kept.
- Drop the shared resolver: main's resolveLocalWorkspaceRuntime now finds
the project by repo id or source repos, folders borrow their single
local repo, and a \\wsl$ path is not refused on a stale distro list.
Window code is back to main.
- Agents started in an existing worktree use that worktree's answer; the
phone's fallback paths spawn with the answer they quoted for, and a
runtime that needs repair shows its message instead of a plain shell.
- The OpenCode model check looks where the agent will run.
- A Windows --shell in a \\wsl$ workspace names the path, not a runtime
setting that cannot change it.
- The temporary folder rule names its follow-up, STA-10231.
* fix(agent-launch): OpenCode model picks in WSL folders keep today's Windows runtime
Problem: once a folder follows its repo's WSL runtime, an OpenCode launch
that picks a model would be checked and started inside WSL, where Orca
cannot yet verify OpenCode models, so picks that work today could be
refused.
Where main had no runtime for the scope (only folders), a model-picked
OpenCode launch keeps main's answer. Start, resume and the worker's model
check read the same answer. Temporary until OpenCode models are verified
inside WSL.
* fix(agent-launch): round 3: OpenCode keyed on the agent, new worktrees quoted for where they land
Problem: round 2's OpenCode exception keyed on a model pick, so a resumed
chat could run somewhere other than where it started, and the worker's
model check read WSL from the Terminal shell and refused folders that work
today. A new worktree's startup used the repo's answer even when the
worktree lands in a WSL share.
- In a folder whose repo runs in WSL, every OpenCode launch keeps main's
Windows answer: create, resume and the worker's model check agree. The
model check reads WSL from the path alone, as main did. STA-10251.
- A new worktree's startup is planned for its WSL-share base folder when
one is set (newWorktreeLaunchScope).
- A \\wsl$ working directory runs in WSL whatever the project says, so
non-OpenCode agents there are written for Linux.
- The planner test derives the answer instead of passing it in.
- Smaller shapes for the PTY settings type, the cache-less resolve,
executionRuntimePlatform, spawnShellInputs and two comments.
* fix(agent-launch): round 3c: one labelled rule for where the path decides; OpenCode follows the WSL-share fix
Problem: OpenCode in a repo on a WSL share whose project is saved as
Windows host still got PowerShell quoting typed into WSL, because the
WSL-share fix skipped OpenCode. The fix and the two temporary parity
rules were also split across two returns, so deleting the temporary
ones could take the permanent fix with them.
- One expression decides when the path places the agent, each term
labelled: WSL share + Windows host (permanent fix), folder + Windows
host (STA-10231), OpenCode in a folder (STA-10251).
- OpenCode now gets the WSL-share fix too; a model pick there is refused,
as main's model check already did.
- The OpenCode entry-point tests set a WSL Terminal shell, so the model
check test fails if it reads WSL from that shell again.
- Tests trimmed with no coverage loss; the spawn-precedence unit test is
dropped (the C0 WSL-share row covers the reachable case).
* fix(agent-launch): keep the window's WSL answer for panes the window spawns
Live Windows QA (Terminal shell wsl.exe, global default Windows host, repo
with no project setting) saw an agent command written with PowerShell
quoting typed into a WSL pane, so codex lost the apostrophes in its prompt.
The host wrote the command for its own answer (Windows, PowerShell), but on
routes where the window spawns the pane (the phone while a window is open,
a focused desktop chat create, `orca terminal create --command` in a repo)
the window spawned it by the Terminal shell, which is WSL. Quoting and the
pane came from two different answers.
Those routes ran in WSL with Linux-style quoting on main and worked, so the
temporary host-legacy term (STA-10231) now also covers them: when the window
spawns the pane, the path and Terminal shell decide, as on main. Background
routes keep the approved change (PowerShell instead of cmd.exe with
Linux-style quoting, which was broken on main). createTerminal and the
pre-launch planning share one `windowSpawnsTerminal` rule so they can't
disagree about who spawns the pane.
* Revert "fix(agent-launch): keep the window's WSL answer for panes the window spawns"
This reverts commit
|
||
|
|
8a0f1ee4b6 |
Resume in New Native Chat for Grok and OMP terminal conversations (#28049)
* feat(chat): Resume in New Native Chat for Grok and OMP terminal conversations A Grok or OMP conversation started in the terminal now offers "Resume in New Native Chat" in its Session History row menu and its tab's right-click menu, like Claude and Codex: a new chat of that agent takes the conversation over, showing its history and continuing it. The host finds the conversation through the agent's transcript import and fills the chat from it, the same adoption path Claude and Codex use; the adopted handle is now any agent's, not only theirs. Both agents keep conversations per folder, so the action shows only in the workspace the conversation was recorded in, as for Claude. * fix(chat): hide Grok 1.0's runtime context and rules in a chat that takes over its conversation Grok 1.0 appends the user's rules to its runtime-context row; the decoder only knew the older shape, so a Grok chat taking over a terminal conversation showed that row as the first message. * Fix OMP and Grok transcript lookup for chat adoption * Fix scoped Grok and OMP chat history lookup |
||
|
|
975c27b780 |
fix(usage): cap Cursor's Retry-After at 24h and honor its HTTP-date form (#28406)
Cursor parsed Retry-After with a bare Number(), so a huge header could block usage refreshes for years and an HTTP-date was ignored. Claude and Cursor now both call parseUsageRetryAfterMs, which applies the 24h usage cap to the shared Retry-After parser, replacing Claude's private copy of the same algorithm. The shared parser is renamed from relay-retry-after-header/parseRelayRetryAfterMs to retry-after-header/parseRetryAfterHeaderMs: relay, the updater, mobile and usage all use it. |
||
|
|
24bda717f4 |
fix(terminal): a slow window no longer makes orca terminal create fail and then duplicate (#28158)
* fix(terminal): start CLI-created workspace terminals in main so a slow window cannot duplicate them A focused or interactive-agent `orca terminal create` asked the window to make the terminal and reported failure after 10 s, while the window still made it later; a retry then made a second one. Main now starts every terminal that names a workspace and shows it in the window best-effort, as it already did for every other create. * fix(terminal): start a main-created terminal at its worktree's measured pane size An unfocused CLI agent opens as an inactive tab that is not resized until the user opens it, so starting it at 120x40 left its output wrapped to 120 columns. Start it at the size of the pane active in the worktree's active group, where the new tab opens; fall back to 120x40 only when that worktree has no measured pane. * fix(cli): keep sending rendererBacked for hosts before v1.4.211 Hosts without #22384 leave a background-started agent's startup queries unanswered, so a new CLI talking to an old, still-running host must keep routing interactive agents to the window. Newer hosts ignore the flag. * fix(terminal): start host-created terminals at the measured size of their tab group The window publishes, on its graph sync to main only, the grid an unsplit terminal would get in each shown tab group, computed by one pure function that mirrors FitAddon.proposeDimensions and is pinned against it; a ResizeObserver on each group body republishes when that grid changes. Main starts a new terminal at the size of the group it opens in (the named group, else the active one) and falls back to 120x40 only when no window measured it. This replaces the active-pane lookup, which mis-sized split groups and could read a phone's fitted size. * fix(terminal): measure group sizes from an untitled default-font pane and use the live active group A pane with a title or banner pushes its xterm host down, which skewed every group's row count; skip it like a zoomed pane. Size from the window's latest published active group, which is where its reveal places the tab, before the saved session's. * fix(terminal): size a CLI agent in a workspace the window has not shown Co-Authored-By: Claude <noreply@anthropic.com> * fix(terminal): learn the single-group inset only from bodies shown in the content area Co-Authored-By: Claude <noreply@anthropic.com> * chore: rerun PR checks against main with #28297 Co-Authored-By: Claude <noreply@anthropic.com> * chore: rerun PR checks against main with #28301 Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
b8e49222a3 |
fix(remote-runtime): wait for the desktop's terminal list before opening a terminal (#28154)
* fix(remote-runtime): wait for the host's terminal list before opening a terminal A paired client opening a workspace whose terminals it had not heard about yet read "no answer from the host" as "zero terminals" and asked the host for one. When the host's list then arrived with terminals, the workspace gained an extra terminal; when it arrived empty, the first-frame bootstrap created a second one. The client now treats an unknown host count as unknown. Activation without explicit startup work waits, and the active subscription's first real answer seeds the workspace if it is empty. An "ask me later" frame from a host that has not published yet no longer records a count of zero, and the host's later answer (sent as an update) is accepted as the first answer for seeding. * fix(remote-runtime): count an unknown host as empty unless it answers later Hosts before #25736 answer an empty workspace with "ask me later" and never follow it with a real answer, so waiting for that answer left a blank workspace with no terminal against them. Wait only on hosts that advertise session-tabs.empty-worktree-answer.v1; older hosts keep main's behaviour. Workspaces this client just created are known empty and open their first terminal without waiting. Co-Authored-By: Claude <noreply@anthropic.com> * fix(remote-runtime): log a failed delayed terminal open; drop an inert flag Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
3a011e48d1 |
fix(native-chat): load long chats on the phone by keeping every update within its limit (#28203)
* fix(native-chat): fit chat stream pages in one mobile bridge frame A long native chat failed to open on a phone with "the shell ended this stream (overflow)". The host sized each history page to ~1.94 MiB (half the relay cap), but the mobile app ends any stream whose event frame is over 640 KiB, so the opening snapshot of a long chat never arrived. Pages now fit that frame; older rows load through Load earlier as before. The page also words a stream the app ended in plain language instead of the protocol's reason. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(native-chat): keep the outline on the relay page share; plainer end words Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): read the bridge frame cap from its shared definition Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(native-chat): bounded frames for the phone's chat stream A phone page now asks the host (when it advertises agent-session.bounded-frames.v1) to keep every agentSession.subscribe frame within the bridge's 640 KiB frame: - each row is cut to what the phone draws (tool output to its detail length unless it draws as a diff, tool inputs to the diff view's length) and then to one row's share of a page, its long strings cut to a common length; the row is marked `truncated` with its full size - history frames carry rows and metadata held to a fixed ceiling; the draft queue and the background-task roster go in frames of their own; the command list is not sent, since the phone reads it from options - the queue's admission bound counts whole cards and is sized to one frame, so a phone always gets every card whole Other subscribers get the frames they always did. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(native-chat): bounded frames review round 1 - the phone waits for the host's capabilities before opening the stream, so its opening frame is never sent unbounded - rows are bounded once, by the reader that sizes the page; the item form is an options field and always a function - one cut rule (`cutLongStrings`) serves the display trims and the fit - a side list rides an empty history batch rather than a second frame; commands changes send a bounded subscriber nothing - queue admission measures the candidate list once; a legacy queue of more cards than fit lists the ones it sends first Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(native-chat): bounded frames review round 2 - an over-budget legacy queue cuts its text to one common length and marks the cards it cut, and no longer names a next card it left out - one predicate says whether a subscriber is owed the `/` list - a lighter invariant fixture Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(native-chat): type the shortening guards against named shapes Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(native-chat): bound every phone's chat stream at the host The host now bounds frames for every phone (clientKind 'mobile') at the RPC boundary, so the subscribe param, the host capability and the phone's wait for the capability probe are gone. Phones of every version already ignore the stream's slash commands, and a phone reached through the desktop relay signs in as a phone too. - A live gap of more than one page reaches a phone as the newest page, so no publish outruns the bridge's unacked window; older rows load on scroll. - Queue admission sizes each card as wide as a failed send can make it. - Rows are bounded only as a page takes them; the unread row marker is gone and a shortened queue card is marked `truncated: true`. - A bounded subscriber no longer reads or tracks the slash commands. - The stream-end words are back to main's: `overflow` also covers a page that stopped acking, which "too large" would misstate. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(native-chat): count every card in line and never shorten one Queue admission now counts every card the queue can publish again without passing admission: waiting and returned cards, and sent cards whose delivery is not yet accepted, which a refusal or a Stop puts back. The fixed per-card reserve for a failed send's words is gone. When a phone's list is over its frame room anyway (an older build's larger queue, or a card that grew after admission), the phone gets the cards it sends first, whole, and the rest once those leave. Cards are never shortened, so no app version can Edit a partial copy over the full draft, and the `truncated` card flag is gone. Bounded frames strip only the roster from an event; no event carries the other side lists. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(native-chat): bound only phone frames; keep main's queue and page sizes Queue admission is main's again. The phone's list is bounded where it is sent: the cards it sends first, whole, so every frame fits for any queue size and no card is shortened. History pages keep main's size for every reader but phone subscribe frames. A reader is now its item form plus its page budget, so desktop pages, the outline and phone history replies (chunked by the app's bridge) are as on main, and only bounded frames use the bridge frame's share. Drops the queue's `nextQueuedMessageId` rewrite, which no phone reads, and a test that duplicated the prefix check. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(native-chat): bound phone rows by size alone; final tidy Rows the journal already caps at a 16 KiB head gain little from a second, display-shaped cut, and the diff check behind it passed a character count as a line count. A row is now held to its frame share by shortening alone, and native-chat-diff is back to main's. Unexports file-local bounds, fixes a capability-era test comment, and adds tests for string shortening and a phone's stop-note refresh. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(native-chat): assert a phone's stop-note refresh pages Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(native-chat): run the bounded-frames suite in the Node runtime project It opens a real SQLite journal. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(native-chat): bound legacy dispatch reasons on read Rows written before the write-side bound carry a provider's full error text, which every page and phone frame attaches with its submission. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
8471b3a5c2 | fix(layout): restore the saved active group as a window does (#28174) | ||
|
|
962c338b67 |
feat(wsl): Windows path actions for WSL hosts through one owner-routed capability (WSL-via-orcad C8) (#27955)
* feat(wsl): open WSL host paths from Windows through one owner-routed capability (WSL-via-orcad C8) * test(wsl): typed shell handler lookup for the WSL owner tests * fix(wsl): read known WSL hosts through a registered accessor; e2e for the Windows folder picker * fix(wsl): open the host-path entry filled in after a Windows pick; e2e screenshots * test(wsl): read the e2e picker's default path through the main process env * refactor(wsl): read a WSL host's distro from its record instead of a new accessor * fix(wsl): skip the host browser only for a path Windows' picker or an Explorer drop filled in A path left filled from an earlier pick on an SSH or paired host opens the host browser again, as it did before. |
||
|
|
9f2abd5e70 |
feat(wsl): Add WSL distro host UI and status (WSL-via-orcad C7b, experimental) (#27934)
* feat(wsl): Add WSL distro host UI, host row status and fake-transport e2e (WSL-via-orcad C7b) Experimental toggle (Windows only), an Add WSL distro choice beside SSH host and remote server, the WSL host's own row in host pickers with its liveness state, a Connect action that starts a stopped distro only on request, a WSL hosts list in Settings, and the renderer report of which WSL hosts an open project or terminal needs. * feat(wsl): show WSL hosts as turned off while the experiment is off, and confirm their removal With Experimental → WSL distros as hosts off, a WSL host keeps its row so its projects never seem to vanish, says it is turned off, and offers Turn on (opening that setting) instead of Connect; Settings lists it the same way with Connect disabled. Removing a WSL host now asks first in the dialog SSH hosts use, with WSL copy. The e2e covers both. * fix(wsl): skip the WSL host status bridge when the preload lacks it * fix(wsl): show when this build cannot run WSL hosts instead of failing an add The UI reads main's template capability once. Without it, the add dialog says this build can't run WSL hosts and disables Add, existing hosts' rows and Settings say the same with Connect disabled, and the Experimental toggle notes it. The e2e covers a fake build without the files. * fix(wsl): re-read the WSL distro list when the add dialog's window returns, and on Refresh The add dialog read the registered distros once, so one imported while it was open never appeared. It now re-reads when the window regains focus or becomes visible, and from an explicit Refresh button, never on a timer. The selection is derived from the current list, so a refresh cannot leave a stale pick. The e2e imports a distro mid-dialog. * fix(wsl): list WSL hosts only as WSL hosts, not among the generic remote servers Settings → Remote Orca Servers, the server-update checks and the status bar's server list now leave out environments whose deployment record is a WSL host (by kind, never by name). The WSL hosts section reads them from the store itself. * fix(wsl): say a WSL host's removal ends its terminals, and report what it left running The confirmation now says removing ends the host's open terminals as well as Orca's server, and a removal main could not complete in the distro (a terminal or the daemon kept running) is reported instead of passing silently. * fix(wsl): ship the removal copy that says a WSL host's terminals end The build resolves copy from the English catalog, which still held the old text under the reused key, so the new inline default never reached the dialog. The copy gets its own key, the stale entry is dropped from both catalogs, and a test reads the shipped catalog. * fix(wsl): leave a removed host's workspace, keep its removal warning up, and prove removal in e2e After removal the active workspace is left when it lived on that host, so nothing keeps calling a host that no longer exists, and a warning about what still runs in the distro stays until dismissed. The e2e now checks that removal ends the host's terminals on the server standing in for orcad, shows no warning, leaves the workspace, and starts nothing for the host afterwards. * test(wsl): read the shipped removal copy and the fake's connect log without reflection * fix(wsl): say a WSL host is turned off in its terminal instead of 'Reconnecting' With the experiment off (or without server files) nothing retries, so a WSL host's terminal now says so, with Turn on, rather than claiming Orca is reconnecting. * test(wsl): reproduce project-removal survivors and orphan terminals in e2e The headless server standing in for orcad now carries a project whose terminal must end when the project is removed (and leave the host's own catalog), and an orphan terminal whose project the host already forgot, which removing the host must still end. Both checks fail without the fixes. * fix(wsl): drop the IPC wrapper from file-explorer and editor load errors; offer Turn on when WSL hosts are off The file explorer showed "Error invoking remote method 'runtimeEnvironments:call': Error: ..." for a WSL workspace whose host was turned off. Explorer read errors now go through the shared IPC-error unwrapping, and when the workspace's WSL host is turned off the empty pane shows the same turned-off copy and Turn on button as the terminal banner instead of a read error and a Retry that cannot work. The editor's file-load error view drops the same wrapper at render time (stored text is unchanged for retry classifiers). * test(wsl): open the internal gate for the WSL host e2e, and prove a closed gate shows nothing The fake-transport e2e launches its desktop with ORCA_INTERNAL_WSL_HOSTS=1. A new case leaves the gate closed with a WSL host already saved and the Experimental setting on: no WSL host API, list entry, host row, Add WSL distro action, Experimental toggle or Settings section appears, the fake never starts a distro, and the saved host stays on disk. * test(wsl): count only live shells when proving a removed project's terminals ended On Linux CI the hub keeps a pty-less tab record for the removed worktree after terminal.stop ended its shell, so counting every terminal.list row never reached zero. |
||
|
|
7ed23199a5 |
refactor(orcad-migration): migration source abstraction, capability-gated source.kind, path-rewrite stage (WSL-via-orcad D0) (#27790)
* test(orcad-migration): pin the SSH manifest digest before the source abstraction * feat(orcad-migration): capability-gated source.kind discriminator, SSH wire unchanged * refactor(orcad-migration): MigrationSource abstraction with SSH relay as the only source; table-driven path-rewrite stage (identity for SSH) * fix(orcad-migration): name the wire source type * chore(orcad-migration): exhaustive path-kind switch; no test assertion * test(ssh): run the relay migration source's SQLite test in the Node runtime project * perf(orcad-migration): rename record keys in linear time and skip the walk for the identity SSH exports run the path-rewrite stage on every stage and commit check; re-reading Object.keys per entry made a 10k-key record cost seconds on the main thread. |
||
|
|
a65fdc887f |
feat(wsl): WSL distro hosts, main-process wiring and persistence (WSL-via-orcad C7a, experimental) (#27933)
* feat(wsl): persist WSL host deployments in their own store and type orcad deployment links by kind * chore(wsl): drop an unused link type export * feat(wsl): add, persist and connect WSL distro hosts behind an experimental flag (WSL-via-orcad C7a) Main-process wiring for WSL hosts: list registered WSL 2 distros, add one (C4 deploy and launch through C6 liveness, then persist the paired environment and its C2 record), keep a relaunch's new device token, connect a host only for the user or an open project or terminal, adopt holders at startup, and remove a host (release its holder, drop both records). Refuses a distro whose orcad this desktop already reaches as another server (R25). * fix(wsl): read distro registrations through one reader that returns the canonical unbraced GUID C4's registry reader returned the braced GUID while the C2 record only accepts it unbraced, and C7 parsed the same output a second time. The one reader now yields lowercase unbraced ids and each registration's WSL version; the host distro list uses it. * fix(wsl): refuse to connect a WSL host while the experiment is off Adding, connecting and background connects all stop while Experimental → WSL distros as hosts is off; removing still works. The pre-deploy refusals move to their own module. * fix(wsl): stop a connected WSL host's server when the host is removed Remove ended the bridge and released the distro's holder but left orcad to exit with the distro. A connected server is now ended right away, only in a distro already running. * fix(wsl): import the orcad host errors from their new orcad/ home * fix(wsl): refuse WSL hosts in a build without the orcad template, and say so up front A build shipped without the orcad deployment template let the add dialog run and fail after seconds with a raw internal error. Main now checks the template as SSH hosts do, refuses add and connect (and skips lazy connects and holder adoption) before any deploy work, and exposes the capability so the UI can show it. * test(wsl): let the e2e WSL fake re-read its distro list, to register one mid-run * fix(wsl): end a removed host's terminals and daemon, and stop WSL hosts when the experiment is off Remove now ends the host's terminals through orcad's terminal RPC, waits for the terminal census to prove none remain, closes the bridge, and runs the managed decommission SSH hosts use, which stops orcad and retires its idle terminal daemon. What it could not end is reported back. Turning Experimental → WSL distros as hosts off closes every connection and liveness watch, releases the holders so distros can idle-stop, and keeps liveness from relaunching for visible work; turning it on resumes only the hosts open work needs. * fix(wsl): keep WSL hosts out of the servers a phone reaches through this desktop They are this desktop's own distros, connected only for work open here; a phone listing or routing to one would wake it behind the user's back. * fix(wsl): end a removed host's terminals through its own route, and never respawn it Removal reached orcad with the bridge's raw pairing offer rather than the routed environment and saved pairing the app already uses for the host; on a real orcad ending terminals failed fast and left them, the daemon and orcad running. It now calls by environment id through the same transport as every other call to the host, and the e2e fake runs these real steps against a headless server. Removal also stops tracking the host first (open work, connects, runtime calls), so nothing reconnects it while it stops, and a spawn guard in the connector refuses any holder, deploy or bridge for a distro whose host is gone (the respawn loop after removal). A WSL host that isn't connected answers runtime calls with why, instead of failing as an unreachable network server. * fix(wsl): suggest reconnecting a WSL host, not Tailscale, when it can't be reached Unreachable-server hints are chosen by the owner host's kind: a WSL host is a distro on this computer, so it points at its reconnect instead of the Tailscale advice for network servers. * test(wsl): parse the terminal list removal reads, and log the e2e fake's connects to a file * fix(wsl): close a removed host's terminals by handle and report stopped only on proof Removal stopped terminals per workspace, so one orphan whose project was gone threw selector_not_found and aborted before orcad was stopped. It now closes each terminal by handle, keeps going past failures and names them, and still asks orcad to stop. It reports stopped only when the census shows no session left, the daemon retired, and the orcad the distro records as active is the one this computer was connected to; a re-add that adopted an orcad the record does not name is reported instead of claimed stopped. * feat(wsl): close WSL hosts behind an internal-only gate in every build WSL distros as hosts must not be reachable by users until moving a project between hosts is lossless and the feature matches today's WSL support. ORCA_INTERNAL_WSL_HOSTS=1 opens the gate; otherwise main creates no WSL host service (no holder adoption, wsl.exe spawn or reconnect), refuses add, connect, remove and every runtime call to a WSL host, keeps persisted WSL hosts out of the server list, startup status probes and AI vault scans while leaving them on disk, and the preload exposes no WSL host API. * fix(wsl): keep SSH and paired servers listed whatever the WSL deployment file holds An unreadable WSL record no longer hides an SSH-linked server, and with the internal gate closed a corrupt WSL file is warned about instead of failing the whole registry. * fix(wsl): let an unreadable WSL record hide only a loopback pairing A WSL host always pairs over a loopback endpoint, so a stale record can no longer remove a remote paired server from the list. * fix(wsl): ignore unreadable WSL deployment state while the internal gate is closed A newer or stale record could otherwise hide a re-paired loopback server for users who never open the gate. |
||
|
|
5e9489c0c0 |
feat(wsl): account env for agent tabs on a WSL orcad host (WSL-via-orcad C9b) (#27940)
* feat(wsl): account env for agent tabs on a WSL orcad host (WSL-via-orcad C9b) terminal.createAgentSession and an explicit terminal.ensureAgentSession (new agent tabs, restored and sleeping agents) accept an optional accountEnv, gated by the new agent-session.account-env.v1 capability because both schemas are strict. The desktop shim attaches it only for an advertising WSL orcad, reading its capabilities from status.get; an older host keeps its default account and the desktop shows a one-time notice per distro. accountEnv stays outside the create replay fingerprint. * test(wsl): name the C9b runtime store stub's cast * test(wsl): expect the WSL account notice listener in the app-lifetime IPC lifecycle |
||
|
|
41e6ea530a |
feat(wsl): accounts shim for agents on a WSL orcad host (WSL-via-orcad C9) (#27874)
* feat(wsl): accounts shim for agents on a WSL orcad host (WSL-via-orcad C9) The desktop prepares the selected Claude/Codex account's in-distro home with the existing WSL account code and sends it as env on terminal.create / session.tabs.createTerminal, or as agent.launch's new optional accountEnv. Structured sessions keep their host-derived home and warn. Launch-time CLI resolution inside a WSL guest skips /mnt/<drive> shims. * refactor(wsl): track WSL host environments beside their registry |
||
|
|
2bb02f062d |
[CI] Reuse Windows resolver imports and retire unused helper tests (#28296)
* [CI] Retire the weak local title-corpus comparison * [CI] Retire tests for seven unconsumed helper modules * [CI] Reset the Windows launch resolver graph once per group |
||
|
|
5e09488911 |
fix(terminal-daemon): status says live, exited or unverifiable, and shows the process ceiling (#28239)
* fix(terminal-daemon): status reports live, exited or unverifiable, and the process ceiling `orca terminal-daemon status` printed 'Terminal daemon: OK.' whenever no severed daemon was recorded, including the ~60 s after a kill -9 before the next terminal respawned it. The host now probes the daemon (health check, then its pid record with Linux start-time identity) and returns an additive `daemon` report; the CLI prints the verdict and, for a live daemon, the terminal process ceiling, which no CLI command showed under Electron serve. Fixes BUG-5 and BUG-7 from the real-host Linux test run. * fix(terminal-daemon): load the ceiling description from shared so the packaged CLI can resolve it The packaged CLI cannot load out/main/daemon modules: electron-vite replaces out/main after the CLI build. The ceiling state and its description move to src/shared, which the CLI already ships. * fix(terminal-daemon): a pid the signal proved live is never exited because procfs hides it * fix(terminal-daemon): load the liveness probe lazily so the RPC table stays light Importing the probe eagerly pulled the daemon provider graph into every module that loads the runtime RPC method table, which broke the cross-version terminal wire harness. --------- Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
b724e3a24d |
fix(linux): a supervised AppImage serve follows a replaced AppImage (#28244)
* fix(linux): a supervised AppImage serve restarts from a replaced AppImage Swapping the AppImage file never reached a supervised serve: it runs the copy it unpacked, and even a full service restart went back through the stable launcher to that same copy. The extraction cache now records which AppImage each namespace came from. Before each start the supervisor compares that file's identity with the build it runs; once a settled new file is there and runs on this host, it execs the new AppImage with its own arguments (same pid), whose serve unpacks the new build and re-points the stable launcher. Fixes BUG-4 from the real-host Linux test run. * fix(linux): try a replaced AppImage once, and let it past an old build's give-up - A give-up recorded against the old build no longer stops the supervisor before it checks for a replaced AppImage. - The supervisor records the AppImage identity it is about to exec in the profile. If that build dies before its own crash brake, later starts of the old build keep running it instead of exec'ing the same broken file until systemd's start limit disables the service. A supervisor running that build, a further replacement, or --force clears the record. * test(cli): compare the namespace listing order-independently --------- Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
e09d732d17 |
perf(wsl): pooled wsl.exe command loops and orcad compile cache on the WSL wake path (C4 follow-up) (#27923)
* perf(wsl): run stdin-free probes over pooled wsl.exe command loops; cache orcad's compiled bundle The WSL wake path made ~37 sequential wsl.exe starts at ~180 ms each. Stdin-free execCommand calls now go through a few long-lived wsl.exe sh loops (one command at a time per loop, own session, stdin at EOF, base64-framed output). SSH has no such method, so its commands and options are unchanged. orcad enables Node's compile cache inside a client-managed slot, and hands it to the profile preflight child, which compiles the same bundle. * fix(orcad): share the slot compile cache with the preflight only in a managed slot An unmanaged orcad started with its own NODE_COMPILE_CACHE kept that location for its preflight before; never redirect it into the install dir. |
||
|
|
9e52645e6c |
Show Codex model capacity failures as red agent status (#28231)
* Show Codex model capacity failures in agent status * Reduce Codex capacity detection work on terminal output * Preserve Codex capacity notices across partial terminal frames |
||
|
|
4559c250b3 |
fix(windows): restrict secure files in-process instead of spawning icacls (#28135)
* fix(windows): restrict secure files in-process instead of spawning icacls Every secure write on Windows spawned icacls.exe five times on the calling thread (the Electron main thread for most stores): verify, /reset, /inheritance:r /grant:r, verify, verify. Process creation under endpoint security costs ~0.6 s each, so each write froze the UI for seconds (#26027). The /reset + /inheritance:r pair is also what behavioural EDR scores as ransomware tradecraft (#25583). Add native/windows-security (@orca/windows-security): a small N-API addon over advapi32 that reads and writes DACLs in-process. It exposes the token user SID, a DACL read as SDDL, a DACL write (sync and on the thread pool), a create-with-DACL file write, and an additive grant. Policy and verification stay in TypeScript. - writeSecureFile creates each temp file already carrying the protected user/SYSTEM/Administrators DACL (CreateFileW + SECURITY_ATTRIBUTES), reads it back once, and renames it. No ACL is modified on the write path and no process starts. Before: 5-7 spawns, 94-138 ms per write. After: 0 spawns, ~0.5 ms. - A secure directory is verified once per process. It is rewritten only when verification fails, on the thread pool, because the write walks every descendant (~2 s on a 29k-entry userData). - The verifier follows OpenSSH's rule: only the user, SYSTEM and Administrators may be granted anything, and the path must be protected (or, for a file, inherit from a secure parent). DACLs written by the icacls-era hardening pass unchanged, so there is no migration. - If the addon cannot load, the write still happens and reports unrestricted, logged once. There is no icacls or PowerShell fallback. - grantDirAcl, the startup userData grant, and the install-dir probe and package repair use the same addon, so whoami.exe and icacls.exe are no longer spawned anywhere at runtime. - orcad's Windows slots ship the addon beside their bundles (built per arch, PE-machine checked). Its data-root privacy check fails closed, so without the addon a Windows orcad would not start. Fixes #26027 Fixes #25583 * refactor(secure-file): harden existing Windows files through the sync lane The read path called restrictWindowsPathSync directly and recorded success itself; the existing sync lane already does both. One call site fewer. * refactor(windows-security): one load, one rule, no fallthrough (review #28135) - The descriptor parser is anchored, so a conditional ACE (its body holds parentheses) or anything else it cannot read fails closed instead of verifying the ACEs before it. - findSecureDaclViolation is one flat rule: protected, allow rules only for the user, SYSTEM and Administrators (OpenSSH's ACE rule), and full control for the user. The inherits-from-a-secure-parent exception is gone: it accepted exactly what loosening the parent reaches. - The addon and the user SID load once, behind one wrapper that adds the extended-length prefix to every path and runs async DACL writes to one path in order, so a startup grant cannot land after a re-lock and restore the broader DACL. - With the addon loaded, a failed born-restricted create throws with the matching Node error code instead of falling back to a plain write. - A grant leaves a NULL DACL untouched instead of turning 'everyone' into 'only this SID'. WriteFile runs once and fails on a short write. - The artifact create-intent store writes through writeSecureFile and drops its own temp writer, directory LRU and synchronous directory re-lock. - Test seams the global fake makes redundant are gone. * fix(packaging): fail a Windows build whose workspace addon binary is missing or foreign-arch The @orca/windows-* workspace links exist on every host, so their presence proved nothing: a missing or wrong-architecture .node only surfaced at runtime, as unrestricted secure files or an orcad that refuses to start. beforePack now reads each addon's PE machine against the target arch. * refactor(windows-security): prune what the native ACL left dead (review #28135) - Sync-spawn ratchet: hardenSecurePath and the sync-flag exemption it alone used are gone, and the advice and baseline header no longer say secure writers run icacls. - The install-dir probe reads ACEs through parseSddlDacl, so an ACE it cannot parse fails the whole reading closed instead of being skipped; AC is a resolved alias there now. - The packaging check reads the addon binary through node_modules, the copy that ships. - No winCode-to-errno table: nothing reads .code off a failed secure write. - The load memo is not mutated; secure-path keeps its own log-once flag. - grantInstallDirPackageAcl is a plain function, and secure-file restricts a Windows directory and a POSIX path directly instead of through one two-platform helper. * fix(windows-security): resolve LA in real-ACL tests; tear a file whose close fails - The GitHub Windows runner is the built-in Administrator (RID 500), whose SID reads back as LA. Production already resolves it through localDomainSidOf; the win32 test helpers parsed without it. One shared readDaclOf resolves aliases for every real-ACL suite, and the verifier's unit test covers an LA-aliased directory and LA as a stranger. - createFileWithDacl treats a failed CloseHandle like a failed write (lost data on a network volume) and deletes the torn file. - Drop a duplicate test reset and a typo. * fix(windows-security): load only from inside the app; read DACLs as structure (review #28135) - Loader: orcad's staged binary first, then the workspace package only if its real path is inside the app (process.resourcesPath, or this checkout). A bare specifier otherwise walks every parent up to C:\node_modules, which any local user may create, and orcad holds credentials. - The addon reads a DACL as { protected, aces: [{ type, flags, mask, sid }] } via GetAce, with a SID only for plain allow/deny ACEs and null for a NULL DACL. findSecureDaclViolation and the install-dir probe check that structure, so conditional, callback and object ACEs fail closed. The SDDL parser, its alias table, LA/localDomainSid resolution and getDaclSddl are gone; SDDL is only the write format. - Install-dir tree pass: files flagless as before, but a directory only when its own DACL is protected (inheritably). Re-granting inheriting directories re-propagated each subtree (3.2 s for 1,093 directories). The root is no longer granted, or counted, twice. The two passes are plain functions behind a { root, tree } seam. - One namespacing (path.win32.toNamespacedPath) at the addon boundary, also the per-path ordering key; the comment states the real sync-grant invariant. - restrictWindowsDirectory(dir): Promise<boolean> replaces the callback lane; the reporter is guarded once, so the 'settle' stage is gone. - Sync-spawn ratchet: the secure writers no longer spawn or block, so they leave the gate (109 -> 61 uses). - Errors name the Win32 call and code only; a failed GetSecurityDescriptorControl fails the write instead of defaulting to unprotected. * test(secure-file): assert the durable POSIX write's outcome, not the old chmod order writeSecureFile now chmods the temp file to 0o600 before fsyncing it, so the fsync also covers the mode change and the file is owner-writable when opened. Assert what matters under umask 0o200: the write succeeds, the temp file is fsynced read-only, and the published file is exactly 0o600. * fix(windows-security): namespace paths with the host's toNamespacedPath path.win32.toNamespacedPath rewrote POSIX temp paths (/tmp/x -> \tmp\x) in the Linux runs of the Windows-logic unit tests, so the fake addon wrote to the wrong place. The addon only runs on Windows, where the host function is the win32 one; elsewhere it is the identity. The long-path assertion runs on Windows only. * test(windows-security): assert the install-dir long-path prefix on Windows only The prefix comes from the host's toNamespacedPath, which is the identity off Windows; the Linux run of this suite expected the win32 escape. * fix(windows-user-data-acl): grant the root even when its children cannot be listed (review #28135) A readdir failure on userData aborted the whole grant, so the root grant that could repair a DACL refusing the listing was never attempted. The listing failure is now recorded, the root is still granted, and the marker stays withheld so the next launch retries. * test(windows-security): wait for relock propagation, accept Entra ID SIDs (review #28135) - The large-relock test waited only for the parent to turn protected, which happens before propagation reaches the children, then read a child. It now waits on the parent and both ends of the child range. - The token-user test rejected S-1-12-1 (Entra ID) accounts, failing the suite on Entra-joined dev machines. --------- Co-authored-by: Orca Worker <orca-worker@localhost> |
||
|
|
7246a0e269 |
Fix delayed agent launches such as sleep 30 && claude (#28205)
* fix(terminal): preserve preparation and discovery for delayed agent launches * refactor(agents): centralize launch intent recognition * refactor(agents): keep launch intent in one module apart from process recognition Move recognizeAgentLaunchCommand next to the sleep-prefix scanner so the strict process-table matcher is untouched, name the module away from the agent-launch funnel files, share one executable-name normalizer, and colocate the preparation tests with their modules. |
||
|
|
aceebea0a4 |
fix(terminal): name a new tab's first pane so its saved layout is never empty (#28168)
* fix(terminal): name a new tab's first pane so its saved layout is never empty createTab now always mints the first leaf id, so the window never saves a terminal tab with an empty layout root. With no empty roots coming from the window, main's save-time swap that kept its own root over an empty window root (from #1909) has nothing left to guard, and is deleted. * test(persistence): an empty-root layout from an old session still loads and gets its pane * fix(persistence): name an old session's empty layout root once at load A session saved before tabs named their first pane can hold an unmounted tab with an empty root. If main bound a terminal there, the window's next save of that tab dropped the binding once the save-time swap was gone. Name the pane at load instead (the same pane the window would mount), so no save carries an empty root. The rootless-pane resolver moves to shared so main and the window pick the same pane. * test(persistence): name the bind-then-stale-write order the load repair covers * fix(persistence): name an old empty tab's pane with the stable headless leaf id The load repair fell back to a random id, so an unsaved headless serve gave a phone a new tab::leaf id on every restart. Use deriveHeadlessLegacyTerminalLeafId, which phones already saw for these tabs. Also import the rootless resolver from shared directly and return the loaded state unchanged when nothing needs repair. |
||
|
|
5929350944 |
fix(linux): direct AppImage serve honours the chosen profile and --supervise (#28227)
* fix(linux): run a direct AppImage serve through the CLI so it keeps its profile and supervisor A direct `Orca.AppImage serve` stayed in the Electron process and skipped the CLI's serve front end, so it ignored ORCA_USER_DATA_PATH and silently dropped --supervise. It now execs into the bundled CLI (same pid, so service managers and signals still reach it), translating a leading --user-data-dir into ORCA_USER_DATA_PATH. Also lists terminal-daemon and reference as CLI commands so a direct binary launch of them reaches the CLI. Fixes BUG-1, BUG-3 and BUG-10 from the real-host Linux test run. * fix(linux): drop desktop switches anywhere in a direct serve launch * fix(linux): keep a direct serve's --no-sandbox for the Electron server the CLI starts --------- Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
8cd5bb2a74 |
feat(wsl): deploy and launch orcad inside a WSL distro (WSL-via-orcad C4) (#27819)
* feat(wsl): deploy and launch orcad inside a WSL distro (WSL-via-orcad C4) * fix(wsl): keep the install-dir fallback on the PATH detection searched, so a /mnt/c shim cannot re-enter * fix(wsl): call the install-dir resolver exactly as before when no PATH is given * fix(wsl): import orcad artifacts from their new orcad/ home * fix(wsl): adopt an orcad serving in the distro that the activation record does not name * fix(wsl): filter /mnt/<drive> PATH entries only in an orcad launched as a WSL host SSH-managed orcad inside a distro and Orca under WSLg kept detecting drive-mounted CLIs before this stack; the setsid launch now marks itself with ORCA_WSL_ORCAD_HOST=1 and only that process filters. * fix(wsl): keep the install-dir fallback on the process PATH outside a WSL host orcad On Windows the preflight env merges the persisted registry Path, which the fallback never searched before; only the marked WSL host runtime now passes a filtered PATH. |
||
|
|
49a1129aa6 |
build(linux): carry the pinned Node inside the AppImage so serve's slot works offline (#28204)
* build(linux): carry the pinned Node inside the AppImage so orca serve's slot works offline Linux packaging places the pinned glibc Node at resources/node-runtime/bin/node before the glibc-floor gate, proves it serves one PTY with the packaged daemon entry, and deb/rpm exclude it through fpm. orca serve's orcad slot copies that Node (hash-checked) instead of downloading. * test(serve): type the bundled Node fixture --------- Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
e2751d4318 |
After a restart, an agent tab asks the terminal host what is running, never "Couldn't confirm the agent started" (#28043)
* fix(agent-launch): after a restart a launch pane is an ordinary pane, never "couldn't confirm" A launch pane's spawn read the launch record (and the tab's saved outcome) when no launch was running for it, so a row left pending by a quit showed "Couldn't confirm the agent started." The record never observes the process; the terminal host does. With no launch running in this process the pane now spawns exactly like any tab: a live agent reattaches, an exited or absent one gives a fresh shell, and a gone daemon shows the normal unavailable state. - the running launch tells its pane how it ended in memory (finish carries the verdict); only a definite failure says "couldn't start", an unknown outcome leaves an ordinary pane - deleted: agentLaunchPaneVerdictFromRecord, listAgentSessionOperationRowsOwningPane, the `unconfirmed` verdict and its notice string, the tab's persisted launch pane, ownedPane writes - rows written by an older build that carry ownedPane are ignored; they expire with the row * test(agent-launch): the restart pane test joins the attach suite it shares mocks with * fix(agent-launch): a launch pane whose agent couldn't start keeps its notice for the session and never spawns a shell under it; a reload saves only a pending launch * test(agent-launch): drop assertions on code that is gone, and the taking-back case the IPC suite already pins * fix(agent-launch): narrow the finished-pane check, and test the saved session's launch pane with a typed tab * fix(agent-launch): a launch whose outcome is unknown still says "Couldn't confirm the agent started" for the session, and attaches its agent if it turns up * fix(agent-launch): main keeps a failed or unknown launch's answer for its pane until it quits, so the pane never spawns a shell under its notice; the window's spawn gate goes * fix(agent-launch): a failure kept for a pane goes when the pane is closed during its launch, moved to another tab, or its workspace is removed; comments say what an unknown outcome and a reload now do |
||
|
|
8e4e7be869 |
feat(daemon): pace automatic terminal restores on the desktop too (#28202)
* feat(daemon): pace automatic terminal restores on the desktop too The desktop daemon now enforces the restore budget that orca serve and orcad already enforce: the visible pane first, at most 3 automatic restores at once (1/s), at most 8 live per machine, and the rest stay sleeping until opened. ORCA_TERMINAL_SPAWN_ADMISSION=observe rolls newly launched daemons back to count-only. * fix(daemon): never put a user's own resume back to sleep; keep every refused old-client restore A user-resume past the per-client allowance is still paced as automatic work but is no longer deferred for a pause, the live cap or its deadline, so a visible pane is never put back to sleep in front of the user. The legacy restore record holds as many refusals as the host could run. --------- Co-authored-by: m4air <m4air@Mac.localdomain> |