mirror of
https://github.com/stablyai/orca.git
synced 2026-10-26 16:02:42 +00:00
a4d69d7b45db41e4eab498b261afb29ccb4464ce
501
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
caef20fec8 |
fix(gitlab): paginate TaskPage issues beyond 50 (#13538)
Co-authored-by: Neil <neil@stably.ai> |
||
|
|
fc8c981103 | fix(browser-preview): enforce canonical runtime grants (#16975) | ||
|
|
52ade074a9 |
Display host on automation details and dialog (#16823)
* Show automation host in details and support moving between hosts - Rename AutomationCreateDestinationField to AutomationDestinationField to reflect dual use in create and edit modes - Add host display to automation detail view, showing storage authority - Allow editing automations to move them to different hosts within same authority; project list filters to available projects on chosen host - Update copy from create-only terminology to mode-agnostic wording * Display automation host and support cross-authority moves Users can now move automations to different storage authorities. The destination picker shows all available hosts, and selecting a new one displays a warning about the move. The save creates the automation on the destination and deletes it from the source; if deletion fails, both copies remain and the user is notified. * Remove cross-authority move support for automations An automation's authority (the Orca instance that stores and schedules it) cannot change; edits now only offer hosts within the same authority and move logic is removed entirely. This simplifies the destination picker and removes move-specific UI messaging. * Fix undefined selectedRowKey in automation host recovery Replace references to the undefined selectedRowKey variable with selectedRow?.key to properly access the row's key when recovering automation runs across hosts. * Support moving automations across execution authorities Allows users to move automations between different authorities (desktop ↔ runtime environments) during editing. A save to a different authority creates the automation on the destination and deletes the original with its run history. Includes clear messaging about the move operation, proper handling of workspace id resets, and graceful error handling when deletion fails. Supports destination-aware project and worktree fetching. * Reuse creationKey across move retries when schedule changes When retrying a failed automation move, dtstart is minted fresh each attempt, changing the payload. Previously, operationKey included the full payload, so retries would mint new creationKeys and risk duplicate automations on the destination if the initial create failed in transport. Now key only by the move (source + destination) to ensure stable creationKey across retries. Also fix workspace auto-selection to use authority-scoped worktrees instead of the merged cache, preventing unwanted restoration of source-host workspaces after switching authorities. * Rename `note` to `moveWarning` for automation host moves Clarifies that the field specifically warns when an automation would move to another host, replacing the plain storage line. |
||
|
|
2ac23c29be |
Add parent worktree selection when creating workspace (#15420)
* feat: select parent worktree for nesting when creating workspace Enable users to pick a parent workspace in the composer's Advanced drawer, organizing newly created worktrees hierarchically in the sidebar. The backend validates lineage relationships and gracefully retries without the parent if it becomes unavailable during creation. * feat: select parent worktree for nesting when creating workspace - Record app-picked parents as manual actions, not CLI-flag equivalents, ensuring the same user action carries consistent cleanup semantics across hosts - Gracefully retry without parent if the selection goes stale, warn the user instead of failing - Refactor create into modules: parent resolution, payload building, state merge * Allow selecting parent worktree when creating workspace - New parent worktree picker filtered by execution host and project - Preserve concurrent local writes to lineage by comparing per-record state instead of key-set membership * Allow selecting parent worktree when creating workspace - Rename "Parent workspace" label to "Parent worktree" - Filter candidates by execution host and project to prevent nesting across hosts - Wire parentWorktreeId through composer state and creation request pipeline - Update translations and add new copy for nesting-related messages |
||
|
|
2aec442f42 | Split Linear issue service by operation (#16770) | ||
|
|
7ee8b5e1a6 | Refactor lower max-lines modules (#16760) | ||
|
|
6ba6d58cd2 |
fix(orchestration): route @agent messages by resolved identity, not terminal title (#16237)
* feat(agent-status): add the pane agent identity resolver
Four ladders answer "which agent is in this pane" independently — the tab icon, the
open-tab/search occupant, the sidebar title rows, and the sidebar hook-row fallback — and they
disagree. Two consult the terminal title before the launch record, so a string Orca parsed
outranks a fact Orca owns.
resolvePaneAgentIdentity is the single ranked answer. Two rules, one of which is not an ordering:
1. Evidence is ranked by how directly it observes the process; a display title is last.
2. Each observation carries the runId of the agent run it describes. Evidence from a superseded
run is INELIGIBLE, not merely outranked.
Rule 2 is the part reordering could never supply. A completed hook naming A plus a title naming
B is either a bug (hook right, title stale) or a legitimate pane reclaim (title right) —
identical signals, opposite correct answers. Run ids make them different facts: in the bug both
belong to the current run; in the reclaim the hook belongs to a previous one. That pair ships as
a test asserting the two produce opposite answers from the same evidence.
Missing run ids are treated as eligible. Absence means "this peer does not publish them", not
"this is stale", so an old host's rows are never blanked. Sibling evidence is opt-in so
pane-scoped consumers cannot inherit another pane's agent.
No consumer imports this yet; each migrates separately with its own evidence.
Verified non-vacuous: reversing the authority order fails 10 of 18 assertions and removing the
run filter fails 3.
* fix(agent-status): close three resolver contract holes found in review
**Duplicate evidence of one source resolved by array order.** `eligible.find(...)` returned the
first match, so two live hooks naming different agents were settled by input position — the exact
property this resolver exists to remove. The original order-independence test only used DISTINCT
sources, so it never exercised it. Conflicting same-class evidence now returns null with
`ambiguousAt`, and does NOT fall through to a weaker source: letting a title answer whenever two
hooks disagree is worse than saying nothing.
**A bare numeric runId collided across authority restarts.** `incarnation` is a total order only
within one `authorityId` (agent-status-observation.ts states this), and the id is regenerated per
authority instance, so a restarted host counting from its own floor would report `1` and match an
unrelated live run 1. The run key now carries its authority, and evidence from a DIFFERENT
authority is treated as incomparable — kept, like an absent key — rather than as stale.
**Title stayed reachable by consumers that authorize writes.** Ranking it last makes misuse
unlikely; `minimumSource` makes it impossible. An action consumer passes `'launch'` and weaker
evidence is dropped before ranking, so routing or delivery cannot name a target from a parsed
string even by reordering its inputs. Display surfaces omit it and are unaffected.
Also restores the generic agent-vocabulary parameter, which lives on the routing branch and was
lost when this branch was rebased.
Each fix is mutation-verified: first-match restored fails 3, ignoring authority fails 1, dropping
the floor fails 2. The authority test was itself vacuous on the first attempt — both sides used
`incarnation: 1`, so a resolver ignoring authority still passed on the numeric compare. It now uses
differing incarnations.
The remaining review finding, that `process > launch` has no freshness bound, is NOT fixed here:
it needs an observation timestamp the evidence type does not yet carry. Recorded rather than
silently dropped.
* fix(orchestration): route @agent messages by resolved identity, not terminal title
`@claude` picked its recipients with `buildAgentNameRe('claude').test(title)`, so any pane whose
TITLE contained the word received Claude's messages. Terminal titles carry task text, and people
describe agent work in them, so this is the ordinary case rather than a contrived one: the
recorded title "Switch Claude and Codex off the load balancer… - grok" is a Grok pane that
received both @claude and @codex. Misdelivered instructions, not a cosmetic slip.
The cause is that `RuntimeTerminalSummary` carried no identity at all — `title` was the only
identity-ish field on it, so routing by title was the only option available. Fix the input:
- `RuntimeTerminalSummary.agentIdentity?: TuiAgent` — optional, host-resolved from launch and
foreground-process evidence the host owns, with the title ranked last and contributing only
when the evidence parser finds an unambiguous name. A title that merely mentions an agent
yields no evidence, which is the whole point.
- `resolvePublishedPaneAgentIdentity` in `src/shared` rather than inside the runtime class, so
the decision is testable without a runtime and so routing, delivery and the UI cannot drift.
- Groups match `agentIdentity`; the title matcher and its bespoke Cursor predicate are deleted.
Unknown fails closed. `agentIdentity` is absent when the host predates the field or had no
evidence beyond the title, and delivery is an action: not delivering is visible and recoverable
(the sender sees no recipients), while delivering to the wrong agent is neither. The optional
field is additive, so an old client simply ignores it (wire rule 1).
This is also the first real caller of the evidence parser and the identity resolver.
Tests: 27 in groups, 8 for the publisher, 3 RPC fan-out cases updated to the new contract. The
`@cursor`-must-not-match-"text cursor blink" hazard is now excluded structurally instead of by a
per-agent predicate.
Verified non-vacuous by mutation: swapping the process/title ranks fails 2 publisher assertions,
and reverting groups to title matching fails 15 of 27. One earlier mutation silently failed to
apply after formatting reflowed the block — the file was checked before trusting the result.
* perf(runtime): reuse terminal title during summary build
* fix(orchestration): refuse title evidence when publishing identity for routing
Rebuilt on current main so this carries the hardened parser from #16148 and the corrected
resolver from #16157 (authority-scoped run keys, no order-dependent duplicate resolution).
Applies the resolver's new `minimumSource` floor at the publisher. What this publishes authorizes
an action — routing decides which real agent pane receives a message — so ranking title last is
not enough; the floor removes it from consideration entirely, and no amount of reordering by a
caller can bring it back.
The trade, stated because it is a real capability loss: a hook-less agent over SSH that Orca did
not launch, and whose foreground process the host cannot read, is no longer addressable by @agent.
Accepted because a message delivered into the wrong agent's prompt is unrecoverable while an
undelivered one is visible — the sender sees zero recipients. Whether real panes actually carry
launch/foreground evidence is the open question, and is what live validation must answer.
* fix(pty): preserve agent identity on daemon reattach
* fix(runtime): retire stale pane agent identity
* chore: normalize runtime types formatting
* fix(agent-status): identify a pane from its own hook, not from how it was started
Two defects, one cause: identity was inferred from the outside instead of read from the agent.
**Hook evidence was never plumbed in.** The publisher considered `process`, `launch` and `title`
and contained zero hook references — while the resolver ranks `live-hook` first. The top rung of
the ladder was never connected.
That made identity depend on Orca having launched the agent. Most agents are started by typing
`claude` or `codex` at a shell, which leaves no launch record. On macOS the foreground process
still names them, so the gap was invisible. On WSL the Windows host reads the foreground process
as `wsl.exe` — the distro wrapper, not the agent inside it — so those panes had no signal at all
and became unaddressable by `@agent`.
A hook is the agent reporting itself, so it survives both: no launch record needed, and no
dependency on reading a process across the WSL boundary.
**`launch` outranked `completed-hook`.** Ranking is now by TENSE rather than by how authoritative
a source sounds:
present: live-hook > process
past: completed-hook > launch > sleeping-session > sibling > title
A launch record is an event, not a state — it stays true after the agent exits, which is why a
pane reused after closing its agent kept reading as the old one. A completed hook at least proves
the agent actually ran in that pane; a launch record only proves Orca tried to start one.
Neither rank was covered: all 392 existing tests passed unchanged after reordering. Mutation now
fails 2 on the old order and 4 with hook evidence removed.
Known remaining gap, deliberately not papered over: a hand-started WSL agent with no managed hooks
has no identity signal at all. Restoring a title guess there would reinstate the misdelivery this
PR exists to prevent.
* fix(orchestration): restore title as the last resort, not a forbidden source
An earlier revision passed `minimumSource: 'launch'` so routing could not see a title at any rank,
reasoning that a display string must never authorize a write. That conflated the evidence parser
with the raw substring match it replaced.
`buildAgentNameRe('claude').test(title)` was the misdelivery. `collectAgentTitleEvidence` returns
null on exactly those shapes: "Review the Claude session-history fix" on a Codex pane yields
nothing, and "Switch Claude and Codex off the load balancer… - grok" yields grok from its owner
suffix. Ranking title last is therefore sufficient; refusing it is not necessary.
Refusing it had a real cost. An agent a user starts by hand inside an Orca WSL terminal has no
launch record, no readable foreground process (the Windows host sees `wsl.exe`, not the agent in
the distro), and — until managed Codex hooks install there — no hook either. An unambiguous title
was the only thing left, and dropping it made that pane unaddressable by @agent where the previous
code could reach it. That is a regression, and most agents are started that way.
End-to-end coverage added at the routing layer with title allowed: @claude still does not reach a
Codex pane whose task text names Claude, @codex still does not reach a Grok pane whose task text
names Codex, and a pane identified only by an unambiguous title is reachable again.
* revert(agent-status): keep launch above completed-hook until run keys exist
Reverts the tense-based reorder from this branch. The reasoning behind it was sound as far as it
went — a launch record is a past event, not an observation, which is why a reused pane kept reading
as its previous agent — but it fixed one staleness by opening a worse one.
A completed hook is past tense too, and without an agent-run key it never expires at all. Ranking
it above `launch` lets a stale hook from a previous agent outrank the launch record Orca stamped
for the process running NOW. pane-agent-owner.ts already says this in its own comment: "Ranking
launch/live-hook above the completed/sleeping records keeps a genuine pane on its real agent and
stops a stale record from hijacking it."
The reorder belongs with authority-scoped run generation, which is what makes any past-tense
evidence expire. It is staged in the migration plan rather than shipped here.
What this branch keeps: hook evidence feeding pane identity (so an agent a user starts by hand is
identified from its own report rather than needing a launch record), and title restored as a
genuine last resort behind the evidence parser.
* fix(runtime): guard the pane key so terminal.list survives a non-UUID leaf
`makePaneKey` throws on a leaf id that is not a UUID. The hook-evidence lookup called it unguarded
inside `buildTerminalSummary`, so a single such leaf took down `terminal.list` for the whole list
rather than degrading that one pane — 136 tests across 5 files, and the native code-quality gate
tripped separately on a duplicate test title.
Both were mine, and both were caught by CI rather than by me: I ran the focused suites before
pushing instead of the affected directories.
* fix(runtime): declare published terminal agent identity
* fix(runtime): demote completed hook identity evidence
|
||
|
|
419e3b4496 |
Fix terminal reads that flatten composer drafts into output (#16711)
* fix(terminal): separate composer drafts from read output Rendered screen reads treated cursor-line suggestion overlays as PTY output. Detect composer-owned text from cell attributes and cursor context, remove it from tail, and expose it as structured draft metadata. * fix(terminal): handle wrapped composer overlays * fix(terminal): preserve draft wrapping and tail alignment * fix(terminal): preserve composer wrap boundaries * fix(terminal): preserve draft continuations with middle dots * fix(terminal): recognize configurable Codex status lines |
||
|
|
913509edeb |
fix(orchestration): prevent slow worker-start stalls (#16300)
* Extend orchestration agent submission timing budgets * fix(orchestration): preserve mutation recovery identity * fix(orchestration): preserve recovery executable identity * fix(orchestration): keep worker starts and recovery commands safe * test(orchestration): cover federated worker preflight * fix(orchestration): harden mutation recovery * fix(orchestration): redact dispatch recovery credentials * chore: preserve upstream skill dialog formatting * test(orchestration): stabilize agent prompt submit e2e * fix(orchestration): validate federated start receipts * perf(runtime): cache unchanged prompt verification tail * fix(orchestration): reject worker-start timer overflow * fix(orchestration): normalize worker-start timeout defaults * fix(orchestration): normalize worker-start readiness budgets * fix(orchestration): normalize federated readiness timeout * test(runtime): tolerate current-main degradation exports * chore: preserve current-main orcad formatting * chore: drop unrelated formatting carryover |
||
|
|
3558cf943f |
fix(codex): heal WSL hooks before typed launches (#16535)
* fix(codex): heal WSL hooks before typed launches * test(codex): keep launcher fixture type-safe on Windows * fix(build): list codex-home-wsl-env in the CLI typecheck project `managed-home-shell-preflight.ts` is already in the CLI project's include list and now imports `wslCodexRuntimeHomeForGuestHome` from `src/main/pty/codex-home-wsl-env.ts`, which the list did not cover — TS6307, so the CLI typecheck failed on every push. Added the single module rather than a `src/main/pty/**` glob: it is a 31-line leaf with no imports of its own, so it does not widen what the CLI bundle can reach. * fix(codex): converge the two WSL hook install lanes onto one writer Two independent readiness reviews agreed the Orca-terminal boundary holds, but Codex Sol found a P1 the other rated P2: the new just-in-time repair raced the existing relay installer and the two produced DIFFERENT hook and trust representations for the same managed home. Two unserialized writers emitting different formats is worse than the bug this PR fixes, because it fails intermittently rather than cleanly — a pane works or does not depending on which lane won. - Relay Codex installs now delegate to the runtime-home writer, so there is one canonical representation instead of two. Redirected scripts use the runtime path, the readable wrapper, and the prepended group. - `installForRuntimeHomeSerialized` puts every asynchronous WSL caller for a given home on one queue (`wslInstallQueues`), so concurrent panes cannot interleave writes. Also rewrites the stale pin test the new `-x` guard broke. It asserted the defect — "would run the impostor if the preflight carried an unqualified command name", expecting the hijack marker to exist. The guard is a security improvement, so the test now asserts the contract: an unqualified preflight is skipped and the marker is never written. Rewritten to the new behavior, not loosened or deleted. 818 tests pass across the affected suites; typecheck clean. The changed-file quality gate could not run locally — its pnpm engine-warning JSON parser fails under Node 26 — so CI covers it. The boundary both reviews verified is untouched: paired/relay/mobile clients stay hard-blocked from the RPC, params remain shape-locked to the managed home suffix with traversal rejection, nothing is written outside the managed home, and macOS/Linux stay inert. * fix(codex): serialize resolved WSL hook homes * fix(codex): recover managed WSL homes after restart * fix(wsl): translate Codex preflight through WSLENV * fix(cli): cover bounded WSL Codex repair * fix(codex): coalesce duplicate WSL hook repairs * fix(codex): verify reconstructed WSL homes |
||
|
|
0f522c35e5 | fix(remote): gate empty session inventory on host authority (#16546) | ||
|
|
d60a3c900b | Reset stale terminal modes after dead TUI replay (#16379) | ||
|
|
68d5b9206e |
fix(agent-prompt): stop reporting delivered prompts as stalled (#16095) (#16590)
* fix(agent-prompt): stop reporting delivered prompts as stalled (#16095)
Enter is written before verification runs, so `agent_prompt_stalled` can only
ever mean "turn start not observed" — never "prompt not delivered". Three of the
verifier's blind spots made that misreading routine, and the coordinator then
treated it as non-delivery and pasted the whole preamble a second time into a
worker already running it.
- Accept a hook-reported `working` recorded after the baseline. Hook rows reach
the runtime through getAgentStatusSnapshot with no window involved, unlike the
synthetic-title route that feeds workingSequence (suppressed for codex, absent
for kimi, and gated on window visibility for everyone else).
- Accept pane output after Enter when the agent was already working: a
`->working` edge is unreachable there, so the old predicate could never be
satisfied by a follow-up prompt. An idle agent still owes a real turn start,
so a swallowed Enter stays detectable.
- Give codex/kimi panes a longer effect window; their only turn-start proof is
an out-of-process hook round-trip, not a TUI repaint.
- Coordinator dispatch no longer fails (and re-dispatches) a task whose prompt
stalled; the dispatch stays active with its capability intact so the worker's
own report settles it.
* fix(orchestration): let a worker's own report correct an unobserved prompt (#16095)
Follow-up to
|
||
|
|
08a447dfb2 |
fix(terminal): size the pre-Enter wait to what the host actually ingests (#15925) (#16586)
* fix(terminal): size the pre-Enter wait to what the host actually ingests The Windows agent-prompt submit delay was a flat 1_500 ms frozen from the client's process.platform at import. Measured on two real Win11 hosts, ConPTY ingests a bracketed paste linearly at ~0.009-0.010 ms/byte, so the constant was both far too long for a 2-8 KB prompt (14-89 ms of real cost) and too short past ~145 KB — at 160 KB one host took 1_499 ms, meaning Enter landed mid-paste, exactly the corruption the delay exists to prevent, up to the 16 MB input ceiling. Replace it with getTerminalPasteIngestMs(platform, byteLength) and derive every pre-Enter wait from it: - open-loop fallback = 500 ms settle + ingest bound, uncapped - claude/codex render gate cannot start its quiet window before the ingest bound elapses (an agent that repaints mid-ingest could otherwise satisfy marker-then-quiet while ConPTY was still feeding the paste), and its 8 s hard cap now sits on top of the ingest bound instead of standing in for it - the plain terminal.send suffix path, which had an undocumented flat 500 ms The rate follows the host that owns the pty transport, not the client: a WSL pane is spawned as wsl.exe behind the Windows pseudoconsole so it still pays ConPTY, while an SSH pane follows the relay's reported remotePlatform. Also swap the inter-chunk setTimeout(0) for setImmediate. It cost a full ~15 ms Windows timer tick per 16 KiB chunk (~0.95 s/MB) while pacing ~1.07 MB/s — 11x above ConPTY's drain rate — so it never provided backpressure; the event-loop yield it did provide is preserved. * fix(terminal): stop double-charging paste ingest in the render gate The render gate's hard cap is armed twice -- once at arm() and again when the show-cursor marker arrives -- but it re-added the whole ingest window each time while the ingest clock itself runs once from gate construction. A marker seen mid-ingest pushed the cap out by a second full ingest term (~34 s instead of ~24 s for a 1 MB prompt on ConPTY). Capture the ingest deadline absolutely and arm with what is left of it. Also thread the request AbortSignal through terminal.send so the now payload-scaled suffix wait can be cancelled: at 16 MB it runs ~262 s, well past the CLI's 60 s request budget, and previously nothing stopped the eventual Enter. Cleanups: a pty record's connectionId is only ever an SSH target id, so the wsl: relay-id guard in getPtyWriteHostPlatform was dead; and hoisting action.text removes both non-null assertions in writeTerminalAction. |
||
|
|
9135b6f004 |
feat(orchestration): surface nested worker depth and propagate it across hosts (#16669)
* feat(orchestration): surface nested worker depth and propagate it across hosts Builds on the depth enforcement in the previous commit, which shipped with the setting reachable only by editing settings.json and with workers never told they could nest. Adds the Settings -> Agents control (a 1/2/3 select rather than a free-form number, which bounds the value without inventing a numeric input primitive). The key stays absent from the SettingsUpdate RPC schema, matching agentSkillSharingEnabled: settings.update is reachable from the CLI, so an RPC-writable depth would let a worker raise its own cap. Adds a SUB-DISPATCH block to the dispatch preamble, emitted only when the worker actually has budget left. A worker told it "usually cannot" delegate still tries and then reports the refusal as a blocker, so the section is omitted entirely rather than softened. Propagates depth to federated worker hosts. Previously the home side computed and stored a depth the remote host never received, so a remote attachment always read as depth 1. That is correct at the default cap and wrong as soon as the cap is raised — precisely when someone starts relying on nesting. The field is optional, so an older Run home simply omits it and the attachment's NOT NULL DEFAULT 1 keeps the fail-closed behaviour. Enforcement still runs on the executing host against that host's own cap, consistent with the SSH execution boundary. * fix(orchestration): close nested depth readiness gaps * fix(settings): defer nested depth translations * fix(orchestration): drop federated depth keys that main already landed The enforcement PR's review pass added the same federated depth propagation before it merged, so replaying this branch onto main produced duplicate object keys. Keep main's versions -- its schema entry validates an integer >= 1 rather than any finite number. * fix(settings): label nested worker depth select * fix(settings): move nested depth to orchestration * fix(settings): refine nested depth placement |
||
|
|
8a07bbd8cf |
fix(orchestration): enforce nested worker depth instead of an accidental fence (#16668)
* fix(orchestration): enforce nested worker depth instead of an accidental fence Orca documented that "dispatched workers cannot spawn their own sub-workers (worker-start is coordinator-fenced)". No such check existed. What existed was a single Run-binding check in the workerStart RPC: a worker's terminal is not bound to a Run, so worker-start happened to fail. The rule was emergent, asserted by no test, and written in no doc — and it leaked. A worker could run-create its own Run, task-create, and worker-start: now bound, the check passed. Replace it with a real, configurable depth cap. Depth is derived from the caller's own active Dispatch rather than from Run binding, which is what dissolves the run-create bypass: creating a Run does not stop you being a worker. Enforcement lives in a single dispatch-row writer that owns all three INSERTs that mint a live worker — the generic claim, the supervised worker-start path (including every retry), and the remote attachment. Two of those were missed by earlier drafts of this change, so `creator` and `maxDepth` are required parameters: a new spawn path cannot compile without deciding, and a boundary test refuses the SQL anywhere else. Schema v30 adds depth to dispatch_contexts and remote_dispatch_attachments, NOT NULL DEFAULT 1 and backfilled to 1 so an unstamped or pre-upgrade row fails closed rather than reading as a root coordinator. The attachment pane indexes widen to the five states in which a remote worker may still be running: loss of contact is not evidence of process death, so an unverifiable worker still counts as a nesting parent. Also adds the caller-evidence assertion that workerStart was the only Run-scoped verb to skip, so a declared --from cannot name another terminal's pane and inherit its depth. Default is 1, so behaviour is unchanged unless the new setting is raised. Two limitations are deliberate and documented rather than papered over: this is a guardrail and not a security boundary, since a caller whose launch evidence is unverifiable (any ordinary restored terminal) can declare another handle; and it is enforced at supervised dispatch creation, so a settled worker whose process is still alive counts as a root again. * fix(orchestration): share caller resolution and pin worker gaps * refactor(orchestration): make the caller resolver's pane contract explicit Overloads so requireStablePane callers get a non-null string instead of casting, and rename the attestation opt-out to say what it means: the caller asserts it itself. A flag called assertEvidence:false reads as "attestation optional", which is the hole this helper exists to close. * fix(orchestration): propagate dispatch depth to federated workers * chore(cli): refresh bundled orchestration guide |
||
|
|
cda2280d63 |
Show all automations (#16532)
* Add all-host automations with scoped ownership and multi-authority suppo
Enable automations to run on multiple hosts (SSH targets and local) with
owner-fenced mutations, scoped list queries per host, and conflict
resolution. Introduces desktop and runtime authorities as distinct
automation storage owners, with per-host caching, invalidation, and
retry scheduling on the renderer. Captures registration generations for
SSH hosts to survive re-adoption. Adds CLI support for destination
selection and conflict recovery.
* Filter automation create projects by destination host
Only offer projects available on the selected destination, preventing
the mismatches that would fail at submit time. Auto-adjust the project
selection if it becomes unavailable when the destination changes.
* Add runtime storage authority support for automations
- Support both runtime and desktop as automation storage authorities
- Make owner preconditions optional for legacy-client compatibility
- Cache automation list projections to improve performance
- Add per-row repo/worktree resolution for cross-authority collisions
- Extend automation.list RPC to always include owner metadata
* Replace child_process.execFile with runProcess for external automations
- Migrate external-manager to use cross-platform runProcess wrapper per child-process safety policy
- Abstract electron app/ipcMain APIs in orca-runtime via environment accessors
- Install fake app environment in automation tests for consistent setup
- Reorganize imports to use specific module paths (ssh-target-registry, agent-detection, browser-error)
- Remove external-manager from child-process import allowlists (no longer violates direct import)
* Unify desktop automation CRUD onto the local runtime RPC surface
The desktop authority now speaks the same automation.* RPC contract as
remote runtimes, via callRuntimeRpc({kind:'local'}) -> runtime:call ->
the shared RpcDispatcher. The automations:list/listRuns/create/update/
delete/runNow IPC arms, their preload members, and every renderer
desktop-vs-runtime transport fork are retired; the runtime methods are
the single implementation of scoped lists, owner fencing, and change
publication for both transports (mobile clients already exercised them).
The desktop probe scheduler's priority lease survives the move as an
AutomationService hook the IPC registration installs and the runtime
methods take, so Orca's own automation traffic still parks queued
external-manager probes.
External-manager scope arms and dispatch-loop plumbing stay on IPC by
design; automation change events keep their existing channels (renderer
ingestion already converges them by authority).
* Remove automation ghost SSH tombstone scanning
This functionality for synthesizing tombstones for automation-referenced SSH
targets is no longer needed as part of the automation system refactoring.
* Refuse orphan automations at dispatch time, not migration time
Remove migration-time disabling of orphan automations and the `enabledDecidedBy` field. Dispatch now refuses orphans at runtime instead, simplifying state management and UI. Orphans are left unstamped and enabled; dispatch refuses to run them via `resolveAutomationRunTarget`.
* Show all automations in flat table with unified filter menu
- Replace host picker component with comprehensive Filters menu supporting status, last run, agent, and host filters
- Flatten automation list layout to single table instead of host-grouped sections
- Add Host column to display execution host for each automation
- Display active filters as removable pills below toolbar
- Delete unused AutomationHostPicker* components
* Add automation owner fencing and destination validation
- New AUTOMATION_OWNER_FENCING_RUNTIME_CAPABILITY for owner preconditions; legacy clients get owner metadata snapshotted at RPC boundary for compatibility
- Editor captures and revalidates automation destination before save, preventing silent retargeting if SSH infrastructure changes mid-edit
- SSH target types now isolate renderer-authored fields; generation is server-owned and stripped by IPC handlers
* Route automation recovery actions to the origin host
When an automation action fails due to owner fencing, recovery verbs
("Update server", "Reconnect") must run on the host where the refusal
originated: the row's captured owner for row operations, or the
destination the create dialog captured, not the list's filtered host.
* Remove external manager scope limitation notices
Consolidate create destination eligibility checks with a unified predicate
and fix the bug where desktop repo IDs could be sent to runtime hosts where
they cannot resolve.
* Persist only store-derived automation contexts, not client-perspective o
Store contexts must never be based on client-provided runContext or sourceContext
values—clients speak a different perspective (e.g., 'runtime:<id>' for host IDs
they assign), and persisting those makes the store projection orphan automations
it actually owns. Derived contexts now take precedence in create and update paths,
with explicit null still honored to clear a value. Tests verify this by simulating
drift after storage and confirming that moves re-derive while toggles preserve.
|
||
|
|
a9781a4118 |
STA-4150: client-hosted remote browser (consolidated) (#15448)
Co-authored-by: Jinwoo-H <jinwoo@stably.ai> |
||
|
|
98bdd653ab |
fix(native-chat): stop the spinner on a not-yet-flushed transcript (#16493)
* fix(native-chat): stop the spinner on a not-yet-flushed transcript A brand-new agent session can take minutes to write its first JSONL line, and one that is never prompted never writes it at all. The host emitted no stream frame until the file resolved, so every native-chat client sat on a bare spinner with the composer enabled but the transcript blank -- forever, in the never-prompted case. The resolve poll now reports the transcript as pending after a short grace, and both host handlers emit a `pending: true` snapshot. It is deliberately not a plain empty snapshot: an empty window sold as a settled read would capture over retained history and unblock consumers that require a trustworthy transcript (the launch-draft adoption would re-offer a prompt the agent may already have taken). Clients render it as the "start a chat" empty state while keeping the read unsettled -- `awaiting-transcript` on mobile, an `awaiting` read phase on desktop, which also stops the seed loop expiring into an error card for a session that is simply new. New optional field only, so older clients ignore it and still stop spinning. * fix(native-chat): negotiate pending transcript frames |
||
|
|
e361da7fb7 |
Deleting skill (#16357)
* Add skill deletion with cross-platform transaction safety Implements end-to-end skill removal with placement enumeration, dependency guards, and transactional recovery. Covers native, WSL, and remote hosts; users can delete canonical directories and alias placements (symlinked directories or files) in a single atomic batch. Includes UI selection flow, preview, confirmation, and results band. Block reasons (bundled, plugin, unowned, stale) gate deletions that would fail or contradict user intent. * Organize IPC handlers into module subdirectories Move register-core-handlers and skill-delete-ipc-handlers into dedicated subdirectories for improved code organization and to reduce the flat structure in src/main/ipc/. * Make skill deletion recovery transactions idempotent Defer journal cleanup until both staging removal and receipt cleanup succeed, leaving the journal in place for startup to retry if either operation fails. This ensures the recovery process is safe to run multiple times without leaving partially-deleted skills. * Consolidate skill-delete files into dedicated module Reorganize skill deletion functionality into a modular structure under `src/main/skills/skill-delete/` with simplified file names. Remove the redundant `skill-delete-` prefix from file names since they now live in the dedicated directory. Update all import paths throughout the codebase to reflect the new structure, including imports from IPC handlers and RPC methods. * Fix broken import paths and add deletion robustness improvements Import paths using `..//'` were invalid and broken. Replace with explicit module names (`skill-discovery-sources`, `skill-install-filesystem`, etc.) to clarify dependencies. - Bind WSL filesystem methods to preserve `this` context - Keep recovery journal when rollback rename fails, so startup can retry - Skip symlink-based tests on Windows where they cannot run - Only treat ENOENT/ENOTDIR as empty directories; propagate other errors - Fix cross-platform path parent calculation to handle drive roots - Replace shared constant with localized string for user-facing message - Use `runProcess` for WSL integration test instead of bare `execFile` * Add batch limit for skill deletion and improve host availability checkin - Limit concurrent deletions to prevent remote host overload - Add retry logic for capability probing to handle transient unavailability - Add reprobe() method to recheck capability after errors or user refresh - Fix status logic: receipt cleanup is best-effort, completion depends only on content removal - Improve error message for unreachable hosts |
||
|
|
deae7212d9 |
fix(orchestration): derive inject's agent guidance from the recognized-agent roster (#15874)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: vam <a@a.com> |
||
|
|
09048c63d4 |
feat(orcad): add headless browser providers (#16193)
* feat(orcad): add headless browser providers * fix(orcad): merge the duplicate runtime-browser type import |
||
|
|
1c436a8084 | refactor(rpc): split terminal methods into cohesive modules (#16188) | ||
|
|
1a7934c54f |
Fix deleted remote worktree reappearing due to host ID mismatch (#16039)
* Fix deleted remote worktree reappearing due to host ID mismatch Paired clients and servers may use different spellings for execution hosts (e.g., client 'runtime:env-1' vs server 'local'). Resolve these spellings before worktree.rm and worktree.forceDeleteBranch to prevent failures and orphaned worktrees. * Validate runtime kind before comparing environment IDs Ensure parsed host IDs are actually runtime environments before accessing their environmentId property. Fixes incorrect host ID matching during worktree cleanup that caused deleted remote worktrees to reappear. * Use hostId directly when same-ID surviving host exists When a surviving host has the same ID as the deleted worktree's original host, use the hostId directly instead of qualifying it through the runtime call host. This prevents worktrees from reappearing due to host ID mismatch. --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
43461fca46 |
fix(ai-vault): replace scanner internals with actionable panel copy (#16229)
* fix(ai-vault): replace scanner internals with actionable panel copy Agent Session History painted the scanner's supervision errors verbatim: "AI Vault service restart circuit is open." and "AI Vault service timed out after 130000ms." Neither tells a user what happened or what to do. Add a shared mapper that rewrites the supervision family into copy tied to an action, passing anything unrecognized through so scanner-authored messages (host name, remote path, cap) keep their own wording. It also strips Electron's `Error invoking remote method` wrapper, which this path never handled. Applied at both surfaces the panel paints: the local leg's scan-issue row and the thrown-rejection banner. Humanizing in main covers remote clients too; the raw text moves to the main log. Also make "Refresh to try again" true. The relay path lets a forced refresh reopen the restart circuit, but on the local path `force` stopped at the cache layer and never reached the supervisor, so the refresh button was inert for the full 60s fault window. The client file sat at 299/300 lines, so extract the child-listener and init-frame wiring into `attachAiVaultServiceChild`, next to the ready-waiter and retirement helpers it belongs with, rather than bumping the max-lines ceiling. * fix(ai-vault): humanize runtime scanner errors * fix(ai-vault): preserve runtime error metadata * fix(ai-vault): normalize wrapped scan errors * fix(ai-vault): preserve non-scanner relay errors * fix(ai-vault): make forced retries cancel backoff |
||
|
|
c618ec7393 | test(reliability): protect recent P0 regression invariants (#16163) | ||
|
|
f975035809 |
refactor(ipc): split preflight and SSH registry out of the ipcMain modules (#15927)
* refactor(preflight): split agent detection out of the ipcMain registration
First of the IPC extractions the revised design requires. `src/main/ipc/preflight.ts`
mixed 285 lines of agent/tool detection with 35 lines of `ipcMain.handle`
registration, and the runtime calls that detection during normal operation
(`orca-runtime.ts:573`, plus the preflight RPC methods). So the runtime dragged
`ipcMain` into its graph to reach pure logic.
Detection moves to `src/main/preflight/agent-detection.ts` — named for what it
contains, per AGENTS.md. `ipc/preflight.ts` keeps only the handler registration and
re-exports the domain module so existing importers are unaffected. The runtime and
its RPC methods now import the domain module directly.
Ratchet baseline 36 → 35: `src/main/ipc/preflight.ts` is no longer reachable from
the runtime. The gate detected the improvement and refused to pass until the
baseline tightened, which is the behaviour it was built for.
Verified: 2 files / 1,187 tests pass across every suite touching preflight;
`pnpm typecheck` clean; `oxlint` clean.
* refactor(ssh): split the SSH target registry out of the ipcMain module
Second IPC extraction, and by far the biggest win: this removes **eight** modules
from the runtime's Electron graph, taking the ratchet baseline 35 → 27.
The runtime needed five thin accessors from `src/main/ipc/ssh.ts` —
`connectRegisteredSshTarget`, `getRegisteredSshState`, `listRegisteredSshTargets`,
`listRegisteredRemovedSshTargetLabels`, `getActiveMultiplexer`. Each is a one-line
read over module-level state. Importing them dragged in `ipcMain`, `powerMonitor`
and a `BrowserWindow` accessor — and, transitively, `ipc/pty.ts` (8,031 lines),
`ssh-browse`, `ssh-passphrase`, `ssh-relay-deploy`, `ssh-remote-cli-host-passthrough`,
`wsl-hook-relay-launch` and `user-data-path`.
`src/main/ssh/ssh-target-registry.ts` now holds that state plus its accessors.
`registerSshHandlers` populates it; the runtime reads it. The indirection is kept
deliberately: SSH providers register after construction and may reconnect, so
callers must resolve the current generation rather than freeze one.
`ipc/ssh.ts` re-exports all five, so non-test importers are unaffected.
`connectRegisteredSshTarget` still throws `ssh_handlers_not_registered` when no
handler layer registered — a headless host must fail loudly rather than report a
target as unreachable, which would read as `exited` (see ssh-execution-boundary.md).
Verified: 9 files / 59 tests across the ssh, automations and trust-preset suites;
orca-runtime.test.ts 1,183 pass; `pnpm typecheck` clean; `oxlint` clean.
* refactor(host): resolve the app root through the port in fork-reachable modules
`parcel-watcher-entry-path.ts` and `session-scanner-service-entry-path.ts` read the
app root via `require('electron').app` inside a try/catch that already returns null
when Electron is absent. They were therefore correct under plain Node at runtime and
only failed the *static* text check — which is real, not pedantic: the comment in
`ports/port-scan-command-client.ts:19` records that the plain-node-entry-guard fails
on that literal text, try/catch or not.
`hasAppEnvironment() ? getAppEnvironment() : null` gives the identical "no app root
here" answer without the text. That restores `hasAppEnvironment`, which an earlier
commit in this stack deleted as unused — it now has the caller it was waiting for.
Ratchet baseline 27 → 25.
Verified: 74 files / 458 tests; `pnpm typecheck` clean; `oxlint` clean.
* test(ssh): mock the SSH target registry alongside the ipc/ssh mock
Thirty-eight suites mocked `vi.mock('./ssh')` for `getActiveMultiplexer`. That
factory went inert when production started importing the accessor from
`../ssh/ssh-target-registry`, so the real module loaded and the assertions drifted.
Adds a companion registry mock returning the same stub, plus a
`sshTargetRegistryModuleMock` builder beside the existing `sshModuleMock` so the
shared harness stays one place. No assertion changed.
Found by a full-suite run: the targeted ssh/runtime suites were green while
30 tests in ipc/worktrees and ipc/repos were not.
* refactor(runtime): read app paths and the packaged flag through the port
`orca-runtime.ts` is the last module in its own graph that imports `electron`
directly. Nineteen of its uses were `app.getPath` (12) and `app.isPackaged` (7) —
exactly what the AppEnvironment port already covers.
Also removes a dead `const { app } = require('electron')` inside
`getOrchestrationDb`. It was left unused once the path came from the port, and it
is precisely the dynamic-require pattern `plain-node-entry-guard.ts` exists to
catch, sitting in the runtime's own constructor path.
What still binds `orca-runtime.ts` to Electron is now three sites, not nineteen:
`new Notification(...)` (one), `BrowserWindow.fromId` (one), and the
`ipcMain.on('terminal:tabCreateReply')` renderer round-trip — which is the browser
tab path, and the same one that would hang a headless host for ten seconds.
Two suites drove `electronMocks.app.isPackaged` directly; they now install a fake
AppEnvironment reading the same mutable field, so their per-test toggles work
unchanged and no assertion moved.
Verified: 376 files / 4,717 tests across src/main/runtime; typecheck and oxlint clean.
* test(serve): add the built-artifact terminal round-trip acceptance smoke
"The server started" proves almost nothing. Terminal creation dispatches into
OrcaRuntimeService, and without an installed headless PTY controller that path
falls through to a renderer reply that never arrives and times out after ten
seconds. A boot probe, a port bind, and a `host.platform` call all pass against a
server whose terminals are dead — which is exactly the gap the design doc's own
boot proof was retracted for.
This boots the BUILT `out/main/index.js --serve`, parses its ready payload, pairs a
real client over the advertised endpoint, lists worktrees, creates a terminal, runs
a command through the PTY, asserts the output comes back, and asserts clean
shutdown. It drives nothing but the public pairing + RPC surface, so the same
script is the acceptance gate a future Node-only backend must pass unchanged.
The sentinel invokes `process.execPath` rather than `echo`, because the shell
differs per platform and node does not.
Verified both directions: passes against the real server, and fails with an
actionable message when the command produces no output — a smoke that cannot fail
is worthless.
* fix(ssh): fail loudly when the multiplexer resolver was never installed
`getActiveMultiplexer` resolves through a resolver that `ipc/ssh.ts` installs at
module scope. A process that never loads the SSH layer — which is the whole point
of the Node-only backend — would get `undefined` from every call.
`undefined` already means something specific here: "not connected". So a missing
resolver and a disconnected target were indistinguishable, and a host with no SSH
layer would quietly report every target as not connected. That is the
unverifiable-reported-as-exited conflation `docs/reference/ssh-execution-boundary.md`
exists to prevent — the doc is explicit that absence of contact is never evidence
of absence of the thing.
A missing resolver is a wiring error, not a connection state, so it throws, matching
what `connectRegisteredSshTarget` already does for unregistered handlers.
Verified: 432 files / 4,759 tests across ipc, ssh, preflight, automations and trust
presets; typecheck and oxlint clean.
* refactor(pty): stop faking a BrowserWindow for the headless PTY path
`registerHeadlessPtyRuntime` passed `registerPtyHandlers` a stub object cast to
`BrowserWindow` whose `isDestroyed()` returned true and whose `webContents.send`
was a no-op — a window-shaped thing that lied about being a window, purely to
satisfy the type. Adversarial review named it as the same "looks fine, silently
returns a lie" pattern this codebase rejects elsewhere, and it is the shape that
keeps `electron` on a path that otherwise needs none.
`registerPtyHandlers` now takes `BrowserWindow | null`. An absent renderer is
semantically identical to a destroyed one — all 42 call sites already guarded on
`isDestroyed()` and skipped — so `src/main/ipc/pty-renderer-surface.ts` states that
directly: `isRendererGone`, `sendToRenderer`, `rendererWebContents`. The compound
`isDestroyed() || webContents.isDestroyed()` guards collapse into one predicate.
`isPtyWriteEventFromMainWindow` becomes null-tolerant and fails closed: with no
renderer no sender can legitimately match, so every write is rejected. Those
handlers cannot fire headless today, but failing closed is the right answer if that
ever changes.
This is the precondition for installing a PTY controller without Electron, which is
what a Node-only backend needs and what `terminal.create` actually calls.
Verified: 129 files / 2,473 tests across ipc/pty, providers and orca-runtime; the
built-artifact acceptance smoke still passes end-to-end (boot → pair →
terminal.create → sentinel → close), which is the check that matters most here
since this changes the headless PTY path itself; typecheck and oxlint clean.
* refactor(pty): read app paths and the packaged flag through the port
Follows the fake-window removal. `ipc/pty.ts` had nine `app.*` reads — all
`getPath`, `getVersion` or `isPackaged` — which the AppEnvironment port already
covers. The `BrowserWindow` import was also dead after the null-window change.
What still binds this file to Electron is now `ipcMain` (75 uses, all handler
registration) and `powerMonitor` (2). That is a clean statement of the remaining
job: split logic from registration, the same shape already applied to preflight
and the SSH registry.
Test wiring: the shared `pty-ipc-suite-environment` beforeEach installs a fake
AppEnvironment that reads through the existing `vi.mock('electron')` app object
rather than freezing values — suites toggle `app.isPackaged` mid-test to exercise
dev-mode spawn paths, so the port has to observe the same mutable field. One edit
in the shared harness covers every pty suite.
Verified: 128 files / 1,290 tests across ipc/pty and providers; the built-artifact
acceptance smoke passes; typecheck and oxlint clean; ratchet unchanged at 25.
* refactor(pty): inject the ipcMain surface so the PTY module loads without Electron
This closes the round-3 blocker: "the doc never says how orcad installs
setPtyController without Electron."
`registerPtyHandlers` owns the `RuntimePtyController` that `terminal.create`
actually spawns through — the thing a Node backend needs and cannot get from the
provider thunks. The module was otherwise host-agnostic already; the only thing
pinning 8,031 lines to Electron was a static `ipcMain` / `powerMonitor` import used
purely to register renderer handlers that no headless host will ever receive.
`src/main/ipc/pty-host-bindings.ts` makes those surfaces settable, defaulting to
no-ops. Unlike AppEnvironment and SecretStore, the default does NOT throw: a host
with no renderer legitimately has nothing to register against, so not registering
handlers nobody can call is correct rather than a hidden downgrade. The desktop
installs the real objects in `attach-main-window-services` before its handlers run.
Also converts the remaining electron import to a top-level `import type`. oxlint's
`no-import-type-side-effects` caught that inline `type` specifiers still leave a
side-effect import — precisely the "type-only is not enough if esbuild still emits
require('electron')" trap a reviewer flagged.
**`src/main/ipc/pty.ts` now bundles with zero `require("electron")`.** A Node entry
can call `registerPtyHandlers(null, runtime, …)` and get a working PTY controller.
Verified: 128 files / 1,290 tests across ipc/pty and providers; the built-artifact
acceptance smoke passes end-to-end — which is the check that matters, since this
changes how every PTY handler registers; typecheck and oxlint clean.
* fix(pty-bindings): drop two unused eslint-disable directives
CI runs oxlint with unused-disable reporting; the two
`@typescript-eslint/no-explicit-any` suppressions I added were never triggered by
any enabled rule, so they failed static analysis as dead directives. The `any[]`
rest args stay — they mirror electron's own IpcMain signature, and narrowing them
would reject the real object at the desktop call site.
Verified with the exact CI invocation: `oxlint --format github` reports 0 warnings,
0 errors across the repo.
* fix(pty): install the host bindings per process, not per window
A real regression my own change introduced, caught by the SSH docker E2E
(`paired-startup-exec-readiness` — "recovers startup exec through a headed paired
desktop owner"). It reproduced on rerun, so it was not a flake.
`setPtyHostBindings` was called inside `attachMainWindowServices`, i.e. when a
window attaches. But `registerHeadlessPtyRuntime` (index.ts:3163) calls
`registerPtyHandlers` on the serve path *before* any window exists — so those
handlers registered against the no-op default and never reached the real `ipcMain`.
A paired desktop owner then attached to a runtime whose PTY handlers were wired to
nothing.
The bindings describe the *host*, not the *window*: an Electron main process always
has `ipcMain`, whether or not a window is open. Installing them beside
`setAppEnvironment`/`setSecretStore` at the top of bootstrap fixes both paths.
Verified: 128 files / 1,290 tests; the built-artifact acceptance smoke passes;
typecheck clean; `oxlint --format github` (the exact CI invocation) reports 0/0.
* feat(orcad): de-electron the runtime core and add the Node entry + build gate
**`src/main/runtime/orca-runtime.ts` — 41,048 lines — no longer imports electron.**
Its last three sites go through `runtime-desktop-surface.ts`: a native notification,
the authoritative-window lookup, and the one `ipcMain` channel used by the
renderer-backed tab-create fallback. All three are unreachable without a renderer —
`createTerminal` already takes the background branch when no window exists (#10333) —
so a Node host installs none and the runtime relays notifications to paired clients,
which is the better destination anyway. Ratchet 25 → 24.
Adds `src/main/orcad/orcad-entry.ts`: Node host adapters plus a `startOrcad` that
constructs the runtime, installs the PTY controller via `registerPtyHandlers(null, …)`,
and serves RPC. It sets two defaults the constructor gets wrong for a headless host —
`canRecoverPersistentLocalPtys: false` (no daemon here) and
`getDesktopWindowStatus: 'blocked'` (a Node host can never be promoted to a desktop
window, which is what `'openable'` claims).
Adds `config/scripts/build-orcad.mjs`, which **currently fails, on purpose**: 25
modules still import electron (browser and speech clusters, plugins, jira/proxy,
filesystem-watcher, and four `require('electron').app` one-liners). It names them.
Two bugs found while building it, both worth recording:
- The first bundle looked clean and was not. `electron` was bundleable, so esbuild
rewrote the metafile `path` to the resolved file under node_modules and a check for
`path === 'electron'` passed while the package was in the bundle — it failed at
runtime with electron's own installer message. The check now reads `original`, and
electron is marked external so a residual import fails loudly instead.
- `jsonc-parser`'s UMD build breaks the bundle at load; aliased to its ESM entry, the
same fix `build-relay.mjs` already carries.
Verified: desktop unchanged — the built-artifact acceptance smoke passes, runtime/pty/
provider suites green, typecheck clean, `oxlint --format github` 0/0.
* refactor(host): drop the last two require('electron') app lookups
`computer/sidecar-client.ts` and `ports/port-scan-command-client.ts` read the app
root through `require('electron').app` inside a try/catch. Both were already correct
under plain Node at runtime — they return null when it throws — but the literal text
fails the plain-Node entry guard regardless, which is why port-scan carried a comment
warning it must never become reachable from a fork entry.
Reading the AppEnvironment port gives the identical "no app root here" answer without
the text, so that warning is now obsolete and the comment says so.
Ratchet 24 → 22. Every remaining entry is a real coupling: the browser cluster (15,
which variant B does not ship), speech (2), plugins (2), and jira/proxy-settings (2,
needing an HttpClient port for Chromium session partitions).
Verified: 25 files / 209 tests; acceptance smoke passes; typecheck and
`oxlint --format github` clean.
* docs(orcad): record that the ratchet under-counts orcad's graph
The ratchet reports 22 electron importers; the orcad build reports 23. The extra is
agent-hooks/wsl-hook-relay-launch.ts, and the cause is a gap in the gate rather than
a rounding error: the ratchet measures what orca-runtime + runtime-rpc reach, while
orcad's entry also imports ipc/pty directly to install the PTY controller.
Once orcad ships it must become a ratchet entry point, or the two numbers drift and
the gate quietly stops covering the artifact it exists for.
* refactor(runtime): inject the browser commands factory
Drops 14 modules from the runtime's Electron graph in one change — the whole Chromium
browser cluster. Ratchet 22 → 8.
`OrcaRuntimeService` constructed `RuntimeBrowserCommands` as a field initializer, and
that construction is what pulled in `BrowserWindow`, `session`, `webContents` and the
cookie jars. Importing the class for its *type* is free; only building it costs.
So the class import becomes `import type`, and the instance comes from
`runtime-browser-commands-factory.ts`. The desktop installs the real factory at the
Electron entry. **All ~80 existing `this.browserCommands.*.bind(...)` delegations are
untouched** — a review round specifically warned that rewriting those was the
expensive, risky part, and this avoids it entirely.
With no factory installed, browser commands reject per call with `browser_unavailable`
rather than resolving to a stub that silently succeeds. The runtime already filters
browser capabilities out of `getStatus()` when no backend exists, so clients do not
offer the affordance in the first place.
Also corrects a stale comment in `pty-renderer-surface.ts` that still described the
fake window as present tense; it was deleted two commits ago.
Verified: 451 files / 5,513 tests across `src/main/browser` and `src/main/runtime` —
the entire browser automation suite; the built-artifact acceptance smoke passes;
`pnpm typecheck` and `oxlint --format github` clean.
* refactor(host): extract the plugin client list and port two app lookups
Ratchet 8 → 5.
- `listPluginsForClients` moves to `src/main/plugins/plugin-client-list.ts`. It needed
only three `plugins/*` helpers, none of them Electron — it was colocated with
`ipcMain.handle` registrations, so the runtime's `plugins.list` RPC dragged all of
Electron in to call a function that reads a lockfile. Same shape as preflight.
Dropping it also releases `ipc/plugin-marketplaces.ts`.
- `agent-hooks/wsl-hook-relay-launch.ts` and `speech/stt-service.ts` read `getAppPath`
and `isPackaged` through the AppEnvironment port.
The five that remain are all genuinely Chromium and need the HttpClient port or a
watcher split, not another mechanical swap: `browser/cdp-bridge` (webContents),
`ipc/filesystem-watcher` (ipcMain), `jira/authenticated-request` and
`network/proxy-settings` (net + session partitions), `speech/model-manager`
(`net.request`, which honors app proxy settings that Node https does not — replacing
it is a behaviour change, not a rename).
Verified: 219 files / 1,922 tests across plugins, speech, agent-hooks and the runtime
RPC methods; the built-artifact acceptance smoke passes; typecheck and
`oxlint --format github` clean.
* refactor(network): resolve the default proxy session lazily
Ratchet 5 → 4.
`proxy-settings.ts` needed exactly one Electron value: `session.defaultSession`, as
the fallback when a caller does not pass `options.proxySession`. Callers could already
inject a session; only the default was hard-wired. It now comes from a settable
resolver, so the module loads under plain Node.
**A resolver rather than a Session, because a Session eagerly throws.** The first
attempt installed `session.defaultSession` directly in pre-ready bootstrap and broke
startup outright — `TypeError: Session can only be received when app is ready`. The
acceptance smoke caught it before commit. Deferring to first use is always after ready.
Behaviour with no session is not a degradation: there is no Chromium proxy config to
discover, so `resolveProxy` is skipped and the environment variables become the whole
answer rather than a fallback. Applying rules to a session that does not exist is
likewise skipped; settings are still honoured because outbound requests read the env.
This reaches past Jira — a review round noted `ensureElectronProxyFromEnvironment` is
also on the Claude HTTP path via `oauth-refresh.ts` and `rate-limits/claude-fetcher.ts`.
Verified: 48 files / 526 tests across network, jira and rate-limits; the
built-artifact acceptance smoke passes; typecheck and `oxlint --format github` clean.
* fix(index): merge the duplicate proxy-settings import
CI's code-quality lint (`oxlint --config config/oxlint-code-quality-native-plugins.json
--deny-warnings`) flags a module imported twice in one file. My earlier insertion added
a second `./network/proxy-settings` import beside the existing one.
Verified with CI's exact invocation: exit 0.
* refactor(network): add the HttpClient port and lift BrowserError out of cdp-bridge
Ratchet 4 → 2.
Two unrelated couplings, both of the same shape — a small thing living inside a
Chromium-heavy file.
`BrowserError` is a seven-line error class with no dependencies, but it lived in
`browser/cdp-bridge.ts`, which imports `webContents`. The runtime catches that type on
paths with nothing to do with CDP, so one import kept a Node host from loading the
runtime at all. Moved to `browser/browser-error.ts`; cdp-bridge re-exports it.
`jira/authenticated-request.ts` fetches through `net.fetch` and reads
`session.defaultSession`. `network/http-client.ts` makes both settable. This one is a
**named port rather than a silent fallback, because the fallback is not transparent**:
Electron's net follows Chromium session/proxy state, avoids undici's stale keep-alive
sockets after a VPN path change, and sends a Chrome user agent that Jira's XSRF check
depends on. A Node host gets `globalThis.fetch`, reads proxy config from the
environment, and sends Node's user agent. That difference is documented at the port.
`session.defaultSession` is read per call, not captured at install — it throws before
the app is ready, which is the mistake the previous commit made and the acceptance
smoke caught.
Test wiring: `jira/client.test.ts` installs the port *inside* `loadClientModule`, after
its `vi.resetModules()`, since the reset gives the module a fresh singleton.
Verified: 461 files / 5,616 tests across jira, browser, network and runtime; the
built-artifact acceptance smoke passes; typecheck, `oxlint --format github` and the
code-quality lint with `--deny-warnings` all clean.
* fix(http-client): register the Node fetch fallback with the call-site audit
`global-fetch-call-site-audit.test.ts` guards every global-fetch use, because the
global runs on undici where an unread response body can crash the whole process
(orca#8695). The HttpClient port's Node fallback is a new such call site and was
unregistered — the guard caught it in a full-suite run.
Registered with the reasoning, and the port's doc comment now states the body-safety
contract explicitly: it hands the Response straight to its caller and never inspects
it, so the consume/cancel obligation stays exactly where it already was — with the
caller, unchanged from when they called Electron's net directly.
Two comments elsewhere mentioned the global by name and tripped the line scan as false
positives; reworded to describe the behaviour rather than name the API.
Verified: audit passes; typecheck and `oxlint --format github` clean.
* fix(app-environment): read hasAppEnvironment through the realm slot
|
||
|
|
ef096d539d |
fix(terminal): refuse a cursor on a screen read, and correct the source docs (#15563)
Review follow-up on #15380. The RPC accepted `cursor` and `screen` together. The CLI refuses the pair, but terminal.read is reachable without it, and honoring both answered with rendered lines carrying the stream's pagination metadata — two frames of reference in one payload, which is the confusion `source` exists to remove. The guard beside it, withVisibleSnapshotFallback, already declines to substitute rendered lines when a cursor is present; the screen path now agrees, at the RPC boundary where every remote caller passes. Nothing could previously send both, since `screen` did not exist, so rejecting breaks no existing caller. The command notes and the runtime comment both still described the fallback as `source: stream`, left over from renaming that value to `screen-unavailable` during implementation. The spec text is surfaced through `orca help` and the agent-context schema, so a caller following it would test for a value the code never emits. Both now describe all four states, including that an absent source means the host predates the field. |
||
|
|
59c5624e14 |
fix(terminal): require a proven exit before retiring a subscription's lease (#15470)
isPtyKnownExited read a PTY record as exited whenever `connected` was false:
if (pty) { return !pty.connected }
Its own leaf fallback, one line below, demands getTerminalState(leaf) === 'exited',
which is only true once lastExitCode is set. So the same function proves absence on
one path and infers it on the other, and the inferring path is the one that runs
whenever a record exists.
onPtyExit is the only writer of lastExitCode. The liveness sweep clears `connected`
with no exit code for every PTY behind a dropped relay, so that state is a lost
connection to a process that may still be running on the host — not an exit. Reading
it as one makes subscribeToPtyExit fire its listener synchronously at subscribe time,
which retires the lease and emits `end` for a live terminal. Mobile reads `end` as
"PTY gone" and rearms; after three attempts it stops and leaves the composer on
"Waiting for terminal…", which is where the 0.0.44 permanent lock comes from.
Use the runtime's existing three-valued discriminator so both paths demand the same
proof. 'unknown' now keeps watching, and the later real exit still fires the listener;
a subscription that outlives its PTY is still bounded by the connection abort.
Both callers are in subscribeToPtyExit — the subscribe-time fast path and the
registration-race re-check — and both want proven-exited, so neither changes shape.
|
||
|
|
0e5b348414 | fix(repos): keep the desktop-owned manual project order authoritative across paired clients and SSH catalog publishes (STA-4850) (#15538) | ||
|
|
a61b39a9a6 |
fix(runtime): stamp a runtime's own project setups as local, and report remote status about the remote (STA-4792) (#15376)
* fix(runtime): stamp a runtime's own project setups as local, and report remote status about the remote (STA-4792) Two independent frame-of-reference bugs, both from code describing one machine while labelled as another. #15366 — projectHostSetup.* persisted the caller's host id verbatim. Those `runtime:<environment-id>` ids are minted by the calling client's own pairing store, so they name a machine only relative to that client. A client sending one is addressing this runtime, and runtimes do not proxy these calls onward, so the host it names is us. Storing the client's spelling made one machine look like a different host to every other client, hid its rows from them, and defeated the (projectId, hostId) duplicate check — two laptops paired to one server each created their own setup for the same checkout. Re-spell it as `local` at the RPC boundary. Rows written earlier keep their old stamp; readers already project `local` back to `runtime:<their-id>`, so the client-visible model is unchanged and no ids are rewritten. STA-4792 defect 4 — `status --environment <name>` hardcoded app.running:false to mean "no desktop on THIS machine" while every other field in the same object described the target, including a desktopWindowStatus echoed straight from it. The result contradicted itself and read as "that run was headless" when the remote GUI was up. `app` now describes the target, keyed off the one window status that requires a live renderer, and the result names its own subject so the frame can't be misread again. The remote pid is not knowable, so it stays null. STA-4792 defect 2 gets a regression test rather than a fix: routing already made the client remote, which is what stops a Windows destination being joined to the local cwd. The test pins the exact reported invocation. * fix(status): share the remote app projection with the SSH host passthrough, and name the version gap on project host setup Two review follow-ups. The SSH host passthrough answered `app.running: true` unconditionally for the Orca host a caller reached over SSH, claiming a desktop app even for a headless `serve`. That is the same defect as the paired-server path, one transport over, so the projection moved to shared and both now answer the question the same way. `--host runtime:<id>` routes project commands to a paired server, which means a client can reach a server that predates project host setup without meaning to. That answered a raw `method_not_found`, which reads as an Orca bug rather than a version gap; the CLI now names it the way the desktop already does. Reverted a third change: making the persistence duplicate check treat `local` and `runtime:*` as one machine. That assumption holds at the RPC boundary, where a `runtime:` host means the runtime being addressed, but not in the store, which also records independent provisioning metadata for machines that are not itself. An existing test covers exactly that, and it was right. The duplicate convergence therefore stays bounded to rows written after the normalization. |
||
|
|
9b8dbd8930 |
fix(mobile): keep a terminal lease alive when the handle lookup fails (#15463)
* fix(mobile): keep a terminal lease alive when the handle lookup fails `waitForTerminal(..., 'exit')` rejects with `terminal_handle_stale` whenever the handle cannot be resolved right now. The loudest source is `record.rendererGraphEpoch !== this.rendererGraphEpoch` — every renderer graph reload invalidates handles issued before it, on panes whose PTY is untouched. All three terminal.subscribe branches wired that rejection straight to teardown (`.catch(() => registration.releaseIfCurrent())`), so the host retired a live lease and emitted `end`. Mobile reads `end` as "PTY gone" and rearms; after MAX_REARM_ATTEMPTS = 3 it stops and leaves the composer on "Waiting for terminal…", recovering only when a replacement handle arrives. A live PTY keeps its handle, so no replacement ever comes: the pane is dead until the app is backgrounded and reopened. Demand proof instead. Retire on abort (this socket is going away) or when isLeafPtyProvenAbsent says the process is gone; a real exit still resolves and retires through the .then leg. Absence must be proven, never inferred from a lookup that failed. Reproduced live: a paired 0.0.44 client over LAN, fault injected on the handle lookup, composer locked on "Waiting for terminal…" and STILL locked 97s after the fault was lifted with the PTY alive and the handle unchanged. Narrows one behavior #14992 pinned: a stale handle alone no longer retires the owner. The ownership property is unchanged — once absence is proven, the owning registration is still what retires. That test is updated, not deleted. * fix(mobile): observe PTY exit independent of stale handles * fix(mobile): stop subscribe setup when an exited pty releases synchronously The runtime doubles for terminal.subscribe never stubbed subscribeToPtyExit, so the new lifetime watcher threw and killed every subscription under test. Also return early when the watcher releases synchronously: cleanup had already run, so the stream handlers, view subscribers, and mobile presence registered afterwards were never torn down. |
||
|
|
3ffab9a6b3 |
feat(terminal): read the rendered screen with terminal read --screen (STA-4792) (#15380)
* feat(terminal): read the rendered screen with `terminal read --screen` (STA-4792) `terminal read` returns accumulated pty output with escape sequences stripped. That is the right answer for "what happened over time" and the wrong one for "what is on screen": any program that repaints a line comes back as stacked fragments, so one `clear` typed key by key reads as `cclclecleaclear`, and a prompt that draws a space by moving the cursor loses it. Nothing in the output said which question had been answered, so it was used as rendering evidence and produced false conclusions. The runtime already knew how to render — it replays the byte stream through a headless emulator — but only as a fallback for blank reads, alternate screen, and never-attached ptys. A normal attached terminal never reached it. `--screen` asks for it directly. Every read now reports its source, which also surfaces the pre-existing snapshot fallback that until now swapped rendered lines into an ordinary read with no indication. `screen-unavailable` distinguishes "asked for a screen, none could be rendered, here is the stream" from a stream the caller asked for, and an absent source means the host predates the field. Because an older host strips the unknown param and answers with its ordinary read, `--screen` against one fails with that explanation rather than passing the stream off as a screen. `--screen` and `--cursor` are mutually exclusive: a screen is the current frame and has nothing behind it to page. * refactor(terminal): stamp the screen source where rendered lines enter the read Inferring it from tail array identity worked but made a load-bearing contract out of reference equality; any later path spreading the read would silently mislabel. Rendered lines only enter through one builder, so it stamps there and anything still unlabelled is the stream. |
||
|
|
9b5538d786 | fix(runtime): scope create-with-activate navigation to the requesting client (STA-2802) (#15407) | ||
|
|
79be5b7fde |
feat(orchestration): report a worker blocked on a human prompt (STA-4513, STA-3714) (#15261)
* feat(orchestration): report a worker blocked on a human prompt (STA-4513, STA-3714) A lane parked on an approval, trust, or permission prompt looked exactly like a lane that was thinking or inside a long tool call. On origin/main, driving a real cursor-agent through Orca: surface running `sleep 60` awaiting approval worktree ps agents[].state working working terminal show / list no such field no such field terminal wait --for tui-idle satisfied: true satisfied: true worker-show no agent state no agent state The runtime already fuses hook state, OSC title, and matched prompt text into a `permission` verdict inside getTerminalAgentStatus — it was reachable only from the renderer, and it was blind to cursor-agent approvals. Two gaps, one boundary. Exposure: getTerminalInteractiveWait publishes that same fusion, minus the async foreground probe, as `agentWait` on `terminal show` and on `worker-show`'s observation. It carries the evidence that proved the wait (hook, prompt-text, or title) so a coordinator can weigh it. Null means no proof; a missing field means the host predates it — absence is never read as "not waiting". Detection: cursor-agent's hook set has no approval event and beforeShellExecution fires identically for auto-allowed commands, so its rendered menu is the only authority. Matched on the key-bound choices rather than the prose, requiring two, and self-clearing when the follow-up input line returns. Its live spinner title is exempted from the staleness rule that clears startup modals, because cursor keeps spinning while it waits. Falls out of routing it through the shared verdict: `dispatch --inject` into a cursor pane on an approval now refuses with agent_prompt_blocked instead of typing the preamble into the dialog. Fixtures are captured verbatim from cursor-agent 2026.08.11-e8db854 driven through Orca; the same case matrix was replayed live against a built runtime. terminal list stays untouched: its rows would each need a full tail scan, and STA-4694 owns the one-call-per-run aggregate. * fix(orchestration): only call a Cursor approval live while it owns the screen Independent review found the approval detector trusted one dismissal string, so any later output that did not contain cursor's follow-up line left the menu reading as a live wait. Reproduced: a tail of the real menu followed by two lines of ordinary output returned agent-approval-prompt, which fails tui-idle and refuses prompt injection on a healthy lane. Replaced with the structural property the string was standing in for: a live dialog owns the bottom of the screen, so the last choice may sit at most one line above the end of the retained tail. That tolerates a status footer or a partial line mid-redraw without admitting scrollback, and it drops the vendor prose. Being bottom-of-screen is also the dating this reason needed, so it no longer requires waitBlockedAt. A tail restored from terminal history carries none, and a lane parked on a prompt emits no bytes — so before this, an Orca restart made exactly the lane both issues are about go quiet for good. The startup modals keep the timestamp rule: their text lingers in scrollback with nothing to say whether it was answered. Also from review: - worker-show and federationShow reuse the verdict showTerminal already computed rather than rescanning the tail, so the two can no longer disagree. - The worker-show test now drives a real runtime, real PTY tail, and the real detector; it previously mocked getTerminalInteractiveWait, so it would have passed with detection permanently returning null. - The guard claim is now asserted against the guard: a blocked pane rejects both assertTerminalAgentSendable and sendTerminalAgentPrompt, and a working pane still passes. - Added a non-local (connectionId) pane case, since the verdict is derived from retained tail and title state on every host. * fix(agent-status): stop a hook wait from outliving its agent A third reviewer caught that the hook branch proved agent ownership from the pane title alone, while the shared verdict it claimed to reuse also probes the foreground process. A shell that takes a pane back usually sets something like `user@host: ~/repo`, which no title rule recognizes, and a hook row stays fresh for AGENT_STATUS_STALE_AFTER_MS — so a dead agent could be reported as waiting on a human for half an hour. Hook evidence now goes through getTerminalAgentStatus, which is the only thing that can answer whether an agent still owns this PTY. The two prompt branches skip it: a matched prompt is on the pane's screen now, so it proves itself. That makes the probe cost fall exactly where correctness needs it, and getTerminalInteractiveWait async, which only showTerminal had to absorb. Also trims the comments the same reviewer flagged as longer than the repo's rule. * test(agent-status): pin that a dead pane stops reporting a human wait A fourth reviewer noted the approval menu sits at the bottom of a dead pane's tail forever, and that no test covered process exit with no trailing output. The snapshot already refuses an exited pane, and worker-show gates agentWait on proven identity — this pins both so neither can drift into reporting a worker that needs intervention as one that needs an answer. * fix(orchestration): never report an unchecked worker as not waiting Automated review caught that the three worker paths which return before the wait is ever evaluated — unattached, missing, and identity_changed — then had their undefined coerced to null by the emitters. A worker whose process was replaced was reported as `agentWait: null`, which reads as "Orca looked and nobody is waiting" when Orca never looked. That is the false negative this field exists to remove. The field is now emitted only when it was evaluated, so a present null is a claim about the pane and an absent one means nobody looked — because the host predates the field, or the worker's identity could not be verified. The CLI and the worker-show note say that rather than blaming an old host. Covered on the context-only path, where the regression test fails against the previous behavior; the supervised and federated emitters take the identical one-line change. Also trims the two test-file headers to one statement of purpose. * fix(agent-status): tighten the Cursor menu match and stop guessing on unknowns Fourth review round, three findings, each reproduced before acting. Matching each choice marker with an independent lastIndexOf let text outside the menu carry the anchor. An agent narrating "next time I'll suggest Run Everything" after the menu was answered pulled the match down to the bottom of the screen and revived it. The match is now confined to the last lines of the tail, and a choice is a line that ends in the key that picks it — prose writes the same words but not the same shape. The one line of slack under the dialog went with it. It was a guess; every capture of a live dialog ends on its last choice, and one line is exactly enough room for that narration. A redraw caught mid-flight now reads as no wait until the next poll, which is the safe way to be wrong. The hook branch awaited a foreground probe that reaches a PTY controller which may be a remote host, so a wedged probe stalled every caller of showTerminal — a path that never probed before. It is bounded now, and a timeout leaves the wait unevaluated rather than claiming there is none. Which is the same distinction the previous commit only fixed one level up: getTerminalInteractiveWait itself turned an unreadable pane into `null`, so showTerminal published "looked, nobody waiting" for a pane it could not read. It returns undefined there, showTerminal omits the key, and worker-show's text output prints unknown rather than rendering it the same as none. * fix(agent-status): bound the wedged probe's cost and stop matching prose keys Fifth review round. No correctness defects in the shipped behaviour this time; two robustness holes and the documentation of the contract. The bounded probe abandoned the wait but not the request, so a coordinator watching a wedged remote host added one live probe on every poll. It is single-flighted per PTY now, the way the leaf-absence probe already is. The trailing-key rule that separates a menu row from the agent narrating a choice was written as a character class, and any lowercase run up to twelve characters satisfied it — "…suggest Run Everything (as before)" passed. Spelled out as key names instead, which also lets the glyph forms of those keys through. The contract wording said an absent agentWait meant an old host or an unverifiable identity. It also covers an unreadable pane and a probe that did not answer, and a reader diagnosing an old peer from that would be wrong. Corrected on the type, the worker-show note, and in docs/reference/remote-wire-compatibility.md, which had no entry for a field whose absent and null states mean different things. Also strengthens the worker-show agreement test, which compared the terminal and observation payloads without asserting either held the expected wait, so it passed when both were absent. |
||
|
|
a77a2f93f7 | fix(remote): search Quick Open paths on the host (#15158) | ||
|
|
8cd338357e |
fix(runtime): tear down terminal subscriptions only through their owning registration (#14992)
* fix(runtime): tear down terminal subscriptions only through their owning registration
A terminal.subscribe teardown was keyed on `${terminal}:${clientId}`, which is
stable across reconnects. cleanupSubscription invokes whichever cleanup currently
owns that key, so after a mobile reconnect rebound the id, a late teardown from the
dead connection killed the replacement stream and the terminal froze (STA-4510).
Add registerOwnedSubscriptionCleanup, returning a registration handle whose
releaseIfCurrent no-ops once the id has been rebound, and route all 12 teardown
call sites in the three terminal.subscribe branches through it. Register-time
eviction now also targets the owner it captured rather than re-resolving the key.
terminal.unsubscribe gains the same ownership rule via connectionId, matching the
runtime.clientEvents.unsubscribe precedent: make-before-break migration sends the
unsubscribe over the old session after the new one has already rebound the id.
The teardown-by-key pattern predates the bug; #7490 made it reachable by binding
the exit-waiter to the per-socket abort signal, so every socket close now runs it.
Tests use a faithful subscription-registry double; the ad-hoc Map stubs they
replace never evicted the prior generation, which is why no test caught this.
* test(runtime): migrate the remaining subscription stubs to the faithful registry
streaming.test.ts and terminal-provider-snapshot-sequence.test.ts still stubbed
registerSubscriptionCleanup only, so terminal.subscribe's owned registration was
undefined and teardown never fired. Assert the registration is retired rather than
spying on cleanupSubscription, which the owned path now calls internally.
* fix(runtime): guard the lease-only presence release and use the shared registry double
Review findings on this PR:
- The lease-only branch's compensating handleMobileUnsubscribe ran unconditionally
after a rebind. Presence is keyed (ptyId, clientId) with no refcount and `closed`
is exactly the post-rebind state, so a superseded handler deleted the replacement
subscriber's presence. Gate it on the registration still being current. This also
gives SubscriptionRegistration.isCurrent its production caller.
- The STA-4510 regression test hand-rolled a second registry copy that diverged from
the shared double (no try/catch around cleanup, no in-flight join). Import the
shared double instead, so the test proving the bug uses the same fidelity as the
rest of the suite.
- cleanupSubscriptionIfOwnedByConnection treats an absent connectionId as authority.
That is the connection-less local unix-socket path, not an oversight; say so.
* fix(runtime): drop the lease-only presence guard; it disabled a real compensation
Review pass 2 showed the guard added in the previous commit was wrong.
registration.isCurrent() is always false whenever `closed` is true: either our own
cleanup ran, in which case cleanupSubscriptionAndWait already deleted the map entry,
or a rebind replaced it. So the guard did not distinguish the two cases — it made the
compensating handleMobileUnsubscribe unreachable, which is precisely the
resurrect-after-cleanup case that line exists to handle.
The scenario the guard was meant to fix is also not reachable: the lease-only call
passes no viewport, and both !viewport paths in handleMobileSubscribeInternal return
with no await, so the subscribe resolves in microtasks and a socket close cannot win it.
Revert the guard, and drop SubscriptionRegistration.isCurrent with it — it had no
remaining production caller and shipping unused runtime API invites exactly this.
Also from review:
- cleanupSubscriptionIfOwnedByConnection reported false for an id with no registration,
conflating 'refused, another connection owns it' with 'already gone'. Report true.
- Note on the registry double that it mirrors production and can drift; the runtime
tests pin real behavior, the doubles only pin routing.
* fix(runtime): make the unsubscribe refusal observable and restore test-double parity
Review pass 3:
- The registry test double omitted production's 'unregistered id is already gone'
early-out, so it returned refused where production returns gone. The legacy
terminal.unsubscribe path reaches that branch with a never-registered bare id, so a
routing test would have locked in the inverse of production.
- terminal.unsubscribe ORed the bare-id and composite results. Registrations always use
the composite, so the bare id reported 'already gone' and masked a real ownership
refusal: a stale connection was correctly refused but told unsubscribed: true. The
composite answer is authoritative when we try it.
- Pin why the lease-only compensating handleMobileUnsubscribe is deliberately unguarded:
it is safe only because that call passes no viewport and returns with no await.
- Cover the no-registration branch, which had no test.
* fix(runtime): report an unsubscribe refusal without masking a real teardown
Review pass 4 disproved the previous commit's rationale. A clientless legacy-JSON
stream registers under the bare terminal id, not the composite, so either call in
terminal.unsubscribe can be the real teardown. Overwriting reported false after a
destructive success; the earlier OR reported true after a refusal. AND over the calls
that actually ran is the honest aggregate: false needs a genuine refusal.
Tests:
- cover the bare-id path, which the ownership tests never exercised
- pin the microtask invariant the unguarded lease-only compensation depends on. The
first version of that test was vacuous: it raced two setTimeout(0) timers, and the
earlier-registered one always won, so it passed with an await injected. It now drains
microtasks only and goes red under that mutation.
|
||
|
|
64de8dd637 |
fix(workspaces): delete on the confirmed host, and make both hosts' rows selectable (STA-4343) (#15013)
* fix(workspaces): host-qualified workspace deletion (STA-4343, STA-4448) Squashed integration of PR #14606 + the codex review-loop output, replayed onto current main. Granular history preserved on brennanb2025/sta-4343-review-full. Fixes the regression from #13413: a workspace id is repoId::path with no host component, so the same repo at the same path on two hosts published one id for two workspaces, and deletion routed by that id landed on whichever host routing preferred - usually the ACTIVE one, not the row the user confirmed. - removeWorktree takes a REQUIRED host-qualified WorktreeRemovalTarget; omitting the host is a type error. All destructive callers migrated. - Projections dedup on (host, id), so two hosts render as two selectable rows while the createWorktree/fetchWorktrees race duplicate still collapses. - Ephemeral VM cleanup is host-scoped. It matched on bare workspaceId, so the host-scoped delete path destroyed the SURVIVING host's VM and its unpushed filesystem - a leak fix that had become data destruction. - Selection, keyboard routing, lineage grouping and Space rows carry host identity end to end; fixing the executor dedupe alone would have turned one-row intent into deleting both hosts. Files split to stay under max-lines rather than raising any cap. * refactor: split files that crossed max-lines The review-loop commits used --no-verify, so the pre-commit hook never enforced the caps. Extracted cohesive units rather than raising any limit: renderer teardown, delete-with-toast, pinned-group rows, host-scope helpers, workspace-kind predicates, filter actions, kanban drag selection, the renderer removal result type, and the native-chat persistence tests. * refactor(workspaces): extract cleanup deletion-phase selector Clears the last max-lines violation and the import-type side effect the changed-code gate flagged. * refactor(sidebar): track the delete-dialog extraction modules * fix(workspaces): preserve host identity across remaining surfaces * fix(sidebar): re-carry host through the rewritten palette result model #15170 replaced PaletteSearchResult while this PR was open. Re-applied the host qualification on top of the new model instead of taking either side: results carry worktreeHostId again, and the board filter keys its matched set on host identity rather than the bare id. Known gap, documented in the board test rather than deleted: searchWorktrees resolves evidence through a `documents` map keyed by BARE worktree id, so two same-id host rows collapse before this code sees them. Closing that belongs with the palette work. * test(cmd-j): pin the palette collision gap instead of asserting the old model The palette collision test asserted two host-qualified rows, which #15170's rewrite made unreachable: item ids are bare again and worktreeMap is id-keyed. Rewritten to assert what holds — activation always names a host — and to pin the defect it exposes: two same-id rows render on ONE command value, so React sees duplicate keys and a click on the first row activates the second row's host. That reproduces on main, so it is pre-existing, not from this PR. Pinned rather than deleted so fixing it must update this test. --------- Co-authored-by: QA <qa@local> |
||
|
|
0bedeea642 | fix(orchestration): expose unsupervised dispatch lanes (#15105) | ||
|
|
32ee3b0536 |
reland(browser): route every cookie-import write through CDP identities, and never clear what it will not write back (#15030)
* reland(browser): restore CDP-identity cookie-import writes (#14729) Reverts the revert |
||
|
|
be07b43a2b | fix(orchestration): enforce honest recipient routing (#14964) | ||
|
|
30b76e6edb | fix(orchestration): enforce task dispatch state invariant (#14961) | ||
|
|
7afce2ea41 |
fix(ssh): stop reporting a confirmed kill when the SSH provider is gone (#14977)
* fix(ssh): stop reporting a confirmed kill when the SSH provider is gone A detached relay PTY is designed to outlive the provider that addressed it (it ignores SIGHUP and ships with an unlimited grace), so "the SSH provider is no longer registered" is lost contact, never evidence the remote process stopped. Both stop primitives in the PTY controller returned `true` from that branch, and every caller downstream reported the fabricated success: the CLI printed "PTY killed.", worker-stop settled the dispatch as stopped, and — because the stop "succeeded" — the unstopped-PTY gate never ran, so worktree removal walked straight past a live remote agent. `kill`/`stopAndWait` now still tombstone the local lease but report an unconfirmed stop and record why, using the three-verdict vocabulary the worktree teardown gate already spoke (`live` / `unverifiable` / `exited`), promoted out of that module into `src/shared/pty-liveness-verdict.ts`. The close receipt, the CLI wording, worker-stop and the removal gate all read that verdict instead of inferring an exit from silence. The same rule fixes the mirror-image defect: the aggregate inventory only enumerates registered providers, so a dropped relay clears `connected` for every remote PTY at once. The sweep now separates the provider answering "absent" (an exit) from no provider being able to answer (lost contact), so worker-stop stops claiming `exited` from a disconnect. The `connected` wire field is unchanged in meaning and shape. * fix(orchestration): apply the same honesty to the federation stop path The federation host runs its own copy of the worker observation and stop logic, with the same two defects: `inspectRemoteAttachment` read a dropped relay's `connected: false` as `exited`, and `federationStop` settled the dispatch as stopped from a close it never confirmed — relaying a fabricated success all the way home to the coordinator. Two guards also had to move so the honest verdict does not become a new refusal. `federationRead` gated on `status !== 'running'`, which would have rejected a connected terminal the moment a stop lost contact with it; it now gates on `status === 'exited'`, which is equivalent for every pre-existing status given the two guards beside it. Local `workerStop` likewise still attempts the close when the verdict is `unverifiable` — losing contact is a reason to report the outcome honestly, never a reason to stop trying. The show observations now carry the reason alongside the status, so a bare `unverifiable` is actionable. Both are new optional fields. * fix(ssh): preserve unconfirmed stop verdicts across consumers * fix(ssh): use canonical live verdict wording * fix(ssh): refuse wrong-host teardown verification * test(orchestration): confirm worker release teardown * fix(orchestration): negotiate honest worker stop receipts * fix(agent-teams): fence uncertain teammate respawns * fix(ssh): avoid duplicate missing-provider teardown * fix(orchestration): preserve archives across release retries * fix(ssh): preserve verdicts across synthetic kill exits * fix(ssh): preserve liveness evidence across teardown * fix(agent-teams): replace panes only after confirmed stop * fix(ssh): distinguish host exits from relay loss * fix(ssh): narrow concurrent inventory verdicts * fix(orchestration): serve archives after uncertain release * fix(orchestration): expose unverifiable read liveness * test(ssh): align liveness assertions with verdicts * fix(ssh): preserve host scope across inventory failures |
||
|
|
77ef6bb9ee | fix(terminal): verify agent prompt submission (#14962) | ||
|
|
8ca4ed945e |
feat(terminal): report execution host and listing scope in terminal list (#14973)
* feat(terminal): report execution host and listing scope in terminal list `orca terminal list` returned rows with no host identity and no statement of what the listing covered, so a scoped listing that saw nothing read as "nothing exists anywhere" — an agent reported a live remote worker dead. Each row now carries an optional `executionHostId` derived from the PTY id (SSH and paired-runtime ids embed their owner), and the result carries an optional `hostScope` naming the hosts covered and the known hosts skipped. Both are surfaced in `--json` and in the human-readable CLI output, where an absent field renders as `unknown` rather than `local`. Both row builders route through one resolver, so the rule lives in one place. * fix(terminal): preserve unverifiable host scope * fix(terminal): fail closed on unverifiable hosts * test(terminal): name unverifiable scope explicitly * perf(terminal): keep graph hydration host scans narrow * fix(terminal): reject blank foreign host owners * fix(terminal): validate inferred inventory hosts * fix(terminal): preserve paired folder host scope * fix(terminal): keep inventory host inference typed * fix(terminal): disclose paired folder hosts |
||
|
|
fa9b20cb41 | feat(skills): reland private bundle sharing safely (#14934) | ||
|
|
3c8410d927 |
Revert "fix(browser): route every cookie-import write through CDP identities …" (#14942)
This reverts commit
|
||
|
|
bf6dc6fcba |
fix(browser): route every cookie-import write through CDP identities (STA-4300) (#14729)
* test(browser): repro STA-4300 CHIPS partition downgrade on native cookie import success path * fix(browser): route every cookie-import write through CDP identities (STA-4300) * test(browser): cover partition fidelity on both import write paths (STA-4300) * fix(browser): never stage unreadable cookie partitions (STA-4300) * fix(browser): gate partition skips on client support (STA-4300) * test(browser): anchor the client partition-skip capability assertion (STA-4300) * fix(browser): skip Firefox CHIPS cookies whose Chromium identity cannot be rebuilt (STA-4300) * fix(browser): tolerate a Firefox schema without originAttributes (STA-4300) * docs(browser): correct a comment that still named the removed cookies.set write * fix(browser): block lossy Firefox cookie import on old clients * fix(browser): read Firefox CHIPS from schema flag |
||
|
|
763b1febeb |
Revert "feat(skills): add private bundle sharing (#14401)" (#14913)
This reverts commit
|
||
|
|
757fae28d7 |
feat(skills): add private bundle sharing (#14401)
Co-authored-by: E2E Test <e2e@test.local> |