mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 00:02:10 +00:00
d72daf8153ec11cd4fc97071de0e31bf71aa948e
763
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d15939c5fd |
fix(terminal): recover rejected paired-runtime input (STA-2830) (#12675)
With a desktop client paired to a remote Orca runtime, terminal panes could report connected, writable, and `terminal.send` returning accepted — yet keystrokes never reached the agent. No error, no banner, no recovery; input silently vanished. The ticket was really two bugs. The attach half was already fixed by #12589 (subscriber-driven daemon attach), confirmed by reproducing against current main. This fixes the remaining half: a write the host refuses had no way to tell anyone. A capability-negotiated `WriteUnavailable` opcode carries that refusal back to the client, where it feeds the pane's pre-existing recovery hook. Capability gating matters because decoders reject unknown opcodes on desktop — and, worse, silently drop them on mobile — so the signal is negotiated in the subscribe handshake. Verified per direction: an old host strips the unknown Subscribe key, an old client omits it so the host never emits, and capability cannot be inherited across resubscribe. Independent review then found the signal was being delivered and discarded: recovery demanded an authoritative liveness answer, and `pty:hasPty` had no `remote:` guard, so a paired pane's id fell through to the LOCAL provider, which returned false, and recovery bailed before remounting. Every test stopped at the transport boundary, so all of them passed while the pane stayed just as stuck. `pty:kill` already had exactly that guard. The fix makes main answer LESS rather than claim more: `pty:hasPty` now returns unknown for a `remote:` id instead of a fabricated false, because main cannot speak for another host's PTY. The remount is then authorized by positive evidence — the process that owns the PTY stating it refused this specific write over a live negotiated connection — not by inference from silence. Local and app-SSH ids keep the probe, where a false genuinely means the shell died. Nothing is destroyed on this path; the remount rebuilds the renderer over the session it already had. An end-to-end test now carries a rejected write from the host through to an actual remount, which no prior test did. A surviving mutant was also killed: the legacy-binary capability gate could previously be deleted with nothing turning red. The reliability gate stays experimental — live paired journeys and mixed installed-release evidence remain uncollected. Fixes STA-2830. |
||
|
|
a766ee4bcd |
fix(runtime): refuse to silently wake a deliberately slept pane (STA-3465) (#12672)
`activateMobileSessionTab` gated only on `publicTab.status !== 'ready'`. A deliberately slept pane publishes as `pending-handle` indefinitely — indistinguishable at that call site from a pane awaiting reconnect — so the reconnect probe added by #11542 respawned it with a re-resolved agent launch, waking something the user had deliberately put to sleep. The first attempt refused activation for any pane with a `worktree-sleep` record, applied to every path. Independent review found that broke the documented wake gesture: opening the tab IS how those panes are meant to cold-restore (`wake-sleeping-agents-in-background.ts`: "Those panes cold-restore --resume when their own tab is opened"). A mobile tap sends the byte-identical call the reproduction test used, and in three of four topologies no wake clears the record first — so the tap became a permanent no-op with no feedback. This carries intent explicitly instead of inferring it. A new shared `TabActivationIntent` ('user' | 'automatic') rides the existing ActivateTab schema as an optional additive field; `isAutomaticTabActivation` returns true only for an explicit 'automatic', so an absent value is permissive BY CONSTRUCTION in one place — an older client that does not send it keeps today's behavior rather than silently losing its wake gesture. The field is required on the mobile helper's params, so no call site can be added without declaring who asked. Every user path (mobile tab switches, paired tab clicks, shortcuts, palette, the pane's own open) is labelled 'user'. The only automatic sender in the codebase is `waitForResubscribeHostSessionHandle`, the #11542 reconnect probe. Verified per topology: user activation materializes a parked pane under headless serve, a paired runtime client, a completed agent with restoreOnTabOpenOnly, and a running agent whose wake cleared the record. The automatic probe is refused without retiring the surface, and #11542's reconnect tests stay green. Also fixes a test fixture that made a real bug untestable: the store stub ignored the host id, so mutating the partition lookup to 'local' left the suite green. Correcting it exposed three existing SSH reattach tests that had been relying on that looseness — their workspace session sat in the local partition while their repo was SSH-hosted, a store production would never read. Production was always right; the tests described an impossible world. Fixes STA-3465. |
||
|
|
9accd97bd9 |
fix(browser): stop an over-limit screencast frame from killing the paired runtime socket (#12680)
Opening any webpage in the remote browser dropped the paired runtime connection, and the client then retried forever without recovering. Causal chain: the screencast travels host->client, a direction that admits up to 8 MiB. The host's encrypted channel rejects anything larger with close code 1013 "Outbound reply buffer overflow" — killing every subscription on that connection. The producer treats a false return as backpressure and retries the identical frame, which for an over-limit frame can never succeed. A permanent condition was being treated as transient. Two changes: 1. A paired-runtime admission wrapper: an over-limit frame is dropped rather than handed to the transport, and reported as handled so the producer advances instead of retrying something doomed. The generic Chromium producer is untouched, so local browser behavior is unchanged. 2. The actual source of over-limit frames. Live frames are hard-bounded by maxWidth/maxHeight, but the navigation snapshot path ignored those bounds entirely, feeding capturePage device pixels straight into the encoder — capturePage's rect is CSS pixels while the bitmap is device pixels, so at deviceScaleFactor 2 a snapshot could be 4x the pixel area the live path is allowed to send. That path fires on page load, which is literally the reported trigger. Applying the caller's own clamp there makes the drop a backstop rather than the mitigation. Dropping a frame is safe here because frames are complete standalone images, not deltas — each replaces the client image wholesale, so the next frame fully repaints. Disclosed in the PR: mobile web-view mode sends no viewport and takes the unclipped screenshot branch, where the drop guard remains the only protection; still strictly better than a 1013 that kills every subscription. Verified by reverting in place: neutralizing the admission guard fails 3 oracles, with the integration test emitting the real [1013, "Outbound reply buffer overflow"] from an actual E2EEChannel — the production symptom, not a mock. Neutralizing the snapshot clamp fails its own oracle, re-proven after the test was relocated. The second half of the report — never recovering without an app restart — is only partly addressed here and is now tracked as STA-3483: the browser stream restart arms a single 500ms retry and never reschedules, so any connection loss can strand the pane. Fixes STA-2970. |
||
|
|
950985645d |
fix(runtime): retire an exited pane's surface in its owning host partition (#12671)
`retireMobileSessionSurfacesForPty` called `getWorkspaceSession()` / `setWorkspaceSession()` with no host id, so every retirement wrote to the LOCAL partition — while its sibling `retirePersistedStablePaneOwner` correctly scopes to the SSH execution host. For an SSH pane exiting cleanly this is not a harmless misdirected write. Measured on main: the write went to the local partition instead of `ssh:conn-1`; the SSH partition still held the dead PTY binding; the local partition gained a bogus topology revision for an SSH repo; and the published tab list contained a RESURRECTED leaf hydrated back from the stale SSH partition. The wrong-partition write was accepted — a tombstone recorded and the revision advanced for a surface that could not be found. Found during independent review of #11542; pre-existing, not caused by it. Fixes STA-3463. |
||
|
|
4e370062a8 |
fix(remote-runtime): make hidden-output recovery reason-driven instead of timer-guessed (#12655)
When a remote terminal tab is hidden, the host stops sending its output and discards what it queued, so on reveal the only way to recover the missed output is to ask the host to serialize its buffer. That reply was ambiguous — one empty answer covered several unrelated situations — so the client inferred "output is lost" from elapsed time, using budgets sized for local IPC. Over a network that guess was routinely wrong: users saw "[Orca skipped hidden terminal output because main recovery was unavailable.]" on a healthy pane and got a permanent scrollback gap, worst exactly when an agent was streaming heavily and there was the most to lose. The key insight is that there is no provable-absence case at all. A pane with genuinely no retained output returns a SUCCESSFUL snapshot with empty data, because the host serialized fine and found nothing. The real defect was the host sending an untagged empty reply when no serializer answered — reporting an unprovable failure as proven emptiness. The host now states why a snapshot is unavailable and the client acts on that reason: an empty snapshot is success; retry-worthy retries and then gives up honestly; permanently-unavailable banners immediately with no waiting; and a host too old to say latches that pane to the pre-existing timer heuristic. Local panes are unchanged. The self-heal repaint no longer yanks the viewport of a user scrolled back reading — it waits for the terminal to return to following output. Retries are bounded by COUNTING REPORTED OUTCOMES, never elapsed time. Independent review found that the single budget also charged attempts for causes returned locally, where the host was never asked — meaning a re-arming resync could exhaust it and banner on a perfectly healthy host, a residual instance of this very bug. Host answers and local gates now have separate budgets; local gates send zero frames, so retrying them cannot pressure the host. Review also found a duplicate-banner path where a repaint timer armed before a permanent answer survived the abandon; the clear is scoped to the branch that banners, since the retry loop deliberately arms that timer. Wire change is additive: an optional field on an existing frame, dropped on the success path, so old clients see an unchanged frame. STA-3476 tracks replacing the legacy-host detection (currently inferred from an absent field) with a positive capability signal. Closes STA-3457. |
||
|
|
003114dfad |
fix(remote-runtime): stop the host tab mirror from fighting renderer-owned agent status (#12641)
A remote-paired terminal tab flickered several times per second between the agent-generated title with a running status, and the plain title "Terminal" with "Done - Claude" in the sidebar.
Two writers owned the same state. For remote panes the client parses agent status out of the terminal byte stream, while every host tab snapshot rebuilt the mirrored tab WITHOUT the client's generated title and re-decided status by comparing timestamps taken on two different machines. The host also treated a neutral live title ("Terminal") as proof the agent had finished, and re-stamped that conclusion with the pane's last-output time — so it advanced with every output byte and always looked newer. Neither writer could ever win.
This removes the second writer rather than trying to arbitrate two clocks: the client is authoritative for panes whose status it parses (only while attached, released on teardown), the host no longer invents a finished state from a neutral title, and the generated title is carried through snapshot rebuilds. The client was chosen as the authority because the host snapshot format carries no generated title at all — making the host authoritative would permanently lose generated titles on paired clients.
Purely local and plain SSH panes are structurally unaffected: they have only one writer.
Verified with a reproduction that is red on main (frames show done -> working -> done with the label flipping on every publication) and green with the fix. Independent review additionally found and fixed a defect where a superseded pane's late cleanup could permanently strip a live pane's authority, reinstating the very flap being fixed.
Deferred follow-up STA-3455: host `blocked`/interactive-prompt states can still pierce the fence and fall back to cross-machine timestamps; fixing that properly needs an origin marker on the status entry.
|
||
|
|
eebaf47df0 |
Add 'Has Workspace' mode to show Linear issues linked to local worktrees (#12632)
* feat(linear): add 'Has Workspace' mode to show issues linked to local wo Enable users to view and open existing workspaces attached to Linear issues instead of accidentally starting duplicates. Includes shared worktree attachment labeling for consistent UX across GitHub and Linear surfaces. * fix(linear): apply search filter in 'in-orca' mode to prevent drops - Apply search filter in 'in-orca' mode even without active context label to prevent team filters from silently hiding linked tickets (no "Fetch more" recovery path) - Add aria-label to workspace-open button for accessibility - Update tooltip from "local worktree" to "Orca workspace" - Reorganize i18n: move workspace.open from lib.linear to components.issue - Expand test coverage for workspace start and activation scenarios * fix(linear): avoid mutating in-orca linked refs during render React Doctor fails static analysis when refs are written during render. Keep the latest linked refs in an effect so the in-orca loader can still read them without re-running on identity-only worktree churn. |
||
|
|
c736031773 |
Fix setup-gated agent startup on long worktree paths (#12623)
* fix(worktrees): preserve gated agent startup on long paths * fix(wsl): forward sequenced agent startup env |
||
|
|
fe72eeb75c |
Add linked issue guidance and ELI5 sections to PR generation prompts (#12613)
* Add linked issue guidance and ELI5 sections to PR generation prompts Include linked GitHub issues in PR descriptions with Fixes/Refs guidance, and require ELI5 Problem and Solution sections before implementation details. Tests verify linked issue substitution and prompt structure enforcement. * Include linked issue details in PR description generation - Fetch the linked GitHub/GitLab issue title and body so generated PRs reference real issue context instead of just a number - Use provider-specific reference syntax (Fixes/Refs, Closes/Related to, AB#) and label the issue by the active provider - Feed issue title and description into the generation prompt while treating them as untrusted context, never as instructions - Fall back to a cached work-item title when the provider lookup fails, and skip cross-provider issue attachment |
||
|
|
0ce108d935 |
fix(browser): add native-UA session profiles (#12608)
* fix(browser): add native-UA session profiles * test(browser): add Google sign-in UA probe * fix(browser): preserve native profile UA identity |
||
|
|
5ed45739e9 |
fix(runtime): make sibling-workspace terminal-path resolution an explicit client opt-in (#12616)
files.resolveTerminalPath began returning a foreign worktree id + relativePath for absolute paths owned by a sibling workspace, with no protocol or capability gate. Mobile 0.0.36 in the field ignores resolved.worktree and reuses its own worktree id for the follow-up files.open, so a tap on a sibling-worktree path opened the WRONG worktree's copy of that file (on 1.4.168 the tap was a safe no-op). Gate the sibling-workspace lookup behind a new optional crossWorkspace request field: clients that honor resolved.worktree opt in; everything else keeps the pre-sibling-resolution contract. Old servers strip the unknown field (zod), so every version pairing degrades to the safe legacy behavior. Optional-field addition, so no RUNTIME_PROTOCOL_VERSION bump per protocol-version.ts rules. The terminal-path RPC tests move to files-terminal-path-resolution.test.ts because files.test.ts sits at the max-lines cap. Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
39c3c58d55 |
perf(runtime): gate terminal.list visual layouts (#12450)
* perf(runtime): gate terminal.list visual layouts and stop the false writable claim visualLayouts is ~31% of a large terminal.list payload (44,208 B of 137,412 B on a live 134-terminal remote runtime) and has exactly one consumer: the human-readable CLI formatter. Gate it behind an includeVisualLayouts request param that defaults to included, so pre-flag clients are unaffected, and have every --json/internal caller opt out. Also drop the record-backed builder's writable, which was a verbatim copy of connected. terminal.show now states writability explicitly as exactly what terminal.send's PTY gate enforces. * test(runtime): type the payload-size fixture arrays for tsc * fix(runtime): preserve terminal list compatibility * test(runtime): guard terminal list optimization * fix(cli): preserve agent access to terminal layouts |
||
|
|
72245918a1 |
fix(terminal): attach never-activated daemon sessions on remote subscribe and provider-read fallback (#12589)
* fix(terminal): attach never-activated daemon sessions on remote subscribe and provider-read fallback A daemon-backed terminal whose tab was never activated in the host UI was never attached, so the daemon emitted no bytes: paired clients rendered blank/frozen panes and `terminal read` returned an empty tail while the PTY was alive. - Runtime: first remote view subscriber of a known-but-unattached local daemon session triggers an attach through the pty controller — attach-only, no resize, no renderer mount/focus, headless-safe, deduped across concurrent subscribers, and never detached on release. Excludes SSH-scoped ids and sessions a local spawn already published this generation. - Read path: withVisibleSnapshotFallback now falls back to the provider tail for an empty-tail never-attached live local session; unprovable state stays empty, never an error. - pty controller: expose attach with getProviderForPty-style routing, answering false on doubt; local daemon provider only. - Daemon adapter: attach rides the session's applied size instead of a hardcoded 80x24, sends attachOnly, and retires a pre-v31 daemon's accidental spawn instead of publishing it. Deterministic harness drives the real terminal.multiplex handler against a real OrcaRuntimeService with an injected daemon-model controller whose data events are gated on attach; covers snapshot-capable and snapshot-null daemons, concurrency, release, replacement-spawn exclusion, and negative safety. Red on base, green with the fix, red again with the fix reverted. * fix(terminal): refuse degraded-provider attach fallback and surface failed legacy-spawn retire Verifier follow-ups on subscriber-driven daemon attach: - DegradedDaemonPtyProvider.attach routed unknown ids to the in-process fallback, whose no-op attach resolves — the runtime then pinned a subscriber-driven attach as succeeded while the stream stayed blank. Attach now refuses any route that resolves to the fallback (a fallback pty cannot own a daemon-surviving session), so the controller answers false, no sticky success is recorded, and a later subscriber attaches once a daemon adapter proves the id. Session-probe adoption moved to degraded-daemon-session-routing alongside the new refusal. - The pre-v31 attach-only TOCTOU retire (accidental legacy spawn kill) now logs a warning with the sessionId on kill failure instead of swallowing it, so an orphaned replacement shell is diagnosable. Regressions: degraded provider refuses unowned/fallback-owned attach and routes to a daemon once it proves the id (red on previous commit); runtime harness pins refused-attach retry for a later subscriber; adapter test pins the surfaced kill failure. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
51ca82d028 |
fix(runtime): seed terminal previews and titles from restore payloads (#12579)
* fix(terminal): seed list/read records from reattach restore payloads After an app relaunch the PTY daemon survives and spawn silently reattaches, but the restore payload (reattach snapshot, cold-restore scrollback, relay replay, lastTitle) arrives as a spawn RPC result and never passes through runtime.onPtyData — the only feeder of the terminal records behind `terminal list`/`terminal read`. Every restart therefore left connected terminals with empty title/preview/lastOutputAt and a zero-line read tail, blinding orchestrators that poll terminals. The spawn flow now calls runtime.seedTerminalRestoreTail with the restore text and lastTitle, unconditionally of the renderer-authority emulator gate (the records are main-side only). The seed reuses the live path's normalize/tail/preview pipeline on a capped 256 KiB suffix (re-anchored at a line boundary so a cut escape cannot leak), only fills records that never saw output (a remount reattach cannot re-apply history), routes titles through the applySeededAgentStatus precedent (state writes only — no waiters, no side-effect facts), and never stamps lastOutputAt or waitBlockedAt: restored bytes are historical, not fresh activity. lastTitle is threaded from the daemon reattach snapshot and cold-restore checkpoint into PtySpawnResult; relay replays seed preview only. SSH and runtime-controller paths are unchanged — seeding is gated on the fields existing. * fix(terminal): seed restore records on the controller spawn path and prime the wait baseline Follow-ups to the restore-record seed, from independent verification: 1. The runtime-controller spawn flow (createTerminal background creates — headless `orca serve`/CLI — and pane splits) never consumed restore payloads, so the exact orchestrator-blindness this fix targets survived on the topology that needs it most. The extraction now lives in one helper called from both spawn choke points (renderer pty:spawn and the controller flow); the runtime's empty-record guard makes overlapping seeds a no-op. 2. The throttled per-PTY wait scanner starts with a null baseline, so a permission prompt visible only in seeded HISTORY read as newly gained on the first benign live chunk and stamped waitBlockedAt "now". Seeding now primes the scanner baseline from the seeded tail without stamping; only a signal appearing in genuinely new output counts. 3. Cap re-anchoring accepts \r as well as \n (newline-free CR-redraw streams), consuming a full \r\n pair so the seed does not start with a phantom blank line. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
3d8131d7ea |
fix(runtime): reject leaf terminal sends only on controller-proven PTY absence (#12578)
* fix(runtime): reject leaf terminal sends only on controller-proven PTY absence orca terminal send to a leaf whose ptyId no provider in this process owns was a silent no-op reported as success: the graph mirror answers writable=true, every provider write to an unknown id is accepted fire-and-forget, and bytesWritten is computed from the payload rather than delivery. sendTerminal and sendTerminalAgentPrompt now consult a controller liveness probe when the provider does not synchronously know the id (hasPty), and throw terminal_not_writable only on an exact false — unknown liveness, probe errors, SSH/remote scopes, and probe-less providers never reject (#12393's rule: null is not absence), so a restored daemon session still accepts writes before its pane remounts. Push-on-idle orchestration delivery gains the same gate so a proven-dead leaf keeps its messages queued instead of marking them delivered into a void. The pty controller now exposes probePtyLiveness, routed like write: a provider probe is preferred, the in-process local provider's refusal is authoritative (sole owner), and remote-scoped or SSH ids without a probe answer null after awaiting the cold-start daemon swap. Proven-absent verdicts cache 15s per ptyId with in-flight dedupe, superseded the moment the provider re-learns the id. * fix(runtime): arm one probe-deferred delivery continuation per pty Review (GPT verifier) confirmed: triggers arriving during one in-flight absence probe each attached a continuation to the deduped probe promise, and since Claude-target delivered_at stamps only after the delayed Enter, every continuation re-read the same unread rows — double payload injection and two armed Enters. Single-flight the deferred continuation per pty; the one armed continuation re-reads fresh rows when it fires, so nothing is lost, and the guard clears on settle so later triggers defer again. The narrower pre-existing 500ms sync-path window is unchanged and out of scope. * fix(runtime): single-flight the whole orchestration delivery window per pty The probe-continuation guard cleared at probe settle, but Claude-target delivered_at stamps only in the delayed-Enter callback ~500ms later — a trigger landing in that gap armed a fresh probe cycle, re-read the same un-stamped rows, and re-injected the payload. The identical window existed on the pure sync path pre-PR (two triggers within 500ms double-deliver). Hold a per-pty delivery-in-flight flag from before the payload write until delivery settles: entry-checked before reading unread rows, cleared through one settle point covering the failed write, the sync-stamped coordinator and Cursor branches, any sync throw, and the delayed-Enter callback on submit, refusal, and throw alike. A trigger arriving mid-flight is not dropped — it parks the latest leaf per ptyId and re-runs delivery once on settle, so rows inserted mid-flight deliver without waiting for the next idle event. The probe single-flight stays; the new guard subsumes its post-settle gap, and no trigger site bypasses it. Both strengthened tests are red on the previous commit (first subject injected twice) and green here: in-window re-trigger on the probe path and sync-path double-trigger each deliver the first batch exactly once, with the parked second row delivering alone after settle. * fix(runtime): retire the armed delivery Enter on pty exit; guard fire-time on current state Two variants of one root cause — the delayed-Enter callback outliving the session it was armed for: 1. Cold restore respawns under the same session id. onPtyExit never cancelled the armed Enter or the in-flight delivery state, and onPtySpawned flips the same leaf writable again — so an exit + same-id respawn inside the 500ms window let the stale callback inject \r into the replacement session and stamp rows it never received, then settle against a newer same-id flight. 2. Graph resync replaces leaf objects, so onPtyExit flips writable=false only on the current replacement; a callback trusting its closed-over snapshot still read writable=true and fired after exit with no respawn. The flight record now carries its armed Enter timer and serves as settle identity: onPtyExit clears the timer and drops the flight and any parked re-delivery without stamping (rows stay unstamped and re-deliver on the replacement's next idle — the existing contract), and settle no-ops unless its own flight is still current, so a stale settle can never clear a newer same-id flight or flush its parked trigger. At fire time the callback re-resolves the leaf by key and requires the same ptyId binding and current writability instead of reading the closure snapshot. All three regressions are red on the previous commit: same-id respawn saw \r plus a false delivered_at stamp, exit leaked the flight and parked state, and the orphaned-snapshot resync variant fired Enter after exit. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
27da04d50d |
fix(terminal): truthful handle liveness + no forked resume tabs for hidden restorable panes (#12574)
* fix(runtime): report terminal handles disconnected on controller-proven PTY absence leaf.connected mirrors the renderer graph (ptyId !== null), so a restored surface whose PTY died with a prior process was listed connected/writable forever with empty title/lastOutputAt/preview — the exact signature automation saw on run6 workspaces after a restart. listTerminals now threads the controller inventory it already fetches into buildTerminalSummary and demotes only on proven absence, only for locally-scoped ids; unknown liveness and SSH/remote scopes never demote, and no session or pane is retired. * fix(terminal): stop forking hidden restorable panes into replacement resume tabs paneWillConnectOnActivation still assumed the pre-keep-alive mount model, but every non-parked tab of the active worktree mounts and connects hidden at 0x0. Activation therefore appended a replacement resume tab per non-group-active agent pane and handed it the sleeping record, stranding the hidden pane as a bare shell — or forking two live surfaces onto one provider session when the old PTY survived in the daemon. The predicate now answers "will mount and connect": any non-web-mirror tab of the active worktree qualifies; non-active worktrees still answer false so background wake keeps its append-based resume. Contract change: reverses the hidden-tab expectation from #6800, whose premise (hidden panes never connect) no longer holds; that test is updated in place. * test(terminal): pin the remote-scope exemption and the web-mirror ownership exception CodeRabbit flagged both exclusions as untested: a remote-runtime-scoped leaf absent from the local inventory must stay connected (its inventory lives on the remote host), and a web-mirror tab must not own sleeping-session recovery (it never mounts a local pane), so the appended replacement remains its correct resume path. * fix(terminal): rescue just-spawned ptys from absence demotion; unpark panes owning sleeping records Review (GPT verifier) confirmed two gaps: - listTerminals demoted a live just-spawned PTY when listProcesses snapshotted before session registration (the sweep's hasPty rescue is leaf-gated), and federation reads one connected:false as exited. The summary's proven-absence check now also consults the provider's sync hasPty. - Ordinary per-tab cold parking (30s hidden) kept a non-group-active pane unmounted, so a sleeping record it owns under the new ownership predicate could not cold-restore until the user revealed the tab. Per-tab parks now exempt panes owning a sleeping-session record; worktree-level parks are untouched (they clear on activation). * fix(terminal): reconcile the daemon session cache on inventory; scope the park exemption to consumable records Round-2 review confirmed two holes in the round-1 fixes: - DaemonPtyAdapter.hasPty is cached activeSessionIds membership, and a successful listSessions never removed ids the authoritative inventory omitted — an exit missed while the socket was down kept hasPty true forever, and the new spawn/list-race rescue would trust it, reopening connected-forever for that pty. listProcesses now drops pre-request cached ids the inventory does not list alive (ids spawned mid-flight are snapshot- protected). - The park exemption covered records a pane can never consume (automaticResumeBlockedBy, passive-completed evidence), pinning hidden panes mounted indefinitely. The exemption now lives in sleeping-record-park-exemption.ts and requires a consumable record. Also pins the web-mirror replacement's resume claim and startup command (CodeRabbit round-2). --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
7956335cea |
fix(setup): stop caching an unreadable orca.yaml as "no setup script" (#12469)
* fix(setup): stop caching an unreadable orca.yaml as "no setup script"
`checkRepoHooks` returned `{hasHooks:false, hooks:null, mayNeedUpdate:false}` with no `status` field when the SSH filesystem provider was unavailable, and inside a blanket catch for any read error. The renderer only bails on `status === 'error'`, so that status-less false negative was cached as an authoritative "no setup script" and the prompt stayed on screen.
Mirror the `hooks:check` IPC twin exactly: `status:'error'` for a missing provider, ENOENT-aware in the catch, `status:'ok'` on the folder-repo, binary, SSH-success and local branches.
Fixes #8752
Co-authored-by: Orca <help@stably.ai>
* test(e2e): add recordable proof for setup-script-prompt-false-negative
Fails on origin/main, passes on this branch.
Test: recovers from an unreadable orca.yaml instead of pinning the failed verdict
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
|
||
|
|
8c65dd5094 |
perf(runtime): keep PowerShell ACL work and a second auth off the remote command path (#12451)
* perf(runtime): keep PowerShell ACL work and a second auth off the remote command path Two costs sat on the remote authentication path on Windows: - The E2EE handshake persisted `lastSeenAt` inline, and every secure-file write spawns PowerShell synchronously twice to reapply the registry ACL, so the client's `e2ee_authenticated` waited on both spawns. - Every remote CLI command except `status.get` opened a second full WebSocket connection just to re-read status for the protocol-compat check, doubling the authentications per command. The first sighting of a device still persists inline (rotation drops entries disk says were never scanned); later refreshes update memory now and coalesce onto one deferred write. The compat verdict is saved against the runtime's per-launch `runtimeId`, so a restarted or upgraded runtime retires it. * fix(runtime): preserve compatibility on one remote auth * fix(runtime): flush registry after transport shutdown |
||
|
|
e9cf106769 |
fix(relay): make fenceAndCloseNow stop the liveness safety net (#12482)
The 5-minute liveness tick from #12432 survived fenceAndCloseNow(), so a tick landing between the pre-sign-out fence and the profile wipe could briefly resurrect a broker (benign but soft — the entitlement check bails afterward). The fence now clears the interval; the next auth mutation re-arms it via refreshDemand, and the safety net otherwise behaves identically. Found in release review of the #12432 cherry-pick. Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
8f29f58ab7 |
Keep showing your workspaces when a project is too slow to answer instead of reporting zero (#12222)
A stalled per-repo git scan no longer publishes a healthy-looking empty catalog. Adds an execution-host ownership gate so a degraded host cannot republish another host's worktree rows under its own id. Relates to #11869 — this fixes the stall-publishes-zero half. The issue stays open for the remainder. |
||
|
|
96e31e7bf8 |
Remove a paired computer's deleted projects from every connected device (#12215)
* fix(repos): remove a paired computer's deleted projects from every connected device A project deleted on a paired Orca host stayed in every connected client's sidebar and could not be removed there. Two independent defects: 1. Host-local repo IPC mutations only sent `repos:changed` to the host's own renderer (src/main/ipc/repos.ts:2711). The runtime client-event stream was fed only by mutations arriving over runtime RPC, and clients refetch a remote catalog only on a `reposChanged` event -- there is no polling on desktop -- so the deleted rows persisted indefinitely. The shared `notifyReposChanged` helper now also calls the new `OrcaRuntimeService.notifyReposChangedForRemoteClients()` (src/main/runtime/orca-runtime.ts:5175), mirroring the existing `notifyWorktreesChangedForRemoteClients` precedent. This covers every repo, project-group and folder-workspace IPC mutation, so renames, colors, reorders and adds propagate too. 2. Deleting the ghost row on the client routed `repo.rm` to the owner, which answered `repo_not_found`. `removeProject` wrapped its whole body in one try/catch, so the rejection aborted the local purge before the `set()` (src/renderer/src/store/slices/repos.ts:3466) and the delete button appeared to do nothing. Only `repo_not_found` is now tolerated; any other failure still keeps the row, and an opt-in `errorFeedback: 'toast'` makes it visible at the three single-project user-initiated entry points. Bulk and background callers keep today's silence plus their own aggregate reporting. Closes #11994 Co-authored-by: Orca <help@stably.ai> * fix(repos): revert inert RepositoryPane removeProject arg The settings pane's only render site drops the argument; the toast is already delivered by removeSettingsProjectFromAllHosts. Co-authored-by: Orca <help@stably.ai> * fix(repos): scope duplicate-repo-id deletes to the owning execution host Cover the cross-host collisions #11994's broadcast now fans out to every paired device. Same-name projects on different hosts were already isolated (per-host UUIDs, host-scoped catalog merge and purge) and are pinned by regression tests. Two same-repo-id paths were not: `repo.rm` with a `path:`/`name:` selector and `deleteProjectHostSetup` both resolved one row and then deleted by bare id, taking the sibling host's registration with it. Co-authored-by: Orca <help@stably.ai> * test(mobile): align the poll-interval rationale with the new reposChanged emission Co-authored-by: Orca <help@stably.ai> * fix(repos): resolve deleteProjectHostSetup's repo row only on the setup's own host The sibling-host fallback could only ever pick a row on a host the caller did not name; with no exact match the setup is stale and the existing path already drops just the setup. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
f6878d660f |
Show Claude's AskUserQuestion card in desktop Chat when the agent runs on a paired headless server (#12223)
* fix(native-chat): show Claude's AskUserQuestion card when the agent runs on a paired headless host Three gaps kept the question card off the desktop when the agent ran on a remote `orca serve` host: - The `session.tabs` projection reduced HTTP agent-hook rows to identity only, hard-coding `state: 'done'` and an empty prompt, so `toolName` and the full `interactivePrompt` never left the host. It now publishes the newest fresh hook row's status fields, bounded by the same staleness window `agentType` uses, excluding `providerSessionOnly` resume rows, and yielding to live title evidence unless a question is actually pending. - Nothing republished `session.tabs` when only a hook row changed, and the re-emit carried an unchanged `snapshotVersion` that clients drop on their monotonic gate. Material hook transitions and pane/SSH status clears now bump the version and schedule a coalesced emit. - The desktop card resolved only from live status. It now falls back to the pending ask in the transcript, matching mobile, so a relay gap can no longer leave the composer mounted over a pane parked on a selector. Closes #11761 Co-authored-by: Orca <help@stably.ai> * fix(native-chat): date the hook-row recency guard against a real clock `resolveHookLiveAgentRow` compared a hook `receivedAt` (epoch ms) against title stamps that are title-observation sequence numbers, so the guard could never fire — any fresh hook row overrode live title-derived state, and a manual rename (the one epoch writer) inverted it. Stamp the live OSC title path with wall-clock ms and compare against that alone. The regression test fabricated epoch-valued title stamps production never writes; it now drives the title through `onPtyData`, and a new case pins the opposite direction (hook row newer than the title wins). Co-authored-by: Orca <help@stably.ai> * fix(native-chat): stop an orphaned tool call from pinning a dead question card extractPendingAsk pairs tool results to calls by a global FIFO (tool_use_id is dropped at decode time), so one call that never gets a result desyncs the queue for the rest of the transcript and strands an answered ask as pending. Real transcripts also hold asks the user escaped and typed past. On desktop that card replaces the composer, so the pane became unsendable. Drop in-flight calls at a turn boundary — a user turn or the decoders' interrupt row — since the turn that owned them is over. Claude's tool-result turns decode as role 'tool', so normal FIFO resolution is untouched. Co-authored-by: Orca <help@stably.ai> * refactor(native-chat): trim the headless AskUserQuestion projection Reuse rather than restate: the invalidator now takes the shared `AgentHookEventPayload` instead of a locally redeclared row shape, and the hook live row is a `Pick<>` of the retained OSC snapshot so one projection branch consumes either carrier. Fold the immediate/coalesced session-tabs emit into one method (also drops a redundant re-emit on the provider-session push). Drop card tests that re-route shared-parser assertions through React. Isolate pane-status-clear subscribers and prove the no-republish case by version arithmetic instead of a timed silence. Co-authored-by: Orca <help@stably.ai> * test(native-chat): pin the AskUserQuestion card render under real Electron Why: the 13 parser unit tests pin extraction, but nothing proved a card actually renders where an inert tool call used to. This spec reproduces the paired-headless topology from the client side — live status carrying agent identity and state 'working' but no interactivePrompt/toolName, with the pending ask present only in the transcript — and fails on main. Refs #11761 Co-authored-by: Orca <help@stably.ai> * test(native-chat): drop the unused testInfo parameter Why: oxlint no-unused-vars fails the lint gate on an unused test parameter. Co-authored-by: Orca <help@stably.ai> * test(native-chat): drop leftover proof scaffolding from the ask-card spec The env-var screenshot label and the fixed 2s settle only existed to make the pre-fix capture comparable; the card assertion already waits. Co-authored-by: Orca <help@stably.ai> * test(runtime): use a truly unresolvable pane key in the hook republish guard #11203 taught pane lookup to recover a reminted tab id by leaf id, so the old fixture (new tab id, live leaf id) resolved and bumped the snapshot a second time once this branch merged with main. Co-authored-by: Orca <help@stably.ai> * fix(runtime): refuse a hydrated unconfirmed hook row as live pane status #12346 landed on main after this branch was cut: a nonterminal row restored from last-status.json is stamped `restoredUnconfirmed` because its transition may have fired while no receiver was up, and every freshness gate treats it as never-fresh. The new headless `live` projection here only checked `receivedAt`, so a restart inside the 30-minute window would republish the hydrated row — resurrecting the AskUserQuestion card with no agent left to answer it. `agentType` still reads those rows: they prove identity, just not liveness. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: Neil <nwparker@users.noreply.github.com> |
||
|
|
ed7849eb7b |
fix(worktrees): stop silently switching existing Windows setup scripts to Git Bash (#12406)
* fix(worktrees): stop silently switching existing Windows setup scripts to Git Bash #6967 derived the Windows setup-runner shell from `terminalWindowsShell`. On upgrade, any Windows user whose terminal preference resolved to Git Bash had their existing `orca.yaml` setup script (and issue command) handed to bash instead of cmd.exe. Scripts authored against the cmd runner — `copy`, `xcopy`, `set VAR=value`, `if errorlevel 1`, `%VAR%`, backslash paths — broke with no migration and no warning, and the failure looked like Orca broke the project. The conflation is also wrong in the steady state: a terminal preference is per-user, so two people on the same repo got different interpreters for the same orca.yaml and no project could write a setup script that worked for all of its Windows contributors. The interpreter is now a property of the script, declared the standard way: a leading `#!` line. Native Windows keeps the historical `.cmd` runner unless the script declares a POSIX shell, so no existing script changes behavior. `resolveSetupRunnerShell` keeps its role as the feasibility gate — a bash runner still requires the terminal to resolve to Git Bash, since the launch command is typed into that shell and uses MSYS `/c/...` paths. `buildWindowsRunnerScript` now drops a leading `#!` line rather than `call`ing it, so a declared-bash script that falls back to cmd (Git Bash missing) fails on a real setup line instead of aborting on errorlevel at line one. WSL worktrees, POSIX platforms, and SSH hosts are untouched. * fix(worktrees): keep the cmd setup runner launchable from a Git Bash pane Adversarial review of this PR found that pinning the runner format per script reopened issue #6896 one layer down. - `WorktreeSetupLaunch.shell` had been redefined to mean "the format the runner file was written in". `resolveSetupRunnerCommand` consumes it as "the shell that types the launch command", so a Git Bash terminal with a batch setup script produced `cmd.exe /c "C:\...\setup-runner.cmd"` typed into a bash pane, where MSYS rewrites the `/c` switch into a drive path: cmd opens interactively and setup never runs. `shell` is the terminal's family again; the runner file's .cmd/.sh extension carries the format, and a batch runner launched from a POSIX pane reuses the existing PowerShell ProcessStartInfo launcher. - The cmd runner dropped a leading `#!` line and ran the rest as batch, so a bash script reaching cmd (PowerShell/cmd terminal, or any SSH-to-Windows host) got its interpreter-agnostic prefix executed before failing mid-way. It now prints why and exits 1 without running anything. - A `#!` line's option flags were discarded: `#!/usr/bin/env -S bash -euo pipefail` lost pipefail because the runner is launched as `bash <path>`. The generated posix runner now replays declared flags via `set` and drops the duplicate interpreter line. - Docs cover the per-user setup command in repository hook settings, which goes through the same `#!` rule, and describe what the `#!` line does and does not select. Tests: composed launch command for a POSIX pane + cmd runner (hooks, shared runner command, setup sequencing gate, observed-setup signal), the cmd runner's shebang refusal, and shebang flag replay. Each fails with the source reverted. * fix(worktrees): replay only real `set` flags and keep the gate in the pane's shell Two round-2 review findings: - `#!/bin/bash -l` replayed `set -l`, which exits 2 and aborted the runner under its own `set -e` before a single setup line ran (all platforms). Only the flags `set` documents are replayed now; a bare `-o` with no option name is dropped instead of dumping the shell-option table. - The wait-for-setup gate picked its language from the runner file, so a batch runner launched from a Git Bash pane got the PowerShell gate while the agent startup command was already POSIX-quoted — `Invoke-Expression` cannot parse `'\''`. The gate now follows the pane; the runner still launches through the ProcessStartInfo launcher, never through bash. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
141b1f43f6 |
fix(runtime): keep the listener on loopback for a "This computer only" pairing link (#12405)
* fix(runtime): keep the listener on loopback for a "This computer only" pairing link The runtime pairing URL handler called ensureNetworkExposure() for every offer, including one whose advertised address is loopback. Settings -> "Share this Orca server" offers a "This computer only" radio that pairs against 127.0.0.1 precisely so nothing is reachable off-host, yet choosing it rebound the WebSocket listener from 127.0.0.1 to 0.0.0.0 — and the widen never narrows back, so the runtime stayed exposed to the whole LAN for the rest of the process after the user picked the option that exists to avoid exactly that. Gate the widen on the advertised address: only a non-loopback endpoint (LAN, Tailscale, custom host) needs a listener reachable off this machine, so those paths keep widening exactly as STA-2370 intended. A loopback link is already served by the loopback listener, so it now mints without touching the bind. Classification reuses the shared pairing-address classifier, which also covers localhost, ::1 and 127.0.0.0/8 typed into the custom-address field. Tests: a real OrcaRuntimeRpcServer driven through the IPC handler asserts the bind host stays 127.0.0.1 after a local link and flips to 0.0.0.0 after a LAN one, plus handler-level cases for 127.0.0.1 / localhost / ::1. * fix(runtime): gate the pairing widen on the user's declared reach, not the address shape Review of #12405 found two ways the loopback fix misbehaved. 1. The guarantee died at the next launch. resolveInitialWebSocketBindHost() binds 0.0.0.0 whenever any device has lastSeenAt > 0, and MobileSocketWiring stamps that for EVERY authenticated socket — including the local browser opening a "This computer only" link. So the runtime was still published on every interface, one restart later. Grants now carry the reach they were minted for (DeviceEntry.pairingReach, persisted); a this-computer grant no longer counts as proof that an off-host client may reconnect. Registries written before the field default to network reach, so an already-paired phone still finds a wide listener after upgrading. A pending grant that is re-advertised for the network widens (never narrows) so its link survives. 2. The widen was gated on the shape of the typed address, which the renderer never sent the intent for. A Custom `127.0.0.1:8443` — the documented SSH tunnel / reverse proxy field — skipped the widen and produced a dead link, while `localhost:8443`, `[::1]:6768` and `ws://127.0.0.1:6768` widened, so the same loopback intent was handled three different ways. The renderer now sends the declared reach ('this-computer' | 'network') and main gates on it; the address is only used as a mismatch guard (a this-computer reach carrying an off-host address still widens rather than minting an unreachable link), resolved through resolveAdvertisedPairingHostname so every accepted address form classifies identically. Also corrected the ensureNetworkExposure invariant comment: the widen is no longer confined to the first pairing action, so it can now tear down live loopback sockets — they reconnect on the reused pinned port. Tests: reach-form matrix + tunnel/undeclared/mismatch cases in mobile.test.ts, real-server relaunch bind for both reaches, legacy registry compatibility, the pending-grant reach upgrade, a live-client port-stability guard, hostname resolver coverage, and the renderer reach plumbing. Reverting only the source fails 18 of them. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
20a2901677 |
fix(worktree): tell the truth about live PTYs, and offer force for a wedged sweep (#12394)
* fix(worktree): tell the truth about live PTYs, and offer force for a wedged sweep Two gaps in the #11960 force path: The delete toast described every unstopped-PTY failure as "could not confirm every terminal has exited", including the case where verification positively watched them running. Force Delete proceeds either way, so the user was being asked to waive a doubt that did not exist while a running agent's uncommitted work died with it. The live verdict now gets copy that says so. A sweep that rejects before any per-PTY verdict exists (wedged daemon, dropped SSH channel) fails with a teardown-timeout message that the force classifier did not recognise, so no Force Delete button appeared — the exact dead end #11960 set out to remove. That error now carries the shared prefix and classifies. * fix(worktree): close the sweep-rejection wedge and stop racing the delete Review of #12394 found the fix covered only half the wedge it named, and routed users into a force path whose own safety comment was untrue. 1. Only the outer deadline was classifiable. When a provider *rejects* the sweep — dropped SSH channel, erroring daemon — settleBeforeDeadline rejects with the provider's original error, which carries no marker, so classifyWorktreeForceDeleteReason still returned null and no Force Delete button rendered. That is the exact case #11960 named. A rejected sweep on the destructive path is now reworded through the existing unstopped-PTY prefix (provider text preserved, original kept as `cause`), so old and new clients alike classify it as 'unstopped-pty'. 2. Force could delete files while a sweep was still running. The deadline rejects without cancelling run(), so allSettled resolved with shutdown() still in flight — by construction the deadline error can only fire while something is in flight. Force then deleted the directory a live PTY still held open (EBUSY / half-delete on Windows and WSL). Sweeps are now tracked so the forced path waits for the abandoned work, bounded by a 2s grace; force never wedges, and when the grace expires the warning says handles may outlive the delete instead of implying the sweep finished. 3. The toast test named for the classifier passed the reason in as a literal, so it never exercised it. It now derives the reason exactly as the store does, and fails against main. 4. Added the missing unstoppedPtyLive key to the English catalog. 5. isProvenLivePtyRemovalError anchored the 'still live:' marker to the detail separator, so a worktree path can no longer spell out a live verdict and flip the toast to the destructive copy. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
665b85047b |
fix(relay): revive a dead relay broker without user interaction (#12432)
Broker deaths that end with closeNow() — an auth refresh failing past token expiry (laptop sleep) or a transient context read at open — left no retry timer, so Relay stayed offline until the user clicked Retry or auth state changed. Adds a dead-man's switch: - RelayAuthCoordinator.ensureLive(): reconciles only when there is no live broker, no scheduled retry, and no open in flight - DesktopRelayService arms a 5-minute liveness interval and exposes ensureLive() for wake signals - powerMonitor 'resume' triggers ensureLive (sleep is the common cause) Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
e59a319ffe |
fix(sidebar): keep each project's entry-point workspace visible under "Hide sleeping" (#12257)
"Hide sleeping" swept each project's main workspace out of the sidebar as soon as it had no live PTY, browser tab or agent — even with "Hide default branch" off. For a project whose only row is that workspace (a folder workspace, a fresh clone, a detached-HEAD main), the entire project vanished with no in-place way back. Adds a shared `isSleepingSweepExemptWorkspace` predicate keyed on `isMainWorktree` rather than the branch name, so folder workspaces (no branch), detached-HEAD mains, and SSH rows whose head/branch are blanked while a provider is disconnected all stay put. Wired into `computeVisibleWorktreeIds` (sidebar, Cmd+1-9, workspace board), the jump palette's duplicate inline pass, and mobile's `filterWorktrees`. Ships default-on with an escape hatch: a persisted `alwaysShowDefaultBranchWorkspace` setting surfaced as "Except default branch" under "Hide sleeping". Explicit "Hide default branch" still wins, since it filters before the sleeping sweep. Mobile reads the setting but never writes it back, so a desktop opt-out can't be clobbered by a filter tap before the ui.get roundtrip lands. Combines the two PRs open against #8873. #8966's exempt set is a strict subset of this one, so its production diff was subsumed rather than ported; its jump-palette render harness and e2e spec were carried over, and are the only such coverage here. Fixes #8873 Closes #8966 Co-authored-by: Rod Boev <rod.boev@gmail.com> Co-authored-by: Orca <help@stably.ai> |
||
|
|
0927b9c156 |
fix(gitlab): load pipeline job traces in the Checks side panel (#7732) (#12266)
* test(repro): demonstrate #7732 GitLab pipeline job details never load in Checks panel Co-authored-by: Orca <help@stably.ai> * fix(gitlab): load pipeline job traces in the Checks side panel (#7732) Expanding a GitLab pipeline job in the Checks panel always showed "No inline details are available for this check.": the mapper dropped the numeric job id, `PRCheckDetail` had nowhere to carry it, and every consumer called the GitHub check-runs API, which returns null for a GitLab job. - carry `gitlabJobId` on `PRCheckDetail` and add the `gitlab-job:` branch to all three identity ladders (panel rows, editor tabs, fix-prompt keys) so same-stage jobs with no web_url stop colliding - add a runtime-routed trace client so SSH/remote workspaces work, not just local IPC, and thread the MR's `projectRef` for fork pipelines - bound the trace in main via the existing `sliceCheckLogTail` (now shared, not GitHub-only) so a multi-megabyte CI log never crosses the 1 MB transport frame cap; strip ANSI/section markers up to the CR only, which keeps each section's visible header and command echo - render the excerpt inline instead of "Log tail available in full details." - feed GitLab traces to "Fix with AI", which previously sent bare check names - skip the fetch for jobs that cannot have a trace (created/manual/skipped) so GitLab's 404 does not replace the benign empty state, and re-arm a failed load when the job's state changes since the panel has no retry Co-authored-by: Orca <help@stably.ai> * fix(gitlab): treat a missing job log as an empty log, not an error (#7732) Round-1 review follow-up. - a job canceled before it started (or whose log was erased/expired) is `completed`/`cancelled`, so the panel fetched its trace, GitLab answered 404, and `classifyGlabError`'s issue-edit copy ("Issue not found — it may have been deleted.") landed verbatim on the auto-expanded check row; main now maps that 404 to an empty trace so the row keeps its benign empty state - keep a missing project a real error (GitLab masks unauthorized projects as 404) and add `classifyJobLogError` so 403/unknown failures stop borrowing issue-edit wording on a job-log read - broaden the empty-log copy in all five catalogs: it now covers erased and expired logs, not only jobs that never ran - e2e: derive the repro screenshot dir from `process.cwd()` (or an env override) instead of a hardcoded POSIX path to a throwaway worktree - bound the raw trace before the ANSI/section passes so a multi-megabyte log is not scanned in full on the main-process event loop - drop the redundant `if (repo)` in `handleFixChecksWithAI` and the now-dead "Log tail available in full details." catalog entry Co-authored-by: Orca <help@stably.ai> * fix(gitlab): address review — project ref on reload, retry re-arm, IPC timeout - Carry the MR's GitLab project ref on the check-details tab so reloading a fork/cross-project job tab fetches the trace from the pipeline's own project. - Re-arm the sidebar retry when a details load resolves to null, not only when it throws; a detail-less row otherwise never retried after the job moved on. - Bound the local `gl.jobTrace` IPC call with the same 30s timeout the runtime RPC path uses — glab runs without a subprocess timeout in main. - Document that the trace 404 -> empty-log mapping is deliberately broad. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
e43bd6c4ad | fix(worktrees): preserve folder PTY owner scope (#12429) | ||
|
|
5bd2f59d29 |
fix(runtime): open files from sibling workspaces (#11369)
* feat(runtime): match files to workspace owners * fix(runtime): resolve terminal paths through sibling workspaces * fix(editor): route restored sibling workspace files * fix remote sibling file ownership routing * fix(editor): migrate restored sibling file owners * fix(editor): revalidate restored owner activation * docs(review): record PR 11369 correction evidence * fix(editor): reject collision before activation prep * docs(review): record PR 11369 final correction * fix(editor): retain projected reconciliation narrowing * chore(review): keep verification artifacts out of PR * fix(editor): harden restored owner migration * fix(runtime): resolve workspace root terminal paths --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
3d6d6dd67d |
fix(orchestration): scope agent lineage to its owning run (#11203)
Use durable Task-to-Run ownership and current pane, process-incarnation, and Run-generation authority for sidebar agent lineage. Add schema migrations, bounded lookup indexes, fail-closed renderer cleanup, and runtime/RPC regression coverage. Co-authored-by: Jaeyoung22 <89302528+Jaeyoung22@users.noreply.github.com> |
||
|
|
50594c55a9 |
Stop the Windows Orca CLI from crashing when the environment carries both PATH and Path (#12218)
* fix(windows): stop the Orca CLI dying on a duplicated PATH/Path environment The packaged Windows `orca.exe` launcher read `ProcessStartInfo.EnvironmentVariables`, whose lazy getter copies the case-sensitive process block into a case-insensitive dictionary via `.Add`. An inherited block carrying both `PATH` and `Path` threw `ArgumentException: Item has already been added. Key in dictionary: 'PATH'`, so every `orca` invocation exited 1 before Electron started (native/windows-cli-launcher/OrcaCliLauncher.cs:46, printed at :67). The launcher now mutates its own environment with `Environment.SetEnvironmentVariable` and never touches either `ProcessStartInfo` env property, so `CreateProcess` passes a NULL environment block and the child inherits the live one verbatim. Orca was also minting the duplicate itself. `applyTerminalAttributionEnv` read `baseEnv.PATH` and unconditionally wrote `baseEnv.PATH`, so a Windows PTY that inherited `Path` got a second spelling; which one the child resolved was non-deterministic. `createLaunchEnv` did the same and, because its read always missed on Windows, shipped Agent Teams terminals a `PATH` containing only the tmux shim dir. `resolvePathEnvKey` (extracted from the existing precedent in windows-environment-path.ts) now drives every PATH read and write in the PTY env pipeline, and attribution collapses Windows onto the single OS-resolved spelling. Off Windows the resolver always returns `PATH`, so POSIX behavior is unchanged and a case-sensitive POSIX `Path` variable is never touched. Closes #12046 * test(windows): track the launcher's own-environment marker The #12046 fix moved ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER and ORCA_CLI_COMMAND off ProcessStartInfo.EnvironmentVariables, but this asset test still pinned the old dictionary writes and failed. Co-authored-by: Orca <help@stably.ai> * fix(windows): follow the host block's PATH spelling on sparse daemon env patches Resolving a path-less Windows env to `Path` handed the daemon's own `{...process.env, ...opts.env}` merge both spellings when the host block spelt `PATH`. Fall back to the host block's own key, and collapse again inside the daemon since that merge happens after attribution. Co-authored-by: Orca <help@stably.ai> * fix(windows): resolve the live PATH spelling by block order, not casing Win32 resolves a duplicated variable by taking the first case-insensitive match in the block, so `resolvePathEnvKey`'s hardcoded `Path`-first preference targeted the shadowed spelling on the reporter's own `["PATH","Path"]` block. Drop the attribution-side collapse with it: it deleted the other spelling's value, and deleting the live key promotes the shadowed one, so an env that stripped down to empty lost both. * chore: drop unrelated merge formatting --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
79d3c847bd |
fix(runtime): attribute destructive close requests (#12238)
Attribute destructive runtime and daemon close diagnostics to the requesting client and exact target, record outcomes only after completion, and add reliability-gated attribution regressions. |
||
|
|
9e5bd5fb84 |
fix(worktrees): fence SSH worktree deletion PTY teardown to the owning host (#12388)
Destructive worktree removal swept PTYs by worktree id alone. Worktree ids are `repoId::path` and the store keeps one per host, so deleting an SSH worktree could stop a same-id local (or other-connection) workspace's terminals — or fail outright with `selector_ambiguous` when two hosts owned the id. Every destructive teardown now names its owner (resolvedWorktreeId plus the connection/runtime environment), matching the already-hardened forget-local path: - IPC `worktrees:remove` (git + folder workspaces) - runtime `removeManagedWorktree` (CLI/mobile `worktree.rm`, git + folder) - missing-worktree terminal reconciliation, including its no-provider fallback The #11960 allowUnverifiedStop force-delete gate is untouched. |
||
|
|
ce8b778d31 |
perf(runtime): withhold unchanged mobile snapshots from the graph payload (#12245)
* perf(runtime): withhold unchanged mobile snapshots from the graph payload Every graph sync structured-cloned all 222 worktree snapshots to main even when none had changed: 374 KB and ~5 ms per clone, paid twice because Electron clones on serialize and again on deserialize. That transport cost — not the renderer rebuild — is the bulk of a publication. The renderer now sends only the snapshots main has not acknowledged and names the rest in unchangedMobileSessionWorktrees. Detection is object identity, not a deep compare: an unchanged worktree already returns its cached snapshot object. Main seeds nextWorktrees from that list so its prune keeps withheld worktrees live instead of removing them. The call itself is unconditional. syncWindowGraph is not a one-way publish — its return value is the only channel carrying agentOrchestrationByPaneKey to the renderer, and the handler adopts pre-allocated handles, merges detached leaves, refreshes writable flags, and drains graph-sync callbacks on every sync. Skipping it would starve all of that. Two failure modes are closed explicitly. The memo advances only after main acknowledges, so a publication that throws is resent in full rather than silently withheld forever. And a worktree main dropped on its own — worktree metadata removal — comes back in mobileSessionResyncWorktrees, which also clears the accepted-revision record so the republish is not rejected as a no-op. Unchanged republish at 222 worktrees / 787 tabs: 374 KB to 3.4 KB, 5.08 ms to 0.02 ms per clone. One changed worktree: 5.3 KB. * fix(runtime): resync stale withheld mobile snapshots * fix(runtime): align accepted mobile snapshot membership --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
0586bab4f9 |
fix(mobile): bound terminal viewport resubscribe loop with backoff (STA-3337) (#12362)
* fix(mobile): bound terminal viewport resubscribe loop with backoff (STA-3337) An empty scrollback frame with absent host dims was coerced to 80x24, which never equals a phone viewport, arming a zero-delay unsubscribe/resubscribe loop (~25/s) that broke long-press gestures and drained battery. - Absent host dims now hold the stream instead of resubscribing. - Fit resubscribes are budgeted per handle (3 attempts, escalating backoff) with an absence-gated refill mirroring the chat-side rearm bound; on exhaustion the view degrades visibly via toast instead of hot-looping. - A fresh post-measure match counts as convergence instead of resubscribing. - setTerminalModes keeps the Map identity when the mode is unchanged, so same-mode frames no longer re-render the session route. - Host emits the subscriber viewport as scrollback dims when the snapshot and PTY size are both unavailable, so current hosts converge immediately. * fix(mobile): cancel stale viewport retries after convergence |
||
|
|
9ec4907cfb |
fix(agent-status): restore hydrated nonterminal statuses as unconfirmed (#12346)
* fix(agent-status): restore hydrated nonterminal statuses as unconfirmed A hook transition that fires while Electron is down has no receiver and is discarded, so last-status.json can restore a stale 'working' as confirmed truth for up to the 7-day hydrate TTL. Stamp hydrated nonterminal rows with restoredUnconfirmed, carry it through both IPC paths, and treat such rows as never-fresh in the shared and renderer freshness gates so the sidebar, worktree.ps, and the raw snapshot all present the same degraded semantics. Terminal states restore as-is; any accepted live event clears the flag; the flag itself is never persisted. Interrupt/question inference refuses to fabricate transitions onto unconfirmed rows. * fix(agent-status): shed unconfirmed marker when the liveness sweep verifies done The restored-subagent reaper's reconciled entry spread carried restoredUnconfirmed onto a process-probe-verified 'done', making freshness gates suppress a legitimate completion. Keep the marker only while the reconciled state stays nonterminal. * fix(agent-status): let live evidence replace hydrated rows * fix(agent-status): keep restored rows degraded Sort accepted live evidence after hydrated rows even across wall-clock rollback. Let unconfirmed rows own their preserved pane titles without asserting live state, while retaining independently live sibling evidence. * fix(agent-status): suppress unmapped restored titles Treat a single runtime title as covered by the single restored hook row while layout identity is unavailable. Preserve ordinary age-stale fallback and mapped sibling-pane evidence. |
||
|
|
f4b2b782b5 |
feat(orchestration): coordinator-driven release of settled worker terminals (STA-905) (#12355)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
d7fe9d6bcc |
fix(ai-vault): support session scanning in SSH worktrees (#11004)
* fix(ai-vault): support session scanning in SSH worktrees Add relay-native aiVault.listSessions scanning that discovers agent sessions on SSH hosts. Includes fallback to filesystem crawl for legacy relays, full cancellation support, result validation, and scan coalescing to reduce redundant work. * fix(ai-vault): scan sessions in SSH worktrees with coordinated cancellat - Extract batching logic to `mapRemoteScanBatches` for reuse and proper cancellation checkpoints - Move `AiVaultScanCoordinator` from relay to main to handle concurrent same-key requests with individual cancellation signals - Report scope path truncation consistently across relay and SSH fallback paths - Gracefully degrade relay handler on unsupported platforms instead of aborting startup - Refactor issue display to separate blocking errors, scope notices, and skipped transcript counts * fix(ai-vault): stabilize SSH session scan CI Swallow async WSL relay stdin EPIPE so the live hook-relay shard no longer fails after all tests pass. Merge main, resolve scan/relay conflicts, and align cancellation/host-issue reporting with IPC expectations. * fix(ai-vault): harden session scan cancellation, relay timeouts, and preemption Thread the abort signal through every scan and parse path so superseded or cancelled scans stop promptly instead of parsing every remaining transcript for a caller that already left. Replace the fragile message-text relay timeout check with a typed error code so unrelated errors carrying the phrase "timed out after" no longer suppress the filesystem fallback. Fix scan coordinator preemption so a forced Refresh in one window no longer re-enters as a spurious cancellation in another. Add a host-leg cache for the all-hosts view and cap filesystem concurrency so a single slow remote home cannot stall the whole merge. Co-authored-by: Orca <help@stably.ai> * fix(ai-vault): use stable React keys for scan issue banners Drop array-index keys so react-doctor/no-array-index-as-key passes. Uniqueness comes from host, kind, agent, path, and message. * fix(ai-vault): SSH session scanning with configurable depth limits Implement depth-aware caching and proper scan boundaries to make SSH session scanning reliable in worktrees. Users can now select between faster (250 sessions) and comprehensive (unlimited) history scans. The scanner: - Deduplicates scans across relay, host leg, runtime, and renderer layers - Reuses larger scans to serve smaller depth requests - Properly bounds in-scope discovery per-limit - Fixes timeout enforcement when SSH providers ignore abort signals * Move sessionLimit ref update to useLayoutEffect Keep render pure for React Doctor by deferring ref updates to a layout effect, which still executes before render-dependent effects that consume the ref. * fix(adhoc): stamp version prefix from main, not the feature branch Adhoc builds check out arbitrary refs whose package.json often lags version bumps (e.g. 1.4.165-rc.0 while main is 1.4.168-rc.1). Hourly always builds main so it already tracks the product line; adhoc now resolves the base version from origin/main (or ORCA_ADHOC_BASE_VERSION) so branch builds share that prefix. * Revert "fix(adhoc): stamp version prefix from main, not the feature branch" This reverts commit a26a18eb3fd83f7e7d2db9a6a7c3e02e0f79089a. * fix(ai-vault): fix scoped backfill and coordinator race conditions Resolve race where the last waiter leaving could abort an already-settled scan (add `settled` flag). Redesign scoped session backfill to keep searching through newer files until the scope reaches its requested session quota instead of stopping at the candidate limit; out-of-scope files no longer consume the scope budget. Centralize scan limit normalization and fix error classification for cancelled scans using the proper helper instead of checking Error.name. Disambiguate cache keys using JSON and add cancellation check after scope discovery phase. --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
a4944f5343 |
fix(orchestration): retain update settlement authority (#12336)
* fix(orchestration): retain update settlement authority * test(orchestration): register update settlement gate * fix(orchestration): close update settlement audit gaps * test(orchestration): correct update settlement evidence --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
056c2d9496 |
fix(runtime): bind mobile WS listener to loopback until pairing (STA-2370) (#11956)
The runtime RPC WebSocket listener bound to 0.0.0.0:6769 at startup, so a desktop with no paired device was reachable from the whole LAN before the user opted in. Default the bind to 127.0.0.1 and widen to all interfaces only on an explicit opt-in: - createMobilePairingOffer / getRuntimePairingUrl widen (ensureNetworkExposure) before advertising a LAN endpoint; the rebind reuses the resolved port so an already-issued offer stays valid, and concurrent offers share one rebind. - orca serve and E2E set exposeNetworkByDefault to bind wide at startup. - A previously-connected device (lastSeenAt > 0) rebinds wide at startup so reconnect after restart keeps working; a pending/never-connected offer does not persist exposure across a restart. The advertised pairing endpoint still resolves to a concrete interface address, never the 0.0.0.0 bind host. |
||
|
|
866bcda465 |
fix(terminal): recover degraded daemon spawn routing (#12277)
* fix(terminal): recover degraded daemon spawn routing * fix(terminal): preserve fresh-session recovery semantics * fix(terminal): avoid retaining exited recovery sessions |
||
|
|
cd68a8b00c | fix: preserve live agent PTYs through graph hydration (#11789) | ||
|
|
339045b150 |
fix(runtime): coalesce concurrent host terminal focus (#11841)
Bound exclusive host navigation to a generation-aware latest-wins single-flight so bulk open and switch fan-out stay responsive on large remote fleets. Add freeze repro harnesses and navigated settlement. |
||
|
|
b4d9ae44a5 |
fix(mobile): deliver the agent launch command when a create settles over a bare renderer PTY (#12197)
A mobile New Tab -> Codex create resolves the launch command and hands it to the renderer, but when the renderer's startup queue is lost (the #7587 stall class) the pane spawns a plain shell and the create still settles ready via PTY adoption - silently binding the phone to a bare terminal forever, since the ready status also disables the #7837 activation-time materialize recovery. Record the resolved launch command on the pending create and, at every renderer-backed settle point, deliver it to the adopted PTY when no spawn command was recorded for it. Spawn commands are noted per PTY by both spawn IPC handlers, so a missing record on the locally registered live PTY proves the launch never ran; delivery types the command exactly like the create would have, and the note prevents double delivery. Fixes STA-3214 |
||
|
|
922268af10 |
fix(native-chat): stop clipping assistant text blocks at the tool-preview cap (#12159)
* fix(native-chat): stop clipping assistant text blocks at the tool-preview cap Long assistant messages read over a paired connection (headless orca serve viewed from desktop or mobile) were cut at 4,000 chars with a '… (truncated)' marker and no way to read the rest. The mobile payload diet in nativeChat RPC applied the tool-preview char cap to text blocks, which are the fully rendered message body. Give text blocks their own 64k safety ceiling so real replies pass through whole while pathological multi-hundred-KB blocks still can't freeze the phone. Fixes STA-3230 * test(native-chat): cover long text stream frames |
||
|
|
525ffc5ae0 |
fix(worktree): stop the PTY gate from permanently wedging workspace removal (#12153)
Destructive worktree removal proves every PTY is dead before touching the filesystem. When a stop RPC failed, it re-listed the provider to check whether the PTY had already exited — but on the same deadline the sweeps had just spent, so it timed out without ever asking and read "could not verify" as "still live". The sweep spends that budget every run, making the refusal deterministic; --force never reached the gate, so the workspace was unremovable forever. - Verification gets its own budget instead of an exhausted remainder. - Verdicts split into exited / live / unverifiable; the error names the blocking PTY ids and why. - A reachable escape hatch: allowUnverifiedPtyStop, set only by genuine Force Delete affordances and the CLI's --force — never by the force the ordinary delete confirmation already sets — with an 'unstopped-pty' classifier reason so the desktop actually offers the button. - Force also survives a sweep that cannot complete; the non-force path still fails fast. Fixes #11960 |
||
|
|
95c431f5c3 |
fix(orchestration): worker-start launches the configured agent CLI, not the raw agent id (#12148)
Worker-start passed the Orca agent id straight to the shell as the worker terminal command, so `--agent cursor` ran `cursor` — which on Windows resolves to Cursor IDE's cursor.cmd and opened the desktop app, leaving a blank shell that timed out at agent_readiness. The same gap hit every agent whose CLI binary differs from its id (continue/aug/kiro/qwen-code/mistral-vibe/antigravity/trae/mimo-code/hermes/command-code/claude-agent-teams). Adds TerminalCreateOptions.startupAgent so callers name the agent outright; createTerminal then builds the launch from the TUI agent config (command, agentCmdOverrides, default args/env, preflight trust) instead of sniffing the command string. Also covers repo-less folder workspaces, which previously skipped resolution entirely, and fails loudly instead of spawning a bare shell when an explicit agent cannot resolve. Fixes #11926 |
||
|
|
8c5371ebad |
fix(worktrees): respect Windows shell for setup runners (#6967)
* Honor configured shells during worktree setup
* Align setup launch paths with selected Windows shells
* Carry setup shell selection through deferred launches
* Prove Windows setup shell routing at its real adapters
* Ground remote PowerShell proof in the real writer
* Preserve Git Bash across deferred setup launches
* Harden Windows setup runner shell selection
- Resolve remote PowerShell binary without local pwsh probe: for SSH/remote
Windows worktrees, isPwshAvailable() reflects only the LOCAL host, so an
'auto' implementation could route the remote runner to a pwsh.exe the remote
lacks. Add resolveSetupRunnerShell(..., { probeLocalPwsh: false }) so remote
auto keeps the always-present powershell.exe; explicit pwsh.exe still honored.
- Preserve native exit codes in the PowerShell runner by checking
$LASTEXITCODE before $?, so a failing native command surfaces its real code
instead of a generic exit 1; $? still catches cmdlet soft-failures.
- Write the PowerShell runner with a UTF-8 BOM so Windows PowerShell 5.1 (the
new default powershell.exe) reads it as UTF-8 instead of ANSI, preventing
non-ASCII setup-script corruption.
- Add unit tests for the remote-probe behavior.
* Restore setup-shell scope narrowing over the rebase
The force-pushed rebase dropped five review-fix commits that were already
on this branch; this reapplies their combined effect on top of the new
base and the hardening commit:
- Keep SSH setup shell selection remote-owned (no local terminalWindowsShell
or pwsh routing for remote hosts; supersedes the probeLocalPwsh guard)
- Preserve cmd setup compatibility outside POSIX shells (no .ps1 runner
family, so the BOM/exit-code hardening is no longer applicable)
- Route WSL setup runners from the project runtime
- Avoid blocking PowerShell probes during setup creation
- Correct SSH and WSL background setup fixtures
* Satisfy the changed-code gates for the setup-shell runner
- createWorktreeRunnerScript took 7 positional parameters, tripping the
changed-code max-params gate; move it to a single options object.
- hooks-runner.test.ts deep-equals the createSetupRunnerScript result, so
assert the cmd shell now returned for native Windows worktrees.
* Carry the setup launch shell through observed and issue runners
- buildObservedSetupCommand takes the runner's launch shell so WSL-routed
Windows-drive setup replays use /mnt/c instead of Git Bash /c
- resolveSetupRunnerShell gates the posix runner on the same Git Bash
resolution the PTY uses, so a missing or non-MSYS bash keeps the cmd runner
- issue-command runners carry their launch shell, and the renderer passes it
when building the queued command
- treat a bare `bash` shell setting as POSIX like `bash.exe`
Co-authored-by: Orca <help@stably.ai>
* fix(worktrees): close counsel P1 gaps for Windows setup shells
Route windowless/headless creates through the shell-aware setup runner when a
PTY controller is available, existence-check explicit Git Bash paths before
committing to .sh runners, thread the resolved shell into issue-command
runners, and document the intentional Git Bash interpreter flip with a narrow
scope table.
* Convert setup env to MSYS form and harden the bare cmd runner launch
C3: a Git Bash setup runner now receives ORCA_*/CONDUCTOR_*/GHOSTX_* path
values in /c/... form, matching the runner path and the shell's own HOME/PWD.
C5: extension-less `bash` resolves to Git Bash everywhere, matching how
resolveWindowsShellStartupFamily already classifies it.
C7: runner paths carrying characters that cannot be quoted on a cmd command
line launch through a delayed-expansion PowerShell shim instead, and the batch
runner disables inherited delayed expansion so `!` in setup lines survives.
Co-authored-by: Orca <help@stably.ai>
* docs: note MSYS ORCA_* paths and bare bash Git Bash resolution
Keep the setup-shell release note aligned with C3 env conversion and C5 bare
bash resolution so the published claim matches runtime behavior.
* revert: drop windows-setup-shell doc allowlist and AGENTS link
Keep the counsel P1/P2 product fixes without expanding the docs allowlist
or AGENTS.md guidance surface.
* fix(plugins): contain Parcel unsubscribe rejections under Vitest
Dev plugin watchers fire-and-forget unsubscribe, and in-process Parcel
can reject when temp watch roots are already deleted. Catch those
rejections so they cannot fail the suite as unhandled errors.
* fix(plugins): keep in-process unsubscribe rejection surface
Swallowing Parcel unsubscribe errors broke mocked unsubscribe tests
that return non-Promises and expect rejections. Contain failures only
in PluginDevWatcher fire-and-forget paths.
---------
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
|
||
|
|
484273844a |
feat(updater): add an adhoc release channel for branch builds (#12051)
* feat(updater): add an adhoc release channel for branch builds Hourly covers main. This covers everything that is not main yet: a dispatchable macOS build of an unlanded branch, published to stablyai/orca-adhoc, so the team can run an experimental feature for a few days instead of reasoning about it from a diff. Adhoc sits at the bottom of the version order — 'adhoc' < 'hourly' < 'rc' < stable — so no routine check can walk anyone onto somebody's branch; only an explicit pinned jump reaches one. It gets its own repo rather than sharing orca-hourly's, because a branch build must not appear in the list a developer riding main is looking at. Signed and notarized exactly like hourly, for the same reason: macOS anchors a notarized app's TCC grants on identifier + team, so an unnotarized build reads as a new client and silently loses file access under Documents/Desktop/Downloads. Tags stamp to the second rather than the minute. Hourly runs under a concurrency group and cannot overlap itself; adhoc builds are dispatched on demand, so two people cutting from different branches inside one minute is ordinary — and a minute-resolution tag would collide and fail the second build after its whole pack-and-notarize run. Channel-specific behaviour now derives from one DEDICATED_REPO_CHANNELS list: repo mapping, macOS-only support, and UpdateSource. The RPC schema that validates releaseChannelOverride was a hand-copied enum missing the new channel, which would have rejected the override on its way to the main process; it reads the predicate now. * fix(updater): merge the duplicated shared/types import Co-authored-by: Orca <help@stably.ai> * fix(ci): default the adhoc build ref to the dispatch branch The Actions UI puts its own "Use workflow from" branch picker directly above the ref field, and picking a branch there is what most people read as "build this". Making the field optional means the obvious action is also the correct one; naming a branch explicitly still wins, so main's copy of the workflow runs rather than a stale one on an old branch. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |