mirror of
https://github.com/stablyai/orca.git
synced 2026-09-24 08:02:33 +00:00
0de5288dbc94efa49fea56ea274cbffff077ee47
8741
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0de5288dbc |
fix(runtime): keep a closed-tab worktree under the epoch already publishing it
`closeHeadlessMobileTerminalTab` minted `headless:<now>` on every close. Its sibling headless writers carry the stored `publicationEpoch` forward and mint only when there is no snapshot to inherit from — because a write to a worktree is not a claim to publish it. The close was the one writer that claimed. A paired client retires the epoch a new publisher displaces, and the web mirror's retirement is final: there is no revive lane, and the per-worktree tracking teardown deliberately keeps the epoch history. So an ordinary close published a stranger for a worktree the renderer generation still owned, retired that generation on every client, and the renderer's next publication — carrying the epoch the close had just retired — was rejected forever. The user emptied a workspace, created a terminal, and it never arrived on either machine while `session.tabs.list` showed the host holding it. This is the same thesis the retraction path already states, through the door next to it: a retraction is not a handover, and neither is a close. Measured on `paired-two-client-emptied-workspace-reseed.spec.ts`, six runs each: phase 2 failed 3/6 before (`A=null B=null`, both clients blind for the full 30s budget) and 0/6 after, with both clients adopting in single-digit milliseconds. |
||
|
|
3f66eea7c7 | Merge remote-tracking branch 'origin/main' into HEAD | ||
|
|
97aa5ff19b |
fix(mobile): open native chat when a new worktree launches a default agent (#19850)
* refactor(agent-launch): make the launch-mode decision surface-neutral
`decideWorkerStartMode` was the only shared answer to "structured chat session
or terminal agent?", but it lived in an orchestration-named module and spoke
orchestration's vocabulary, so the other launch surfaces could not call it.
Move the decision to `main/agent-launch/agent-launch-mode` unchanged and leave
`orchestration-worker-start-mode` as the adapter that supplies the noun.
A worker is not a special kind of launch; it is the same launch with a dispatch
attached. Naming the receipt's subject is the only thing orchestration actually
contributed, so that is the only thing the adapter keeps: "worker" in both
sentences, plus the `--terminal` wording, which reads as nonsense anywhere a
`--terminal` flag does not exist. Both are pinned, because they are asserted.
No behavior change. The receipts are byte-identical for every reachable case,
proven by running the new pin against both implementations.
Also pins the wording, which nothing was holding. The existing suites assert
`toContain` fragments ('terminal agent', 'cannot create') and the CLI suite
asserts a receipt handed to it by a mock rather than one this code produced;
all six files stayed green against a deliberately corrupted vocabulary. A
dispatch receipt is the only place a structured-to-terminal downgrade explains
itself, so the whole sentence is the contract, not a fragment of it.
* feat(agent-launch): add the launch intent and the one executor that runs it
The sequencing around the launch decision was duplicated per surface, and the
duplicate is where the bug lives. A new worktree was created agent-first, so
its startup terminal WAS the agent and the structured branch below it could
never be reached — every new-worktree launch was a PTY regardless of the user's
default. Orchestration fixed that for itself in #19431; mobile and the CLI
still have it.
`executeAgentLaunch` inverts the order once, for everyone. When the preference
is structured the worktree is created with NO startup agent, the executing host
is then asked whether it can host a session for the workspace that now exists,
and only then is a surface created. The host verdict cannot be hoisted above
creation: `agentSession.createSupport` only answers for a workspace it can
resolve, which is why the decision stays in two halves.
Agent-first creation is deliberately preserved for PTY launches — it is what
sequences the agent's startup command behind the setup runner, so wait-for-setup
comes for free there.
What actually differs per surface is only how a surface is built (an
orchestration worker's session takes a dispatch hold and a mailbox a plain
launch must not take), so that is injected as a factory rather than branched on.
The intent also strips the reserved agent fields from a migrated create payload:
a caller moving off `worktree.create` passes its existing params, and a stale
`startupAgent` in there would re-create the very path this replaces.
Tests assert order and arguments, not just the resulting mode. Reintroducing
agent-first creation reddens 4 of 11.
* feat(agent-launch): expose the launch executor as the agent.launch RPC
Adds `agent.launch` — one host-side method that decides structured-vs-terminal and
creates the surface — wired to the real runtime factories: `createManagedWorktree`
for the workspace, forking on `startupAgent` exactly as the orchestration worker
path does; `createStructuredAgentSessionForWorktree` for a chat session; and
`createTerminal` for a PTY agent. Allowlisted for mobile, which is the surface the
routing gap was reported on.
`worktree.create` is untouched. Its `startupAgent` keeps meaning "spawn a PTY agent"
verbatim, because it answers with `agentTerminalHandle` only on that path: a host
that quietly routed it to a structured session would hand every older client a
response with no handle and no error. All new behaviour sits behind
`agent.launch.v1`, which the host now advertises and a remote client must negotiate,
so a client that does not gets today's behaviour unchanged.
* feat(mobile): route workspace creates through agent.launch
Picking an agent on the mobile create sheet always produced a terminal, even
when the user's default was native chat, because all three create paths put
`startupAgent` on `worktree.create`. That means "create the worktree
agent-first", so its startup terminal IS the agent and the structured branch
below it is unreachable — while the same phone's in-workspace "+" button opened
a chat.
The blank, branch and new-branch creates now send the same payload through
`agent.launch` and let the host settle the surface. `worktree.create` is
untouched, and a host that does not advertise `agent.launch.v1` (read from the
existing `status.get` probe) keeps today's path exactly.
Work-item creates stay on `worktree.create`: they pre-fill the issue/PR URL as
an unsent `startupDraft`, which a structured session cannot hold yet, so routing
them would submit the URL as a first turn.
* fix(agent-launch): drop the deleted draft-prompt blocker from the reason map
main removed the draft-prompt blocker in #19681 (a structured session now holds
an unsent draft), so the exhaustive Record no longer typechecks.
* chore(agent-launch): carry a SAFETY rationale on the agent placement cast
The type-assertion gate landed after this branch's base, so the new file's
copy of the worker-start cast is now a changed-code finding.
* chore(agent-launch): carry agent.launch through main's RPC typing and casting gates
The typed-method contract, the generated params catalog and the
`assertionStyle: never` casting scan all landed after this branch's base.
- AGENT_LAUNCH_METHODS kept an `RpcMethod[]` annotation, which widened its
method name to `string` and broke assignability; every sibling infers instead.
- `agent.launch` binds a schema under src/main, so it joins the catalog's
RPC_METHODS_WITHOUT_SHARED_PARAMS and the parity gate's hand-listed twin.
- The now-typed methods make most test casts unnecessary; the few that remain
carry the line-specific SAFETY rationale the casting gate requires.
* test(mobile): supply the agent-launch fixture the create-submit recording needs
The golden RPC recordings landed upstream while this branch was out, so they
first met agent.launch here. Three things had to happen, and only one of them is
a fixture bump.
1. workspace-settings-mounts.ts mounts useNewWorkspaceCreateSubmit against a
fixture model that throws on any member it was not given. This PR added a
required getAgentLaunchSupport, so the submit aborted with "Missing model
fixture" before it ever issued the create, and three cleanup checkpoints
vanished. That read like a product regression and was not one. Supplying the
member restores the recording byte-for-byte; it is pinned false for the same
reason the cutover probe is, so the baseline stays on worktree.create.
2. Editing that adapter moves adapterSha256 for the twelve settings goldens it
mounts. Their recordings are unchanged - header only, by design: the digest
is per-golden so editing a module fails exactly the goldens that mounted it.
3. Five goldens changed behaviourally, and both changes are this PR's:
the capability probe now reports agentLaunch, and a create whose reply
carries no worktree returns "Failed to create workspace" instead of throwing
a TypeError off an unguarded result.worktree read. The launch route needs
that guard, since a receipt can arrive without a worktreeId.
* refactor(mobile): decode the launch receipt instead of asserting its shape
The changed-code quality gate refuses type assertions, and the eight it flagged
were worth removing rather than suppressing.
The production one was the point. readAgentLaunchCreateOutcome asserted the RPC
payload into Partial<AgentLaunchResult> and then runtime-checked it anyway, so
the assertion bought nothing and claimed a contract the host had not proven. It
now narrows with `in` and validates each hop, which is the same nullability
question readCreateResult already answers on the sibling path - a launch receipt
can legitimately arrive without a worktreeId. AgentLaunchCreateOutcome ties
worktreeId to the shared contract so a change there fails this reader's
typecheck rather than passing a differently-typed field through.
The test fakes claimed a whole RpcClient via `as unknown as RpcClient` while
implementing one member. They now build a typed literal, matching the pattern in
use-mobile-structured-agent-options.test.ts. The read sites cast params and then
read one field; they now assert the payload with toMatchObject, which removes
the cast and pins more of the shape than the cast did.
Also pins the warning passthrough, which nothing covered: a terminal launch that
seats the workspace but cannot start the pty reports why, and the absent, blank,
non-string and structured-surface cases report nothing. Writing that test caught
a real drop I had introduced in the reader.
* ci(mobile): re-run Mobile Checks when a shared capability changes
Mobile Checks is path-filtered to mobile/**, but mobile imports the negotiated
capability names straight from src/shared/protocol-version.ts and records the
whole capability read verbatim in its goldens. So a capability added desktop-side
rewrites a mobile fixture while never triggering the suite that would catch it.
That is what happened here: #19849 introduced agent.launch.v1 and Mobile Checks
never ran on it. Verified at the run level rather than by check name - the
window-free check-runs API on
|
||
|
|
12d744f253 |
fix(skills): keep computer-use off filesystem and shell tasks (#21069)
* fix(skills): keep computer-use off filesystem and shell tasks STA-7615: "On my desktop create a folder" was matching computer-use because discovery copy said OS/window-level and neighboring skills advertised desktop UI. Scope the trigger to visible GUI with no CLI path, and exclude files/folders/git/shell. * fix(skills): prefer programmatic paths over computer-use State the last-resort rule in discovery copy instead of enumerating files/folders/git/shell. computer-use prefers shell, filesystem, git, HTTP, CLIs, and Playwright/CDP; neighboring skills route to Computer Use only when a visible window needs GUI control those cannot do. * fix(skills): stop advertising computer-use from orchestration Orchestration coordinates workers; it does not drive a GUI. Drop Computer Use and Playwright/embedded-browser routing from its discovery description so those tools are not pulled in from a coordination skill. * fix(skills): drop Playwright from orca-cli discovery orca-cli should not prescribe Playwright or CDP. Those tools may not be installed, and page automation is not this skill's job. * fix(skills): drop the page-only ban from computer-use discovery Page automation is a preference, not a prohibition. If Playwright or CDP is not available, a visible browser window is valid Computer Use. Keep the hard split for Orca's embedded browser (`orca-cli`) only. |
||
|
|
f02d09c1ba |
fix(native-chat): deliver queued messages while the chat pane is hidden (#20659)
* fix(native-chat): deliver queued messages while the chat pane is hidden With two or more messages queued, everything behind the head waited on the user's attention. The drain only inspected the head and returned unless it was `queued`, and a `pending` send deliberately leaves the head `dispatching`. An entry only leaves that state through the journal subscription, which is torn down when the pane goes hidden -- and a worktree switch hides it. Two changes, both needed: - One shared admission rule now says what the queue does next, and the drain takes its `dispatch`: the first `queued` entry, skipping entries the host has already acknowledged. It still stops at an `unconfirmed` entry or a refusal the user must act on. Order is not the outbox's to keep -- the host appends the submission inside the per-session serialize chain before dispatching, so journal order is arrival order. Holding the tail bought no ordering guarantee and cost delivery. Single-flight still keeps sends strictly sequential, and a launch prompt's in-flight send, which runs outside it, still stops the queue. - The journal subscription now stays open while a session has undelivered outbox entries, published from the `writeOutbox` choke point. The subscription's retaining hold is what also keeps the host from evicting the session 15s after the last turn, which would otherwise turn the stall into a blocked head refusing `agent_session_ownership_unknown`. An acknowledged entry stays in the outbox rather than retiring on `pending`: the text is safe either way, since the journal upserts a render item from the submission's own body, but a `pending` can still settle `rejected` or `unknown` and only the entry carries the retry state that answer needs. Follow-on corrections the head-only assumption had hidden: - Single-flight is released where the disposition is applied, not in a later `.finally`. That state write is what re-runs the drain, so the release has to land first or the queue has no trigger left. - One ref now holds the in-flight entry's id instead of a bare boolean, and the reconcile effect keys its release on that, not on the head, so a journal update about the head can no longer discard a still-unsettled send of the tail. - A refusal blocks the entry it refused, read back by index so a rotated id is preserved. - The automatic unknown probe and the Retry affordance both read the blocker at whatever index it sits, the Retry through the same shared rule as the drain. `raises no delivery notice for a stuck message behind a healthy head` asserted that a message behind an admitted head raises nothing, because a Retry could not act on it. It now can, so that guard is rewritten to assert the notice names that entry and its Retry sends that entry. * fix(native-chat): resume outbox after journal admission and scope subscriptions * test: name outbox send request by domain role |
||
|
|
36cdb34097 |
test(agent-status): pin each legacy-bypass detector to its own case (#21004)
The ratchet's planted-fixture test collapsed every detection into a deduplicated kind set, so `passed-map` — which has two independent producing sites — stayed green when either one broke on its own. Give each planted form its own case with an exact expected detection. |
||
|
|
a28085adbf |
refactor(mobile): checked reply readers for the source-control domain (step 7 pilot) (#20950)
* test(mobile): ratchet the 201 unchecked RPC reply readers Step 4 moved every call-site cast into an RpcOperation's `read`, but 201 of those readers still answer `compatible: true` for any payload: `rpcUncheckedPayloadReader` (163), `rpcReadUnchecked` (26 outside its own module) and `rpcUncheckedMemberReader` (12), across 42 files. The cast moved; it did not become true. Held as data with an AST boundary test, shaped on the raw-request-port ratchet: a file that is not listed fails, a listed file that no longer has one fails, and a count that rises fails. Only a call counts, so an import is not a reader and prose never is. No behaviour change: this commit adds a list and a test. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): validate the source-control domain's RPC replies at arrival Replaces all 17 unchecked readers in mobile/src/source-control/ with `rpcResultVariant(variant, schema)`, so a malformed reply is an `RpcIncompatibleReplyError` naming the operation instead of a TypeError three frames downstream. The inventory drops 201 -> 184 and the five source-control operations files leave it entirely. This is a behaviour change, scoped to malformed replies. Six reply-matrix goldens move; every named-scenario golden and every `normal` partition is byte-identical, which is the parity claim. Schemas live one module per reply domain, beside the operations that read them: git-status, git-compare, git-history, hosted-review and worktree-metadata. A member is required only where a consumer reads it unguarded, and each schema records the consumer line that justifies it. Nothing is `.strict()`; every reply a consumer publishes verbatim keeps `z.looseObject` so an undeclared host member still passes through. Six replies have no reader anywhere in mobile and get `z.unknown()`, which is the honest schema for them, not a holdout. Three readers stay total by construction, because their contract is that an unreadable reply is a value rather than an error: the `git.status` projection (a null status three screens route on), the `session.tabs.list` reveal (a null list means poll again) and the generated commit message (a screen's copy, never a decode error in a text field). They gain the salvage report, not a verdict. Consumers take the schema's output type, so `MobileGitStatusResult` and the branch-compare aliases now name what mobile reads rather than the desktop aggregate, and seven call-site casts are gone. Three requirements came from the goldens, not from the host types: `git.history` sends `timestamp: null`, `hostedReview.getCreationEligibility` sends a `reviewLookupOutcome` the shared union does not list, and the `git.status` projection writes an absent member as a present `undefined`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the six source-control reply-matrix goldens step 7 moves Six goldens, all on malformed partitions. Every named-scenario golden and every `normal` partition is unchanged, which is the parity claim for this step. git.history-read / git.history#1 result-absent, result-null, inner-ok-missing, inner-false-string-error, inner-false-object-error: the load rejected with a TypeError reading 'items' or 'map' off undefined/null; it now rejects with `incompatible_reply: git.history-page (git.history)`. hostedReview.eligibility + create-intent / hostedReview.getCreationEligibility result-absent, result-null, inner-ok-*: the fetch fulfilled with the error envelope itself, re-typed as an eligibility and published into the compose prefill; it now rejects, and both callers already route that to the same "eligibility unavailable" state a null answer produced. hostedReview.create-chain + create-intent / hostedReview.create result-absent, result-null, inner-ok-missing, inner-false-object-error: the create form showed the raw TypeError text "Cannot read properties of undefined (reading 'ok')"; it now shows the incompatible-reply message. Every header digest is unchanged -- baseline, recorder, adapter, scenario and lockfile all match -- so the diff is the behaviour and nothing else. Recorded from this branch into a scratch directory and copied in, because there is no scoped honest alternative: scripts/rpc-recording.mts refuses to run unless the product tree equals the pinned baseline, and the README's remedy for an intended behaviour change is to repin, which rewrites the `baseline` header of all 667 goldens. So these six now carry a pin whose tree no longer produces them. That is a real gap in the oracle's design for behaviour changes, not a detail of this step, and it needs a decision before this lands. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the four reply-schema properties the goldens found Each of these cost a reply-matrix golden while writing the source-control schemas, and none of them follows from reading the consumers or the host types: a newer host's undeclared members must still decode, `git.history` sends `timestamp: null`, `hostedReview.getCreationEligibility` sends a `reviewLookupOutcome` the shared union does not list, and the `git.status` projection writes an absent member as a present `undefined`. The `.strict()` case is the one worth stating twice: at the top level it rejects the reply, and on the entry it drops the row, which shows a dirty worktree an empty Changes list. The fifth test pins the salvage report that makes such a drop visible instead of silent. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): give an unreadable reply a message a user can read `RpcIncompatibleReplyError` put `incompatible_reply: <op> (<method>)` in `message`, and `message` is what the screens hand to a toast. Step 7 is the first change that can reach this error at all, so the token would have shipped to users as its own error copy. Fixed at the boundary rather than per site: `message` is now plain copy, and the machine token moved to `code` (`incompatible_reply`) and `name` (`RpcIncompatibleReplyError`), both readable by callers. The cross-bundle fallback in `isRpcIncompatibleReplyError` matched on the old message prefix, so it now matches on `name`, which a foreign copy of the module still carries. No existing test pinned the old text. Two new ones pin the copy, the token and the foreign-copy match. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the recording baseline to this branch and re-record Commit |
||
|
|
740887fbbb |
feat(settings): connected computers rows for session history indexing (#20887)
* feat(session-search): add ranked history panel search and consent * test: wait for initial session indexing before refreshing results * feat(session-history): add local search settings and index controls * Use shared local host identifier for session index status * feat(session-search): merge all-computers search across hosts The `all` scope on `aiVault:searchSessions` now fans out from the desktop to every host the session list enumerates and merges the pages into one. Legs run in parallel: the local index through the search service, SSH and runtime hosts through the existing remote search client. Two fixed orders, because relevance scores from independent indexes are not comparable. `newest` asks every leg for recency and k-way merges on `updatedAt`, nulls last, ties broken on execution host id. `relevance` rotates hosts in host-id order by their own rank. The merged cursor is an opaque base64url payload holding each host's cursor, how many of its current page were already emitted, and the generation that offset counts into, plus the page size and sort the cursor belongs to. A host whose index moved is fenced to `stale` and stops contributing; the rest keep paging. Per-host outcomes ride back on one new optional `hosts` field on the results response. `aiVault:searchStatus` with `all` stays refused, and neither the runtime RPC nor the CLI gains the scope, so a fan-out is never two hops. * fix(preload): let the search bridge address the all-computers scope * feat(settings): live index status, enable confirm, advanced delete * feat(session-search): search every computer from the history panel The panel's "All computers" scope produced no request: the hook parsed the scope into a single host id and stopped when that was null, so the panel answered "Choose one computer to search its sessions." The desktop already merges every enumerated host behind `aiVault:searchSessions`, so pass the scope straight through and stamp each hit with the host it came back on. Hosts the merge could not search are named under the results header with a short reason, since a silent partial answer reads as "no such session". (cherry picked from commit |
||
|
|
aee98ccaa0 |
fix(browser): make the browser identity one process-wide choice (#13822) (#20767)
* feat(browser): process-wide browser identity, chosen before ready
Electron resolves worker identity from a single process-global default, so two
coherent identities cannot coexist in one process. This makes clean/native one
app-wide decision read before `ready`, instead of a per-profile one that leaves
documents on one identity and every worker request on the other.
Both identities are load-bearing, measured across four origins at five reps:
the cleaned identity clears an embedded Turnstile widget and WhatsApp's browser
check where native is refused; native clears a full-page Cloudflare interstitial
that the cleaned identity never clears.
Base commit only: removing the per-profile field, its settings surface, and the
migration notice follow.
* test(browser): cover cross-context UA wire identity
* refactor(browser): make user agent identity app-wide
* test(browser): repair process identity wire fixture
* Fix browser identity startup migration failures
* WIP: rescue in-flight reduced-design work from a dead worker
Worker ctx_cb5b1262d7fe stopped ~2h ago mid-implementation (last heartbeat
2026-09-14T22:48:06Z) leaving this uncommitted. Committed unverified to make it
recoverable; not reviewed, not necessarily green.
* fix(browser): repair the rescued identity work so it typechecks
Finishes the interrupted edits in
|
||
|
|
7c4325457c |
fix: name the notification project by the worktree's own host (#20958)
* fix: name the notification project by the worktree's own host STA-4343: a worktree id is `repoId::path` with no host component, so the local host and an SSH host publish one id for two different workspaces. The id-keyed worktree map is first-wins and the repo map is last-wins, so a colliding workspace could be labelled with the other host's project and branch. Resolve the owning host first and name nothing when hydrated ownership cannot prove one; an omitted label beats a wrong one. Only the collision branch changes: a single-row id still takes the same map lookups it did before. * fix: name the project by its host when a repo id spans hosts A repo id is registered per host, so two hosts can hold one id at different paths. Their worktree ids are then unique, so the single-row path skipped host resolution and fell back to the id-keyed repo map, which is last-wins — a local worktree got the ssh project's name. Gate the repo lookup on the id actually being ambiguous: one owner keeps the plain map lookup and its cost, and only a spanning id resolves the owning host, naming nothing when ownership is unprovable. |
||
|
|
a01027697c |
feat(session-search): enable indexing on paired servers from a client (#20886)
* feat(session-search): add ranked history panel search and consent * test: wait for initial session indexing before refreshing results * feat(session-history): add local search settings and index controls * Use shared local host identifier for session index status * feat(session-search): merge all-computers search across hosts The `all` scope on `aiVault:searchSessions` now fans out from the desktop to every host the session list enumerates and merges the pages into one. Legs run in parallel: the local index through the search service, SSH and runtime hosts through the existing remote search client. Two fixed orders, because relevance scores from independent indexes are not comparable. `newest` asks every leg for recency and k-way merges on `updatedAt`, nulls last, ties broken on execution host id. `relevance` rotates hosts in host-id order by their own rank. The merged cursor is an opaque base64url payload holding each host's cursor, how many of its current page were already emitted, and the generation that offset counts into, plus the page size and sort the cursor belongs to. A host whose index moved is fenced to `stale` and stops contributing; the rest keep paging. Per-host outcomes ride back on one new optional `hosts` field on the results response. `aiVault:searchStatus` with `all` stays refused, and neither the runtime RPC nor the CLI gains the scope, so a fan-out is never two hops. * fix(preload): let the search bridge address the all-computers scope * feat(settings): live index status, enable confirm, advanced delete * feat(session-search): search every computer from the history panel The panel's "All computers" scope produced no request: the hook parsed the scope into a single host id and stopped when that was null, so the panel answered "Choose one computer to search its sessions." The desktop already merges every enumerated host behind `aiVault:searchSessions`, so pass the scope straight through and stamp each hit with the host it came back on. Hosts the merge could not search are named under the results header with a short reason, since a silent partial answer reads as "no such session". (cherry picked from commit |
||
|
|
8153ec2306 |
feat(session-search): search every computer from the history panel (#20885)
* feat(session-search): add ranked history panel search and consent
* test: wait for initial session indexing before refreshing results
* feat(session-history): add local search settings and index controls
* Use shared local host identifier for session index status
* feat(session-search): merge all-computers search across hosts
The `all` scope on `aiVault:searchSessions` now fans out from the desktop
to every host the session list enumerates and merges the pages into one.
Legs run in parallel: the local index through the search service, SSH and
runtime hosts through the existing remote search client.
Two fixed orders, because relevance scores from independent indexes are
not comparable. `newest` asks every leg for recency and k-way merges on
`updatedAt`, nulls last, ties broken on execution host id. `relevance`
rotates hosts in host-id order by their own rank.
The merged cursor is an opaque base64url payload holding each host's
cursor, how many of its current page were already emitted, and the
generation that offset counts into, plus the page size and sort the
cursor belongs to. A host whose index moved is fenced to `stale` and
stops contributing; the rest keep paging. Per-host outcomes ride back on
one new optional `hosts` field on the results response.
`aiVault:searchStatus` with `all` stays refused, and neither the runtime
RPC nor the CLI gains the scope, so a fan-out is never two hops.
* fix(preload): let the search bridge address the all-computers scope
* feat(settings): live index status, enable confirm, advanced delete
* feat(session-search): search every computer from the history panel
The panel's "All computers" scope produced no request: the hook parsed the
scope into a single host id and stopped when that was null, so the panel
answered "Choose one computer to search its sessions." The desktop already
merges every enumerated host behind `aiVault:searchSessions`, so pass the
scope straight through and stamp each hit with the host it came back on.
Hosts the merge could not search are named under the results header with a
short reason, since a silent partial answer reads as "no such session".
(cherry picked from commit
|
||
|
|
3631a1e77f |
feat(cli): show orca search now the settings toggle ships (#20677)
* feat(cli): orca search over the agent session index `orca search <query>` calls PR 5's `aiVault.searchSessions` over the CLI's existing runtime RPC, against the host `--environment` / `--pairing-code` selects and no other. `orca search --index-status` calls `aiVault.searchStatus`. It is the proof the contract works with no panel. Every flag maps onto a contract field and nothing else: `--scope`, `--fresh`, `--limit`, `--cursor`, repeatable `--agent` and `--path`, `--since`, `--sort`, `--debug`, `--json`. No fan-out, no merged output, no `--host`. One command rather than a `search status` subcommand: the query is a bare positional, so `orca search status` could not be told apart from searching for the word "status". `--status` is unavailable because `orchestration task-list --status <state>` already owns the name as a valued flag. No new runtime capability. PR 5 decided an explicit `method_not_found` refusal maps to `unavailable/no-service`, so reusing `createSessionSearchClient` gives an old host a plain "this host runs no session search service" answer at exit 0 instead of a raw JSON-RPC error. `CommandSpec.repeatableFlags` scopes repeatability per command, because `--agent` must repeat for search and stay single-valued for `worktree create`. `help.ts` sat exactly at max-lines, so `skills-command-flag-help.ts` becomes `command-scoped-flag-help.ts` carrying both tables at the same call-site size. * refactor(cli): drop the search type assertions main's casting gate now rejects Main gained a `consistent-type-assertions: never` scan in the changed-code gate after this branch was cut, and it reported twelve assertions in the new files. The four in the argument parser were avoidable. `readEnum` now keeps the value `find` returns, which already carries the narrow type, and the agent filter goes through an `isAiVaultAgent` predicate over a `Set<string>` instead of widening the agent tuple. The test now narrows the printed envelope by shape and re-reads the printed result through `AiVaultSearchResponseSchema`, so the JSON assertions are checked rather than claimed, and the flag table is typed so its callback needs no cast. One assertion is left, for the structural fake client, with the SAFETY rationale AGENTS.md requires. * fix(cli): sanitize host strings and scope pre-command repeatable flags Route every host-supplied string the search formatter prints through the escape stripper, and resolve the repeatable-flag set from the command tokens ahead when a flag sits before the command. * refactor(cli): resolve repeatable flag rules once per command * fix(cli): clarify session search availability and SSH scope * feat(cli): hide orca search until the settings toggle ships `orca search` stays dispatchable but leaves every discovery surface: root help, group help, unknown-command suggestions, and `agent-context --json`. `buildAgentContext` did not filter hidden specs, so it also stops leaking the hidden `terminal stop`. * feat(cli): show orca search now the settings toggle ships * docs(skills): teach the orca-cli guide the search command One section: what orca search covers, one host at a time, scope and narrowing flags, index status before searching, and that a human turns search on. * docs(skills): shape the search section like the other command sections |
||
|
|
46ed53b88a |
feat(session-search): merge all-computers search across hosts (#20670)
* feat(session-history): add local search settings and index controls * Use shared local host identifier for session index status * feat(session-search): merge all-computers search across hosts The `all` scope on `aiVault:searchSessions` now fans out from the desktop to every host the session list enumerates and merges the pages into one. Legs run in parallel: the local index through the search service, SSH and runtime hosts through the existing remote search client. Two fixed orders, because relevance scores from independent indexes are not comparable. `newest` asks every leg for recency and k-way merges on `updatedAt`, nulls last, ties broken on execution host id. `relevance` rotates hosts in host-id order by their own rank. The merged cursor is an opaque base64url payload holding each host's cursor, how many of its current page were already emitted, and the generation that offset counts into, plus the page size and sort the cursor belongs to. A host whose index moved is fenced to `stale` and stops contributing; the rest keep paging. Per-host outcomes ride back on one new optional `hosts` field on the results response. `aiVault:searchStatus` with `all` stays refused, and neither the runtime RPC nor the CLI gains the scope, so a fan-out is never two hops. * fix(preload): let the search bridge address the all-computers scope * feat(settings): live index status, enable confirm, advanced delete * fix(settings): let Button and Collapsible own their spacing and type |
||
|
|
f6197dbd36 |
fix(ai-vault): index Codex agent replies whose content blocks are typed Text (#20763)
Codex 0.153+ writes paginated rollouts whose completed agent messages carry content blocks typed `Text`. The transcript reader matched block types case-sensitively, so every assistant turn from those sessions was dropped before it reached the search index while user turns and tool output were kept. Match case-insensitively and bump the index schema so existing indexes are rebuilt with the replies present. |
||
|
|
0d2f7bcea3 |
fix(session-search): index OpenCode SQLite sessions (#20870)
* feat(session-search): index OpenCode SQLite sessions OpenCode sessions live in one SQLite database read on a worker thread, and the worker only ever answered with the newest few messages for the panel preview. The parser therefore published nothing over the transcript channel, so the search index wrote a placeholder row for every OpenCode candidate and no OpenCode message was ever searchable. Adds a `capture` request to the worker protocol that returns the session and every text part of every user/assistant turn from one open of the database. The agent parser asks for it whenever a sink is listening, so OpenCode joins the whole-document sources on the same path as Grok, Cursor and Gemini. The placeholder path (`parserPublishesMessages`, `noteUnreachableParser`) is gone; an OpenCode read that fails now fails like any other file. Bumps the index schema so existing indexes drop their placeholder rows, and adds `sessionsByAgent` to the index status, which is the count that made this bug visible. * test(session-search): assert every source speaks, not every agent OpenCode has two storage shapes, so asking only that some OpenCode session published messages was satisfied by the legacy JSON fixture while every SQLite session in the vault stayed silent. Assert per discovered source and keep the agent-coverage check beside it. * feat(session-search): capture OpenCode tool and reasoning parts Text parts alone left OpenCode behind every file-based provider: a command someone ran, what it printed, and the model's reasoning were all unsearchable. Widens the capture query to text, reasoning and tool parts. Reasoning folds into the turn's own words, the way the shared block list already treats a thinking block. Each tool part becomes one `tool` message carrying the call line and what came back, built with the same `toolCallText` every file provider uses; OpenCode's `filePath` is renamed to the `file_path` spelling that list knows, so a call is findable by its file argument. Adds a decoded-size ceiling beside the existing part ceiling. It is the bound a non-streaming source needs and a streaming one does not: a JSONL provider publishes each message as it reads it, while this one holds a whole session before posting it across the worker boundary. Neither ceiling truncates; both fail the read so it is retried and surfaces. * fix(ai-vault): fail an OpenCode capture it cannot read the message parts of `readOpenCodeSessionMessages` returned an empty list when the message-part schema probe failed. The sink-aware reader treats that as a complete read, so the consumer committed nothing and marked the source `current`: the session stayed out of the index with nothing on its row to say why and no retry. The part limit a few lines below already throws for exactly this reason, so the two now agree. The preview path is unchanged and still degrades to no messages, which is what a list read should do. Also throws from the fixture's `appendOpenCodeSqliteTurn` when the session id names no row, instead of falling back to the fixture epoch and appending orphan messages a test would then assert over. |
||
|
|
b997fcc77a |
fix(session-search): try phrase and AND routes for prose queries before OR (#20754)
* fix(session-search): try phrase and AND routes for prose queries before OR An exact sentence pasted out of a transcript was not returned. The route ladder only ran the phrase and AND rungs for a literal-looking query, so prose fell straight to OR, where the sentence's common words filled the candidate limit with recent sessions and the old session holding the sentence never reached ranking. The planner now carries a `phrase` candidate: the query's tokens in order with stop words kept, which is what the sentence is actually indexed as. The ladder runs phrase then AND over those tokens for every query of two or more tokens. A one-token query still takes the rung only when it looked literal. `incomplete` is reported by the rung that answered rather than accumulated across every rung tried. * fix(session-search): mark a snippet with the route that retrieved it A phrase hit was highlighted with the OR expression over the stop-word stripped terms, so an exact sentence rendered as scattered bold words with its stop words plain. The snippet now uses the expression the route matched by: one run for a phrase, every typed word for AND, the terms for OR. * fix(session-search): repair a prose phrase without dropping its stop words Typo repair re-planned the query from `plan.body`, which prose has already had its stop words removed from. `relay is droppng frames` therefore came back as the plan for `relay dropping frames`, and the phrase rung searched for a sentence nobody wrote: the transcript holds `relay is dropping frames`, so the exact match fell through to AND. The repair now maps over `plan.phrase`, the tokens as typed, and re-plans from those. Only terms the body holds are offered to the corrector, so a stop word is still never repaired, and the re-plan recomputes the body from the corrected sentence exactly as before. |
||
|
|
dbd750f64d |
fix(session-search): keep the index status honest while a sweep has a backlog (#20753)
* fix(session-search): keep the index status honest while a sweep has a backlog A pass stops reading at its wall-clock deadline and records nothing about the candidates it never opened, which is correct: being owed a read is a fact about the row, not an entry in a queue. But a candidate the opening sweep never reached has no row at all, so the store's `due` count cannot see it. The sweep still reported `completed`, the indexer stamped `lastSweepCompletedAt`, and `status()` answered `current` with a backlog of thousands: "Up to date - 130 files indexed", then 630, then more. The read loop now counts what it decided was owed and did not read and hands the number back as `left`; the pass propagates it; the indexer holds the last pass's count, adds it to `filesDue`, reports `indexing` while it is non-zero or a sweep is owed, and no longer stamps a sweep the deadline cut short as complete. * fix(session-search): only an unread backlog keeps the phase at indexing An armed cadence sweep on a drained index is not a backlog, so it no longer flashes the pane to indexing with nothing due. * fix(session-search): stop counting a deferred `due` row twice in filesDue `status()` reports `filesDue` as `stateCounts().due + left`. The read loop incremented `left` for every candidate the deadline cut off, including one whose row already said `due` — and that row is what `stateCounts().due` counts. A sweep that ran out of time therefore reported each already-due transcript twice. `left` now skips a deferred candidate whose row is already `due`. A candidate with no row, and a `current` row whose file moved, still count: those are the backlog no query can see, which is why `left` exists. |
||
|
|
a7e34d5695 |
feat(session-search): add panel search and opt-in consent (PR7) (#20580)
* feat(session-search): add ranked history panel search and consent * test: wait for initial session indexing before refreshing results * fix(lint): drop the type import #20898 left behind in the windowing test main's tip fails `typecheck` and `static analysis` on `NativeChatMessageList.windowing.test.tsx`: #20898 moved the growth/append suite into its own file and took the last use of `NativeChatMessage` with it, leaving the import. Every open PR reds both jobs through the merge ref, so this rides the first branch that has to merge main in. |
||
|
|
f85d2bf6ad |
fix(ai-vault): say OpenCode-in-WSL is not searchable from Windows yet (#20971)
The scan issue for an OpenCode database on a \\wsl.localhost share read as an error with an instruction the user cannot follow. It now surfaces on the Agent Session Search settings page under a computer row, where it belongs as a known limitation, so the WSL branch now reads 'OpenCode sessions inside WSL can't be searched from Windows yet.' Copy only: the issue keeps kind 'scope' and its path, every other branch is untouched, and discovery, the WSL gate, and the busy-timeout behavior are unchanged. |
||
|
|
3e5eb0329a |
feat(cli): orca search over the agent session index (#20514)
* feat(cli): orca search over the agent session index `orca search <query>` calls PR 5's `aiVault.searchSessions` over the CLI's existing runtime RPC, against the host `--environment` / `--pairing-code` selects and no other. `orca search --index-status` calls `aiVault.searchStatus`. It is the proof the contract works with no panel. Every flag maps onto a contract field and nothing else: `--scope`, `--fresh`, `--limit`, `--cursor`, repeatable `--agent` and `--path`, `--since`, `--sort`, `--debug`, `--json`. No fan-out, no merged output, no `--host`. One command rather than a `search status` subcommand: the query is a bare positional, so `orca search status` could not be told apart from searching for the word "status". `--status` is unavailable because `orchestration task-list --status <state>` already owns the name as a valued flag. No new runtime capability. PR 5 decided an explicit `method_not_found` refusal maps to `unavailable/no-service`, so reusing `createSessionSearchClient` gives an old host a plain "this host runs no session search service" answer at exit 0 instead of a raw JSON-RPC error. `CommandSpec.repeatableFlags` scopes repeatability per command, because `--agent` must repeat for search and stay single-valued for `worktree create`. `help.ts` sat exactly at max-lines, so `skills-command-flag-help.ts` becomes `command-scoped-flag-help.ts` carrying both tables at the same call-site size. * refactor(cli): drop the search type assertions main's casting gate now rejects Main gained a `consistent-type-assertions: never` scan in the changed-code gate after this branch was cut, and it reported twelve assertions in the new files. The four in the argument parser were avoidable. `readEnum` now keeps the value `find` returns, which already carries the narrow type, and the agent filter goes through an `isAiVaultAgent` predicate over a `Set<string>` instead of widening the agent tuple. The test now narrows the printed envelope by shape and re-reads the printed result through `AiVaultSearchResponseSchema`, so the JSON assertions are checked rather than claimed, and the flag table is typed so its callback needs no cast. One assertion is left, for the structural fake client, with the SAFETY rationale AGENTS.md requires. * fix(cli): sanitize host strings and scope pre-command repeatable flags Route every host-supplied string the search formatter prints through the escape stripper, and resolve the repeatable-flag set from the command tokens ahead when a flag sits before the command. * refactor(cli): resolve repeatable flag rules once per command * fix(cli): clarify session search availability and SSH scope * feat(cli): hide orca search until the settings toggle ships `orca search` stays dispatchable but leaves every discovery surface: root help, group help, unknown-command suggestions, and `agent-context --json`. `buildAgentContext` did not filter hidden specs, so it also stops leaking the hidden `terminal stop`. |
||
|
|
dec0e2cd56 |
feat(session-history): add local search settings and index controls (#20582)
* feat(session-history): add local search settings and index controls * Use shared local host identifier for session index status * feat(settings): live index status, enable confirm, advanced delete * fix(settings): let Button and Collapsible own their spacing and type |
||
|
|
291b4ddd6f |
feat(agent-status): route structured sessions through canonical ownership (#20718)
* feat(agent-status): route structured status through canonical ownership and fence child lifetimes Restacked onto the canonical store and child-work contract. Completing that restack drops the `reopenStructuredParent` mutation flag this change had carried, along with its contract field, its codec branch, and its single call site in structured ingest, which passed a hardcoded `true`. The flag was a narrow escape hatch from the absolute `tombstones.has(...)` rule that governed parent upserts in this branch's original base. The canonical store replaces that rule with a revision envelope, because a bounded store compacts tombstones away and a presence-based guard silently stops fencing once one is evicted. With the envelope deciding the outcome, the escape hatch has nothing left to escape from, so removing it changes no production behaviour. `agent-status-store-reopen.test.ts` is rewritten against the envelope: the reopen case now pins that an unflagged republication succeeds while replay from before the reopen stays fenced even after the parent tombstone is compacted away, and the second case pins where the guard genuinely bites — a republication inside the removing mutation itself, for every subject kind. * fix(agent-status): re-admit unchanged structured owners after teardown * fix(agent-status): clear anti-slop object-param and Reflect.apply findings - agent-status-store-byte-budget.ts: type the byte-budget helper's record parameter as the union of what its call sites actually pass (the snapshot header plus each store entity record) instead of the broad `object`. - server-structured-canonical-status.test.ts: replace `Reflect.apply` with a typed, explicitly-bound call that models a caller at an untyped boundary omitting the trusted owner subject. * docs(agent-status): drop the 2A progress doc from docs/reference docs/reference/ holds implementation detail, not rollout progress. The canonical-boundary notes move to the effort's working directory; the agent-status-store status section keeps the boundary statement and loses the now-dangling link. * fix(agent-status): mint the canonical epoch on first use, not at construction The hook server's canonical store was built in an instance-member initializer, so constructing AgentHookServer — which happens at import time for the module singleton — demanded a live randomUUID. Any importer that stubs node:crypto threw 'Invalid agent status store epoch' before a single test ran. The store is now created on first canonical access and reset by dropping it, so construction owes nothing to a crypto implementation and the epoch still rotates per authority incarnation. * fix(agent-status): drop the orphaned snapshot budget and a duplicated pane guard Two leftovers from the canonical-store routing change: agent-status-store-snapshot-budget.ts lost its only caller when the store state switched to agentStatusStoreFitsByteBudget. Nothing in the repo imports it now, so the module goes with the caller it existed for. The replacement is not a straight copy: it only memoises a record's measured size once the record is frozen, so a still-mutable record can no longer return a stale byte count. persistedStructuredWorkerPaneKeyIsValid repeated its public-pane-key rejection verbatim three lines below the first one. The tests covering that rejection pass on the first occurrence alone, so the second decided nothing and only obscured which predicate was load-bearing. * fix(agent-status): stop a failed structured publish from latching as owned Three defects found reviewing the structured routing path. combinedStatusEntries defaulted a missing listing order to 0, but the counter it compares against starts at 1, so any unordered row sorted above every ordered one. Unknown order now sorts last. The owner map recorded a session as owned before the sink ran. A publish that threw therefore left matchesLocation reporting an owned location for a row that was never written, and the unchanged-projection path — the only thing that would re-offer it — stopped. The address still has to survive a throw so teardown can forget a row that did land, so the two facts are now separate: the address is recorded up front, and only a publish that returned marks the row as landed. The reopen test claimed the revision envelope rather than the tombstone fences a stale replay. It cannot tell: transport consecutiveness, the parent-revision validator and the tombstone guard each refuse that replay alone, and ablating any two leaves the test green. It now asserts the outcome and says so. |
||
|
|
15cac68802 |
Native chat keeps scrolling to bottom (#20898)
* fix(native-chat): prevent auto-scroll when transcript is hidden Stop following new messages to bottom when the chat view is not visible (e.g., in an inactive tab). Restore scroll position when the transcript becomes visible again. * fix(native-chat): preserve reader scroll offset when transcript is revea When a reader scrolls away from the latest messages and the chat tab becomes hidden, save their scroll position. If messages arrive while the tab is hidden, don't auto-scroll. When the tab is revealed, restore the saved offset instead of jumping to latest, preserving their reading context across hide/reveal cycles. * refactor(native-chat): extract growth-append tests and status component Move transcript growth/append test suite to dedicated growth-windowing.test.tsx file for better organization. Extract status rendering logic (errors, retry, background tasks) from NativeChatStructuredSession into NativeChatStructuredSessionStatus. Fix scroll offset preservation in test harness when transcript visibility toggles. * refactor(native-chat): remove retry UI Remove unused retry functionality for failed message delivery from the native chat status component. The retryableOutboxEntry state is no longer managed, so the retry button and associated handling can be safely removed. |
||
|
|
07b7687a2e |
fix(sidebar): keep the Projects filter when a project is added (#20987)
Adding a project wiped the Projects filter: both reveal paths made the new project visible by clearing filterRepoIds outright, so a user filtered to A and B was dropped back to every project. The filter is an allow-list, so revealing a repo only needs that repo added to it. revealRepoInProjectFilter widens the selection instead, and no-ops while the filter is off, where adding an id would turn "show everything" into "show only this project". STA-7588 Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
d62328aa4d |
fix(codex): remove redundant Windows hook launcher for Unicode profiles (#20952)
* fix(codex): reuse the Windows hook shell for Unicode profile paths * test(codex): register Unicode hook tests in Windows CI * test(codex): pin trust hash replacement during Windows upgrade * test(codex): retry transient Windows teardown locks |
||
|
|
78609330d1 |
Fix browser viewport presets incorrectly scaled by UI zoom (#20962)
* Fix browser viewport presets scaled incorrectly by UI zoom Browser viewport presets must remain in window DIP (native) coordinates but scale in CSS pixels as UI zoom changes. Store preset dimensions as CSS variables in DIP, then divide by the live UI zoom factor in the stylesheet. Also consolidate zoom factor calculations across the app to use a shared `uiZoomFactorFromLevel()` function and add `windowDipToCssPx()` for converting native coordinates to CSS pixels. * Move viewport preset zoom compensation to CSS class Inline width/height styles outrank class rules, preventing the zoom compensation from applying. Using a class rule ensures the viewport scales correctly as the UI zoom factor changes. |
||
|
|
e39b432c40 |
fix(editor): preserve Markdown scroll after image layout (#20956)
* fix(editor): preserve markdown scroll after image layout * test(editor): harden scroll regression cleanup and geometry checks |
||
|
|
c702e77bc7 |
Stop reading the terminal arguments field on the structured chat route (#20944)
* fix(native-chat): stop reading the terminal arguments field on the structured chat route Setting Claude's Arguments to "--dangerously-skip-permissions --model Opus" made every new Claude tab open in the old terminal-backed chat instead of the new structured one, with nothing on screen to explain why. Removing "--model Opus" fixed it. The cause was a whole-string comparison: the configured arguments were checked against a single blessed value per agent, so any added token at all — including one the agent supports — stopped the string matching and the launch was demoted. Structured chat does not run the interactive CLI. It drives Claude through the Agent SDK and Codex through app-server, and those take narrower option sets that are versioned separately from the CLI's, so one free-text field cannot have a guaranteed meaning for all three. The structured route now reads only what it can actually honour: a replaced launch command, or a launch that names its own working directory. Terminal launches still apply the field exactly as before. Permission posture no longer travels as a raw flag. It is derived from the resolved launch arguments, which is the same fact a terminal launch acts on and which falls back to the default Orca ships when the field was never touched, so bypass stays on by default and Manual is still honoured. Claude gets the SDK's typed permissionMode and allowDangerouslySkipPermissions at query start; Codex gets its bypass flag placed before the app-server subcommand. Both are re-derived per acquisition beside the auth policy and environment overlay rather than stored in the session record, so nothing can disagree with the setting. Codex also loses the --profile, --add-dir and -c passthrough that reached app-server through that field. Only the permission posture comes back. * test(native-chat): pin routing authority on the narrowed feasibility input The routing-authority pin still named the old bundled blocker and built its "customized" fixture out of the arguments field, which is no longer a feasibility input. Both are now the launch command, and arguments and environment are customized on both passes of the loop, so the flag handed to the shared resolver tracks the command alone — a caller that resumed reading either one fails here. No case is dropped and no assertion is relaxed: the blocker list is still exhaustive and every caller must still honour a refusal from the shared resolver. |
||
|
|
e6930fdbe0 |
fix(runtime): keep the retraction boundary out of the receipt bound
Bounding the removal watermark alongside the receipt ledger reintroduced the defect the watermark exists to prevent: past 512 retracted worktrees, evicting a boundary readmits every pre-close frame it was fencing, and a delayed list resurrects the closed tab. A boundary is not a cache. One number per worktree ever retracted on an environment is the cheaper price, and environment teardown drains it; only the receipt ledger stays bounded, by frame age. Split the orphan-adoption port by provenance so the last writer that disagreed with the surface-standing rule stops disagreeing. `adoptRuntimeTerminalOrphans` replays the persisted binding when the claim already matches it and writes a new one otherwise, and both went through a single `recordSurface` that stamped the current graph sequence — so re-adopting an already-adopted orphan lifted a dropped pane's stale stamp and reported it attached, in a quiet workspace possibly forever. The replay now names the pane without standing and the fresh claim takes spawn standing, like every other writer. Replace a receipt-count assertion that was vacuous for a map keyed by environment and worktree with the mirror state and freshness it was standing in for. |
||
|
|
4948afc2ec |
fix(linear): show 'Cannot verify' when skill scan is inconclusive (#20964)
A scan that encounters an error before discovering skills, or hits an unreadable root, cannot vouch for "not installed". Previously these cases were conflated with proven absence, so the checklist would claim the skill step incomplete even when all three steps had been finished. Now the UI distinguishes between confirmed states and unknown ones, showing "Cannot verify" instead of listing the skill as an unfinished step. |
||
|
|
dddb6c9ad0 |
fix(runtime): let the TUI-owner recovery name its pane without claiming the graph holds it
`recoverStructuredTuiOwner` rebinds a recovered PTY from the persisted owner binding — the same replayed-evidence class as the inventory restore — but stamped it with the current graph sequence, so a pane the renderer had already dropped read as attached for one more statement. The guard below it needs the tabId and paneKey, not the standing. Also say plainly in `decideWebSessionTabsSnapshot` what the affirms check does and does not cover: an unpublished-worktree placeholder is withheld from epoch noting only. It still applies, because rejecting it outright would drop the terminal reconciliation that legitimately rides on it. |
||
|
|
d4f8345172 |
fix(runtime): stop an unpublished-worktree placeholder retiring the live publisher
A worktree the host has published nothing for still answers a forced list, with a synthesized `none`/v0 frame that means "ask me later" (host-session-snapshot-authority.ts). Every post-close list and every activation of an emptied worktree gets one. Noting it as a publication retired the renderer generation that is still live, and because that epoch is per-process, the terminal the user created next never reached this client — the same lockout the retraction path was already careful to avoid, through a door it did not cover. `local-structured-session-tabs-sync` already skips the placeholder for this exact reason; the web mirror now does too, on both the receipt ledger and the frame decision. Bound the receipt ledgers by frame age rather than entry count. One bootstrap inventory records a receipt per worktree under a single reserved frame, so evicting by insertion order dropped that batch's own earlier entries, and an absent receipt is what the recovery gate reads as "no evidence for this worktree". Only a receipt no in-flight frame can still be ranked against is droppable. Take the receipt gate off the `web-session-tabs-sync` barrel in the refresh path. Ordering is that path's gate, not an optional collaborator a caller's module mock may leave out, and being reachable only through the barrel is how the path came to have no ordering at all. |
||
|
|
feb04ec254 |
Virtualize automations run history table for large histories (#20916)
* refactor(automations): virtualize run history table - Add virtual scrolling to AutomationRunHistory for efficient rendering of large run lists - Implement sticky table header that stays visible during scroll - Update keyboard navigation and focus management for virtualized rows - Move AutomationRunsTable header inside scroll container for visual consistency - Add virtualizer-test-stub for testing virtual scroll behavior without DOM measurement - Cache DateTimeFormat to avoid per-cell allocation overhead * test(automations): add coverage for virtualized run table - Tests verify row content renders spend, tokens, and workspace labels correctly - Keyboard navigation guards prevent operations during failed host reads - Load-more pagination triggers at scroll end and respects page boundaries - New fixtures support flexible automation run and usage test scenarios * test(automations): verify scroll-to-focus path in virtualized runs - Implement scrollToIndex in virtualizer stub to move viewport window - Optimize row-size estimation to use predicate instead of labels - Test validates keyboard navigation scrolls rows into view before focus * add more tests |
||
|
|
b0d46e2d3d | fix(settings): preserve multiline proxy bypass rules (#20957) | ||
|
|
bfc297df99 |
fix(settings): keep integration connect dialog drafts on backdrop click (#20932)
* fix(settings): keep integration connect dialog drafts on backdrop click A backdrop click in the Settings → Integrations Jira/Linear/Bitbucket connect dialogs dismissed the Radix modal, and each dialog's reset-on-open then wiped the typed credential. Generalize SshTargetForm's dirty-gated outside dismissal into a shared preventOutsideDismissWhenDirty factory and wire it into the three dialogs (and SshTargetForm), so an accidental backdrop click no longer discards a draft while Escape / Cancel / × remain the explicit discard paths. Bitbucket compares email/baseUrl against a props-seeded baseline and only counts the active auth mode's fields, so a mid-edit status refresh and a mode toggle cannot make the form sticky. STA-7332 * test(e2e): drop ticket id from dismiss spec comment |
||
|
|
d8c1cbbe76 |
fix(runtime): order every session-tabs apply path against the retraction
A closed terminal came back on the other client because "this worktree was retracted" was neither durable nor universal: - `refreshWebRuntimeSessionTabsSnapshot` reached `decide` with no place in receipt order at all, so a list the host answered before the close applied after the retraction had already cleared the worktree. It is a production path for close, create, activation, split and PTY reconnect. - the boundary lived in a single receipt slot the next stream frame overwrote, and in a fence that only existed when a recovery happened to be pending when the retraction landed, so a pre-close list could out-rank the republication on `snapshotVersion` alone. Replace both with one raise-only removal watermark per (environment, worktree) and give the list path a receipt position, reserved by the request and carried in its answer so a dedupe joiner inherits it rather than minting a newer one. The pending-recovery fence and its bookkeeping are dead once the boundary is monotonic. The exact-match retirement check in the receipt ledger becomes the one lineage-aware predicate, so a `:headless-merge:` rebuild can no longer be noted as current and retire the live publisher out of its own worktree. On the main side, `recordPtyWorktree` stamped `surfaceRecordedAtGraphSequence` at write time, so any `paneKey` write claimed the standing of a fresh graph statement. The inventory restore in `terminal list` therefore un-dropped the very pane the read was meant to report, on every listing. A surface claim now carries no graph standing unless its writer names one: the graph statement, live leaf output and spawn do, while the inventory restore, the floating liveness restore and the mobile projection replay do not. Defaulting this way means a writer that says nothing fails safe and self-corrects, which the type alone could not guarantee across the projection contract's own `recordPty`. Spawn claims now span the one graph statement the renderer may already have in flight, and retirement proofs compare by identity instead of by position, so a re-delivered exit no longer fans out a `snapshotVersion` bump carrying nothing. |
||
|
|
9ed561c1d2 |
fix(claude): judge Stop against the turn the journal published (#20921)
* fix(claude): judge Stop against the turn the journal published A Stop could be refused for the turn the user was actually looking at. The client derives the id it sends from the published journal rows, but the host compared it against the adapter's own in-memory turn. The journal sink drains asynchronously, so that in-memory value can already name a turn whose row has not landed — an id no client has been shown, and one the client's Stop can therefore never match. The user pressed Stop and nothing stopped. Fix the guard's source rather than the guard. ownsRequestedTurn stays: it is what stops a delayed request from interrupting a later turn, and without it a stale Stop would reach a session-scoped interrupt that settles every queued send as durably rejected. The host now resolves the live turn from the journal projection and hands it to the adapter, which prefers it and falls back to its in-memory read for direct callers that have no journal. It is passed as a read rather than a value because the guard re-checks after the delivery fence may have waited seconds; a value captured at request time would interrupt whatever turn ran next. Both callers supply it. The handoff's own Stop bypasses performCancel, so its body moves into stopNativeHandoffTurn beside the file's other extracted flows, which is also what lets it be tested on its own. * fix(claude): fall back to the in-memory turn while the journal drains The journal drains through a serialized async queue, so a live turn routinely has no published row yet. Judging a Stop only against the journal refused in that window, which gates a user action on bookkeeping. The journal stays authoritative while it HAS an answer; a null read falls through to the in-memory turn, and the nothing-dispatched clause is unchanged. Both call sites now read the live turn through `journal.activeTurnId()`, which folds reduced items instead of rendering and sorting a whole snapshot. |
||
|
|
47bb473ec6 |
Remove agent map from dashboard popout (#20929)
The agent map view was not functional and its components have been removed entirely. The dashboard popout now only supports the kanban board view, with all map-related code, utilities, types, and translations cleaned up accordingly. |
||
|
|
96d77b37c5 |
perf: always show project names and remove notification scans (#20931)
* perf: avoid repeated agent scans when labeling notifications * perf: always label notifications and remove project counting * fix: qualify the notification project group by its folder's host Folder notifications resolved the folder host-aware, then looked its project group up by bare ID. The owner index fails a bare ID closed when two hosts publish the same group ID, so a remote folder lost the project name the catalog already had. Also drops the identity rescans that recovered display fields: the catalog finders now return the caller's row type, matching findIndexedRepoOwnerForHost. Updates the idle-arbitration expectation that still asserted the removed hasMultipleActiveRepos flag. |
||
|
|
357c9780f8 |
refactor(agent-launch): retire the duplicate worker-start mode decision (#20911)
`orchestration-worker-start-mode` becomes a thin adapter over
`agent-launch/agent-launch-mode`, which already owns the same decision.
Orchestration keeps its receipt vocabulary via WORKER_START_VOCABULARY, so
every sentence a dispatch receipt prints is unchanged.
Recovers the cutover written in
|
||
|
|
e6a41081a3 |
fix: name the id-kind mismatch when --ack is given a message id (#15743)
* fix: name the id-kind mismatch when --ack is given a message id
`orchestration check --ack` takes the batch's delivery id, which the check
response returns as the top-level `deliveryId`. Passing a message id
instead produced:
stale_delivery: Delivery msg_5d5cdf77614c does not belong to this Run.
That states a Run-scope verdict for what is really the wrong kind of
identifier, and it names no field that carries the right one. The only id
visible while reading the message list is the message `id`, so the message
sends the caller hunting the wrong axis — #15697 is a detailed report that
concluded the ack path was broken and no delivery id was exposed, when both
were fine.
When the value misses the deliveries table but hits the messages table, say
so and point at `deliveryId`. Anything else keeps the original wording,
including a delivery that exists but belongs to another Run — that one
really is a scope verdict.
Refs #15697
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test: bind message-id diagnostics to queued rows
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Neil <neil@stably.ai>
|
||
|
|
7f23d4463d |
refactor(daemon): consolidate snapshot-safe listener delivery (#20945)
Consolidate snapshot-safe daemon PTY listener delivery and exit payload construction. Reuse listener removal and event types while preserving callback ordering, payload isolation, optional exit fields, and recovery exception handling. Fixes #10984. Adapted from #11119. Co-authored-by: MumuTW <42820974+MumuTW@users.noreply.github.com> |
||
|
|
07c7606fee |
fix(computer-use): reap detached macOS helpers through owner exit (#20926)
Reclaim detached macOS computer-use helpers on abandoned requests and transports. Keep ownership from spawn, escalate SIGTERM to SIGKILL, force pending reaps when the sidecar exits, and clean each failed startup's private socket directory. Based on #14494 by @JuuuuHong. Preserve the original helper ownership/reaping design and regression tests while retaining the upstream line-buffer optimization and adding real-process teardown and resource-bound tests. Fixes #9141. Co-authored-by: JuuuuHong <juhang720@gmail.com> |
||
|
|
7ec2986fd1 |
fix(lint): merge the duplicate agent-status contract type imports (#20907)
main's tip fails audit:code-quality:native on import(no-duplicates), which reds the static analysis and verify jobs of every open PR via the merge ref. |
||
|
|
3520e8eb41 |
fix: highlight bash fences in Markdown source mode (#20592)
* fix: highlight bash fences in Markdown source mode * refactor: trim shell fence alias registration Drop the speculative exports and document the alias-resolution rationale in one WHY comment; the idempotency guard stays. --------- Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> |
||
|
|
a9232e8db6 |
fix(claude): single-own turn identity so Stop reaches a provider-opened turn (#20794)
* fix(claude): single-own turn identity so Stop reaches a provider-opened turn Stop silently failed on any Claude turn the provider opened on its own — a background task reporting in wakes the agent — once the session had dispatched at least once. The transcript read "The provider had already finished this turn." while the model kept working. Turn identity was minted twice from the same stream by two components that never talked. The journal translator writes turnId into the durable turn row, which is the id every client's Stop carries. settleWaiter separately wrote session.activeTurnId, only ever on the dispatch-echo path, and nothing cleared it. Cancel read the adapter's copy; prompt binding, status and both clients read the journal's. They agreed only when a send echo opened the turn. Turn identity is now single-owned. The open turn moves out of the translator's closure into ClaudeOpenTurn, which holds the turn and publishes its lifecycle row, so the id readers ask for is the id the row carries. activeTurnId and activeTurnSequence are deleted rather than widened, so the second writer goes with them instead of a second guard being added beside the first. activeTurnSequence was never turn identity: it asked whether a send was still awaiting its echo, which an interrupt would release as an unexpected turn. That is now derived from the live dispatch waiters. Deriving it also retires a latch — a retired waiter left the stored sequence permanently behind the dispatch sequence, refusing every later Stop for the life of the session. Also fixes the mirror defect the same hazard caused: a stale turn id was accepted against a newer provider-opened turn, because activeTurnId was never cleared when a turn ended. The Claude adapter fixture now acquires with a journal sink, as production does; without one it modelled a session that never ships. * fix(claude): reject stale stop after turn settles * fix(claude): preserve dispatch cancellation fence * test(claude): cover provider-opened stop integration * fix(claude): derive dispatch cancellation fence from journal * fix(claude): honor journal dispatch status before local sends * fix(native-chat): omit absent dispatch observation * fix(claude): release unresolved stop fence after deadline * fix(claude): bound and poll dispatch admission wait * test(claude): cover dispatch admission fast path |
||
|
|
6da72383df |
feat(agent-launch): one executor for agent launches, exposed as agent.launch (#19849)
* refactor(agent-launch): make the launch-mode decision surface-neutral
`decideWorkerStartMode` was the only shared answer to "structured chat session
or terminal agent?", but it lived in an orchestration-named module and spoke
orchestration's vocabulary, so the other launch surfaces could not call it.
Move the decision to `main/agent-launch/agent-launch-mode` unchanged and leave
`orchestration-worker-start-mode` as the adapter that supplies the noun.
A worker is not a special kind of launch; it is the same launch with a dispatch
attached. Naming the receipt's subject is the only thing orchestration actually
contributed, so that is the only thing the adapter keeps: "worker" in both
sentences, plus the `--terminal` wording, which reads as nonsense anywhere a
`--terminal` flag does not exist. Both are pinned, because they are asserted.
No behavior change. The receipts are byte-identical for every reachable case,
proven by running the new pin against both implementations.
Also pins the wording, which nothing was holding. The existing suites assert
`toContain` fragments ('terminal agent', 'cannot create') and the CLI suite
asserts a receipt handed to it by a mock rather than one this code produced;
all six files stayed green against a deliberately corrupted vocabulary. A
dispatch receipt is the only place a structured-to-terminal downgrade explains
itself, so the whole sentence is the contract, not a fragment of it.
* feat(agent-launch): add the launch intent and the one executor that runs it
The sequencing around the launch decision was duplicated per surface, and the
duplicate is where the bug lives. A new worktree was created agent-first, so
its startup terminal WAS the agent and the structured branch below it could
never be reached — every new-worktree launch was a PTY regardless of the user's
default. Orchestration fixed that for itself in #19431; mobile and the CLI
still have it.
`executeAgentLaunch` inverts the order once, for everyone. When the preference
is structured the worktree is created with NO startup agent, the executing host
is then asked whether it can host a session for the workspace that now exists,
and only then is a surface created. The host verdict cannot be hoisted above
creation: `agentSession.createSupport` only answers for a workspace it can
resolve, which is why the decision stays in two halves.
Agent-first creation is deliberately preserved for PTY launches — it is what
sequences the agent's startup command behind the setup runner, so wait-for-setup
comes for free there.
What actually differs per surface is only how a surface is built (an
orchestration worker's session takes a dispatch hold and a mailbox a plain
launch must not take), so that is injected as a factory rather than branched on.
The intent also strips the reserved agent fields from a migrated create payload:
a caller moving off `worktree.create` passes its existing params, and a stale
`startupAgent` in there would re-create the very path this replaces.
Tests assert order and arguments, not just the resulting mode. Reintroducing
agent-first creation reddens 4 of 11.
* feat(agent-launch): expose the launch executor as the agent.launch RPC
Adds `agent.launch` — one host-side method that decides structured-vs-terminal and
creates the surface — wired to the real runtime factories: `createManagedWorktree`
for the workspace, forking on `startupAgent` exactly as the orchestration worker
path does; `createStructuredAgentSessionForWorktree` for a chat session; and
`createTerminal` for a PTY agent. Allowlisted for mobile, which is the surface the
routing gap was reported on.
`worktree.create` is untouched. Its `startupAgent` keeps meaning "spawn a PTY agent"
verbatim, because it answers with `agentTerminalHandle` only on that path: a host
that quietly routed it to a structured session would hand every older client a
response with no handle and no error. All new behaviour sits behind
`agent.launch.v1`, which the host now advertises and a remote client must negotiate,
so a client that does not gets today's behaviour unchanged.
* fix(agent-launch): drop the deleted draft-prompt blocker from the reason map
main removed the draft-prompt blocker in #19681 (a structured session now holds
an unsent draft), so the exhaustive Record no longer typechecks.
* chore(agent-launch): carry a SAFETY rationale on the agent placement cast
The type-assertion gate landed after this branch's base, so the new file's
copy of the worker-start cast is now a changed-code finding.
* chore(agent-launch): carry agent.launch through main's RPC typing and casting gates
The typed-method contract, the generated params catalog and the
`assertionStyle: never` casting scan all landed after this branch's base.
- AGENT_LAUNCH_METHODS kept an `RpcMethod[]` annotation, which widened its
method name to `string` and broke assignability; every sibling infers instead.
- `agent.launch` binds a schema under src/main, so it joins the catalog's
RPC_METHODS_WITHOUT_SHARED_PARAMS and the parity gate's hand-listed twin.
- The now-typed methods make most test casts unnecessary; the few that remain
carry the line-specific SAFETY rationale the casting gate requires.
* docs(agent-launch): stop the receipt-wording comment claiming a migration
The decision was never moved out of orchestration-worker-start-mode; this PR
adds a second copy beside it. Say so, and name the unenforced agreement.
* docs(agent-launch): stop the executor comment claiming a migration that has not happened
The header asserted two things the tree does not support: that every launch
surface routes through the executor, and that the mode decision "already lived"
in `agent-launch-mode`. `agent.launch` is the executor's only consumer, and
`orchestration-worker-start-mode.ts` is byte-identical (blob
|
||
|
|
13ba649c22 |
fix(terminal): let a runtime-created Windows terminal BE the requested shell (#20825)
* fix(terminal): let a runtime-created Windows terminal BE the requested shell
`orca terminal create --environment <windows-host> --command 'cmd.exe'` never
created a cmd terminal. `--command` is text the provider TYPES into whatever
shell it spawned, so the PTY stayed the host's default shell with cmd running
inside it. Captured on `awin`, whose default is Git Bash:
$ orca terminal create --environment awin --command 'cmd.exe' --json
$ orca terminal send --environment awin --terminal term_10656cf7... \
--text exit --enter
$ orca terminal read --environment awin --terminal term_10656cf7... --screen
neil@awin MINGW64 ~/orca/orca ((30f820708f...))
$ cmd.exe
Microsoft Windows [Version 10.0.26200.9445]
C:\Users\neil\orca\orca>exit
neil@awin MINGW64 ~/orca/orca ((30f820708f...))
$
The handle is alive the whole time and `terminal list` shows one healthy
terminal, because the PTY never changed — so the only symptom is that the
caller's terminal is now a shell it never asked for, and every later `send` is
quoted for the wrong one. On `win-lowspec` (default pwsh) the same create lands
cmd inside PowerShell.
Root cause
----------
There are two spawn preflights and they are twins:
- `src/main/ipc/pty/ipc/spawn-preflight.ts` — renderer/IPC spawns, i.e. a
terminal tab opened in the app.
- `src/main/ipc/pty/runtime/spawn-preflight.ts` — runtime spawns: the CLI's
`terminal.create`, headless `orca serve`, and every paired remote
environment.
Only the IPC twin read the caller's requested shell. The runtime twin passed a
literal `requestedShellOverride: undefined`, so a runtime-created terminal on
Windows could only ever be the host default. Everything downstream of that
point — `spawn-options`, the daemon, `resolvePtyShellOverride` in the relay,
`local-pty-launch-plan` — already honoured `shellOverride`; nothing upstream
could supply one.
Change
------
- Thread `shellOverride` through the runtime lane: `RuntimePtySpawnArgs` ->
runtime `spawn-preflight` -> `RuntimePtyController.spawn` ->
`TerminalCreateOptions` -> the `terminal.create` RPC's new `shell` param ->
`orca terminal create --shell`.
- Thread it through the renderer-backed lane too (`createDesktopTerminal` ->
`terminal:requestTabCreate` -> `store.createTab`), so `--shell --focus` is not
silently dropped on a local Windows app.
- An agent launch quotes its startup command for the shell it will actually run
in, so a requested shell now owns the startup-shell family instead of the
global `terminalWindowsShell` setting.
- Lift the relay's `ALLOWED_WINDOWS_SHELL_OVERRIDES` into
`isSupportedWindowsShellOverride` in `src/shared/windows-terminal-shell.ts`
(membership unchanged) so the CLI, the zod param schema, and the relay refuse
the same names. `--shell` therefore cannot carry a path or a command line into
`pty.spawn`; only allowlisted bare shell names pass.
- Gate on `TERMINAL_CREATE_SHELL_SELECTION_RUNTIME_CAPABILITY`. An older host
strips the unknown `shell` param and answers with a healthy terminal running
its default shell — a reply indistinguishable from success — so the CLI
refuses before creating anything rather than creating the wrong shell quietly.
`--shell` stays Windows-only; macOS and Linux hosts spawn the login shell and
the relay drops the value off win32 rather than honouring it half-way. A WSL
project runtime still outranks it, unchanged.
Tests
-----
- `pty-spawn-shell-override-parity.test.ts` pins both preflights against the
exact drift that caused this (verified failing with the fix reverted).
- `createTerminal` passes `shellOverride` to `ptyController.spawn` with no
startup command.
- CLI: sends `shell`, refuses a shell the host cannot spawn, and refuses a host
without the capability — in both refusals without making the round trip.
- Allowlist and `terminal.create` schema accept/refuse cases, including paths
and appended arguments.
* fix(terminal): refuse a requested shell the execution host cannot apply
The first commit made `--shell` reach the spawn, but only a LOCAL win32
execution host applies it: `spawn-options` gates the override on
`process.platform === 'win32' && !args.connectionId`. So `--shell cmd.exe`
against an SSH-routed worktree, or against a macOS/Linux host, still returned a
healthy terminal running that host's default shell — the same
indistinguishable-from-success reply the capability gate exists to prevent, one
layer down.
Refuse instead, before anything spawns. The check sits at the top of
`resolveAgentTerminalCreateOptions`, which every create lane funnels through, so
neither lane has to remember it; the desktop lane additionally refuses a
worktree-less create, which has no execution host to resolve a shell on.
An SSH host's platform and installed shells are not visible to this runtime, and
a POSIX host has no Windows shell to pick. Neither can honour the request, and
saying so is the whole point of the flag.
Docs and the CLI spec now say "refused", not "ignored".
* fix(terminal): refuse a shell that contradicts the project execution runtime
`resolveLocalWindowsTerminalRuntimeOptions` does not merely rank the project's
execution runtime above a per-terminal pick -- it REWRITES the pick, in both
directions, and says nothing:
- a WSL project forces `wsl.exe`, discarding `--shell cmd.exe`;
- a Windows-host project discards a WSL name and falls back to `COMSPEC`
(`getHostShellForProjectRuntime`), so `--shell wsl.exe` spawns cmd. That is
the common case, not an edge: `resolveProjectExecutionRuntime` resolves
`windows-host` for every project that is not WSL, while a repo belonging to no
project honours `wsl.exe` -- so the same flag behaved differently depending on
whether the repo was in a project.
Either rewrite returns a healthy terminal running a shell the caller did not ask
for, which is the failure `--shell` exists to remove.
It also split an agent launch's quoting from the shell that receives it. The
previous commit made the startup-shell family follow the REQUESTED shell, so
`--shell wsl.exe --command codex` on a Windows-host project typed POSIX-quoted
launch args into cmd. Refusing the contradiction removes that case rather than
papering over it.
Refuse instead, alongside the SSH and non-Windows refusals, from the same
`resolveAgentTerminalCreateOptions` seam every create lane funnels through.
Also from review:
- the allowlist test looped the list against itself; spell the members out.
- the runtime spec case claimed to prove the pty's shell when it asserts the
controller received the field; name it for what it checks.
Reported by an adversarial review of the branch.
* fix(terminal): canonicalize --shell and refuse a WSL-path rewrite
Review of the --shell create path turned up two ways the terminal could
still end up being a shell the caller never asked for -- the exact failure
--shell exists to remove.
Bare and mixed-case spellings passed the allowlist but reached consumers
that exact-match the canonical name: resolveWindowsShellStartupFamily
classified `cmd` as the PowerShell family, resolveWindowsShellLaunchArgs
fell through to empty shellArgs (no `chcp 65001`, no OSC 133 bootstrap that
Windows foreground status depends on), and resolveWindowsGitBashShellPath
compares case-sensitively so `Git-Bash` spawned a literal `Git-Bash`.
The allowlist is now one canonical-name map and terminal.create canonicalizes
on parse, so the spawn path only ever sees `.exe` spellings. `pwsh` and
`powershell` stay distinct binaries.
A `\\wsl$\<distro>\...` cwd made the providers force wsl.exe regardless of
the request, and terminalShellOverrideRefusal only inspected the project
runtime -- undefined for a folder workspace with no project. Refuse on the
resolved cwd and the workspace path, judging what the PTY actually gets.
Also: the capability gate reported an unreachable host as too old rather
than unavailable; the SSH CLI shim dropped capabilities from status, so
--shell there blamed the host version instead of naming SSH; and --shell
had no help entry, rendering bare in `orca terminal create --help`. Adding
that entry crossed help.ts's max-lines cap, so the flag table moved to
flag-help-text.ts rather than suppressing the rule.
Adds a behavioural test for the runtime preflight (the one-line fix was
pinned only by a source-text scan), plus coverage for the startup-command
quoting family, the no-workspace refusal, and the WSL-path refusal.
* fix(build): keep tests out of the RPC params catalog bundle
The catalog walk under methods/ already skips *.test.ts, but the contract
directory glob took every .ts. terminal-create-shell-param.test.ts is the
first test to live there, so the bundle pulled vitest into a CJS build and
the generator threw on require(). Same exclusion, same reason.
|
||
|
|
0325f1a22e |
feat(agent-status): add the canonical store and child-work contract (#20717)
* feat(agent-status): add canonical shared store contract * fix(agent-status): harden canonical store invariants * fix(agent-status): close canonical store race windows |