Repository navigation
Conversation
41287c3 to
161aba7
Compare
Sync update (
|
Sync update (
|
Sync update (
|
b378815 to
4dfffbe
Compare
Sync update (
|
…lyai#19783) * fix(orchestration): scope @ group addresses to the sender's Run `@all`, `@idle`, and the agent-name groups (`@claude`, `@codex`, ...) resolved against every terminal on the host. A coordinator meaning "my three reviewers" reached 126 agents across every open project, twice in one day, and every unrelated agent burned a turn discarding mail that was never for it. Every group except `@worktree:<id>` now means the live Dispatches of the sender's own Run, each addressed as `dispatch:<id>` so delivery is durable even when the worker terminal is not attached yet. A sender bound to no Run is refused with `invalid_argument` naming `run:<id>` / `dispatch:<id>`; there is no host-wide fallback and the host's terminals are never enumerated for it. `@idle` and the agent-name groups filter within that set by the same terminal status and host-resolved identity as before. `ask --to @group` returns the same code and points at the owning Run mailbox. Federated Dispatches read relayed control mail rather than a local mailbox, so a Run-scoped fan-out skips them with a `recipient_unreachable` warning naming the direct `dispatch:<id>` address. Group addresses are resolved host-side, so no RPC or stream shape changes; an older CLI sending `@all` to a new host gets the Run-scoped meaning. Claude-Session: run-scoped-group-addresses * fix(orchestration): revalidate legacy takeover before the recipient verdict A legacy coordinator taken over while `listTerminals` was in flight reported `runtime_error` instead of `legacy_read_only`: Run scoping made "no live Dispatch in this Run" the first thing the group send could fail on, and that threw before the takeover check ran. Takeover is a precondition, not a commit-time detail — the sender must be told it is read-only whatever else is wrong with its recipient set. Revalidation moves to immediately after the only `await` in the path. Everything below it is synchronous, so the commit-time window it used to guard is unchanged; only the error paths now see it. The legacy partition test gave `term_current_worker` no Dispatch, so under Run scoping it is correctly not a recipient. It now holds a real current-contract Dispatch in the same adopted Run, which is what the test is named for: one `legacy_direct` and one `current_delivery` recipient in one fan-out. Claude-Session: run-scoped-group-addresses * fix(orchestration): address the Run a nested coordinator created, not its parent A nested coordinator is both a worker of its parent Run and the coordinator of the Run it created. `resolveMessageRun` answers with the parent, correctly, because that is where its own `worker_done` belongs — but audience is a different question. Scoping `@all` to that Run sent a nested coordinator's "shared context" to the siblings it was started beside instead of the workers it started, and reported success, so it never learned its sub-workers heard nothing. Before Run scoping the host-wide fan-out reached the sub-workers by accident; this turned an over-broad delivery into a wrong-audience one, the exact failure class the change exists to remove. Group audience now resolves off the Run the sender coordinates, falling back to its Dispatch's Run. A leaf worker coordinates nothing and is unaffected. This is a separate question from `routing.run`, not a second answer to the same one, so `resolveMessageRun` keeps its meaning for point-to-point mail. Also: when every live Dispatch in a Run is federated, the fan-out skipped them all and threw a bare `Error` that discarded the warnings naming those remote workers and how to address each one. The sender was told "no recipients" while three remote workers existed. That throw now carries a code and the skip explanations. Claude-Session: run-scoped-group-addresses * docs(orchestration): say that no group address reaches a coordinator A coordinator is not a Dispatch, so Run-scoped groups never include one. That follows from the rule, but nothing said it, and the old host-wide meaning did include the coordinator — a worker sending `@all` to raise a blocker would be heard by its siblings and by nobody who can act. The guide, the CLI note, and the docs page now say to use `run:<id>` for that, and that a worker which created its own Run addresses that Run's workers. Also restores the `@cursor` case dropped when the group tests moved: a Claude pane titled "Fix the text cursor blink" must not receive Cursor's mail. That hazard was recorded from real titles and `@droid` alone did not cover it. Claude-Session: run-scoped-group-addresses * fix(orchestration): preserve group audience and mailbox identity * fix(orchestration): validate group scope before dispatch routing * fix(orchestration): preserve pane identity and exclude coordinator dispatches
stablyai#19705) * fix(codex): reserve the label's share of a qualified command row A child's label is raw provider text and was spliced into the command row unbounded, then the pair clipped to the description cap. A label at or past that cap clipped the command away entirely, leaving a row of kind 'command' that named an agent and showed no command - the failure qualification exists to remove, inverted. The same clip could also cut a surrogate pair, which boundSubagentField already guards against on the agent row two lines away. Give the label a reserved share and clip it the way the agent row does. * feat(native-chat): show live background work and name each row by kind The strip suppressed itself in three places: the Claude tracker blanked its roster for the whole of any turn, the Codex tracker returned nothing while a primary turn was open, and the renderer view gated on `turnId === null`. Between them, work in flight was never shown — and a task backgrounded in an earlier turn vanished from the strip as soon as the next prompt was sent. Claude additionally dropped every foreground subagent, so a fan-out reported nothing at all. Report work while it is live, in all three layers. Foreground Claude work is turn-scoped, so `result` retires it — that is the provider's own outcome for a task it marked foreground, not a roster sweep. Nothing settles a Codex child on turn end: those keep reporting well past their parent, so turn frames only prompt a republish. Name each ROW by kind — Subagent, Shell command, Workflow, Monitor — instead of a generic "Background <kind>", each drawing the glyph the shared tool-icon table already uses for that category. A row that carries a provider description still shows it unchanged. The collapsed header summary is deliberately untouched; it is owned elsewhere. The conversation-command gate is unchanged in effect: an open turn already refuses first, and Claude foreground work never reaches the backgrounded set the gate reads. * fix(native-chat): withhold the row stop Claude foreground work cannot honour The strip now publishes foreground rows, but `stoppableTaskIds` still filters on `backgrounded`, so `stopClaudeBackgroundTasks` resolved an empty target list and returned `{ cancelled: false }` that no renderer reads: the user clicked "Stop Subagent" and nothing ever happened. Carry stoppability per row instead of widening the stop to a target the SDK has no way to reach. `AgentSessionBackgroundTask.stoppable` is absent-means-yes, so hosts that predate it keep their working control, Claude emits `false` only on foreground rows, and the strip hides that row's button the same way it already hides the stop-all a provider cannot honour. * fix(claude): scope aggregate-roster authority to the work it enumerates `background_tasks_changed` lists BACKGROUNDED tasks, so a foreground subagent can never appear in it. Treating it as the whole world meant any such frame cleared every live foreground row mid-flight and then dropped every later foreground `task_started` for the rest of the session, killing the in-turn fan-out the strip exists to show in any session that ever backgrounds anything. Decide `backgrounded` before the staleness guard and apply the guard only to a backgrounded start, and retain live foreground entries across a roster replace. Retained rows count against MAX_TRACKED_TASKS, so the map stays bounded, and a stale backgrounded start the roster no longer lists is still dropped. * test(native-chat): pin the strip's monitor amber to the constant that defines it `MONITOR_GLYPH_COLOR`'s comment claimed a test held it and AgentStateDot's amber together, but no test imported it — the assertions hardcoded 'text-yellow-500', so the two could drift with every test still green. Read the colour from the module, which is what the comment always said was happening. Drop the unused `BackgroundTaskGlyph` export too: nothing outside the module names it. * fix(native-chat): keep the task list open across a gap in live work The strip is now mounted on live work, so a sequential fan-out unmounts it between one subagent finishing and the next starting: local `useState` meant the expanded list collapsed itself on every such gap, on top of the strip flickering above the composer. Hand the disclosure to the session, keyed by session id so it does not leak across a session switch. The strip is now controlled and holds no state of its own, which is what makes it survive its own mount churn. * fix(codex): route every command-row cut through one surrogate-safe clip `boundLabel` avoided splitting a pair, then `qualifiedDescription` re-cut the COMPOSED string with a raw slice: label (<=96) plus separator plus description (<=512) is up to 611 chars, so that second cut landed at an arbitrary index inside the description and could publish a lone high surrogate — lossy through any non-JSON UTF-8 hop. `parse` had the identical hazard on an unqualified primary-thread command. One `boundText` helper now owns all three cuts, so no path in the file can emit a lone surrogate from well-formed input. * fix(claude): keep terminal evidence for ids an aggregate roster never lists Narrowing the admission guard to backgrounded starts left a finished FOREGROUND id with no defence: `replaceAggregateRoster` wiped `terminalTaskIds` wholesale, so after any `background_tasks_changed` a replayed `task_started` revived a task whose completion had already been seen — and only a later `result` could settle it again. Scope the wipe the same way the guard was scoped: delete only the ids the incoming roster actually enumerates. A roster still overrules terminal evidence for the work it lists, which is what that behaviour was added for. * fix(claude): keep retained rows in place and evict the stalest, not the newest Re-adding retained foreground entries after the roster made a live row the user is reading jump below the backgrounded rows on every `background_tasks_changed`, and the cap `break` kept the STALEST retained rows while dropping the newest. Merge in the tracked map's own order so a surviving row holds its position, and count the overflow up front so eviction takes the oldest retained rows. Roster entries are never starved and the map stays bounded either way. * fix(claude): retire leftover foreground rows when the next turn starts A foreground `task_started` arriving with no turn open has no `result` coming to retire it, so it sat in the strip indefinitely — with no per-row stop, since foreground rows are not stoppable — and refused conversation commands behind an instruction nobody could follow. Settle on turn start as well as on `result`. This is cleanup only: visibility never consults `startsTurn`, so a missed one degrades to today's behaviour and can never switch the feature off. It shortens the row's life to the next turn; the case where no further turn is ever sent is filed separately. * fix(agent-session): withhold unstoppable rows from readers that predate them Rule 3 of remote-wire-compatibility: changing what the host publishes reaches old clients with no wire change. The Claude host published no foreground rows before this feature; it does now, and a client that cannot read `stoppable` draws a per-row Stop on every one of them — Claude always sets `supportsTaskStop` — which filters to the backgrounded ids, stops nothing, and returns a result no renderer inspects. That is the dead button `stoppable` was added to remove, reappearing across a version skew. Negotiate it. A client can advertise the existing background-task-stop capability and still predate `stoppable`, so this needs its own constant. Readers that do not advertise it get unstoppable rows dropped, and a state whose every row is dropped becomes no strip — exactly their pre-feature view. RUNTIME_PROTOCOL_VERSION is not bumped: this adds an optional field and a new negotiated capability, and changes no existing field's meaning, which is the explicit do-not-bump case in protocol-version.ts. * test(agent-session): name the projected rows so the fixture typechecks An indexed lookup into the fixture's task list is possibly-undefined under `pnpm tc`; the rows are more readable named anyway. * test(web): advertise the row-stop capability in the e2ee auth expectation The web e2ee handshake started sending AGENT_SESSION_BACKGROUND_TASK_ROW_STOP_CAPABILITY, and this test asserts the advertised list by deep equality, so it went red on CI while every targeted test run stayed green. Add the capability in the position the router sends it. * test(claude): pin why the roster empties mid-turn in a sequential fan-out The strip unmounting between two sequential subagents is truthful, not a swept row: A leaves on the provider's own terminal frame, B does not exist yet, and backgrounded work spanning the same gap holds the roster open — so an empty roster is never work the strip is hiding. Also pins the previous-turn rule against the one the subagent roster already applies on the same frame: a still-working FOREGROUND child becomes `unverifiable` there and a backgrounded one is left alone, so the strip drops the first and keeps the second rather than asserting `live` for either. --------- Co-authored-by: Merge Sim <merge-sim@users.noreply.github.com> Co-authored-by: Merge Sim <sim@local>
… CLI agents already do (stablyai#19807) * feat(sidebar): indent native-chat subagents under their session row Stacked on stablyai#19311, which adds the background-task channel this reads. The bridge maps agent-kind background tasks into AgentStatusEntry.subagents, and the renderer status feed confirms per connection so a reconnect cannot leave a child asserting live from a stream that ended. * fix(sidebar): avoid completed age for unverifiable subagents * fix(sidebar): preserve unverifiable child verdicts --------- Co-authored-by: Merge Sim <sim@local>
…i#19868) * feat(native-chat): focus the message box when a chat appears Opening a native chat left focus nowhere, so you had to click the composer before typing. Nothing in the chat surface focused it on open; the only existing focus calls were reactive (typing on the bridge pane background, picker acceptance, attachments, dictation), and the structured pane had none of those. useNativeChatComposerRevealFocus focuses the composer on the reveal edge, covering a new chat tab, a worktree-create landing in chat, the chat-view toggle, and switching back to an existing chat tab. Mount is the wrong signal: retained panes hide with display:none + inert and never unmount on a tab switch. It reuses the existing composer handle and shouldPreserveEditableFocus rather than adding a parallel path, and retries across a bounded run of frames because Tiptap publishes its adapter after mount and Radix restores a closing dialog's trigger in a setTimeout(0). Two supporting changes: - isFocusedGroup, from activeGroupIdByWorktree. On worktree activation both columns of a split flip visible in the same commit, so without it two revealed chats fight over the caret. The bridge route already had this bit as controller.isActive; only the structured overlay needed it. - focusRuntimeTerminalSurface bails on a chat-covered pane. Its DOM-path twin already declines chat view via data-terminal-chat-view, but the runtime path focused the covered xterm unconditionally and pulled the caret out of the composer. Returns true, not false: false sends the caller to the DOM fallback, which for a structured tab id focuses an unrelated tab's xterm. * fix(native-chat): preserve reveal focus ownership --------- Co-authored-by: Merge Sim <sim@local>
stablyai#19915) * fix(relay): keep failed rehome polls out of the durable failure budget The regional rehome worker polls claimRegionalRehome about once a second. Any error thrown before an attempt was claimed - in practice a director pool timeout on the pre-claim control read, 52-74 a day against a pool of 3 - was charged to relay_region_rehome_worker_state.consecutive_failures, which durably disables the control at three. That counter only ever resets on a drain receipt, so while the control is disabled it never resets: production sits at 1068 and still climbing. Enabling the control leaves the stale counter in place, so the next pool timeout latches it straight back off. That is what ended the 2026-08-28 enable after ten minutes. - A poll that never claimed an attempt drained nothing, so it no longer feeds the dispatch-failure budget and logs .._poll_failed instead of .._dispatch_failed. recordRegionalRehomeWorkerFailure had no other caller and is removed. - Enabling the control clears consecutive_failures and paused_until, so a budget spent under a previous enable cannot kill a fresh one. The dispatch interval in next_dispatch_at is deliberately left alone. - The budget's auto-disable now emits orca_relay_regional_rehome_failure_budget_disabled, matching the existing .._safety_disabled precedent. It wrote no event before, which is why this went unnoticed for two weeks. No change to region selection, the candidate query, or host eligibility. * fix(relay): serialize rehome failure accounting with control updates
…g it (stablyai#19911) When a PTY retires mid-delivery, every staged message was marked undelivered, which made all of them redeliverable. That is right for a pointer whose Enter never fired, but an Enter that was already written may have landed: redelivering it types the same mail into the pane a second time. The Enter timer is cleared at the top of retirement, so a RESERVED or WRITE_ATTEMPTED pointer provably never submitted and is released. An ENTER_ATTEMPTED pointer is ambiguous and now stays at its phase for the resume path to revalidate, matching the policy mailbox-pointer-submit.ts already documents for an unverifiable settlement. Co-authored-by: Merge Sim <sim@local>
This reverts commit 68e207c.
Co-authored-by: Merge Sim <sim@local>
… survive (stablyai#19695) * Make the structured turn lifecycle row durable so completed durations survive A structured-chat turn used to end by tombstoning its running lifecycle item, which threw away the only durable record of when the turn ended. Completed "Worked for" labels therefore depended on the renderer having observed the turn finish, and vanished on reopen. The lifecycle item is now revised in place, never tombstoned: - running, with startedAt, at the provider's turn start - completed or interrupted, with completedAt, at the provider's terminal frame, a user stop, or a child exit the host observed - unverifiable, with no end, when a cold acquire finds a running row from a generation whose exit nobody observed Both timestamps are the execution host's clock at receipt, captured before the deferred sink, so the completed value is identical on every client and needs no client clock. Codex history restore uses the provider's own second-granular endpoints for turns that predate this change. Desktop and mobile read settled durations off the journal through one shared selector, and anchor the live counter on the host start with the client's local receipt so a skewed client clock never leaks into the label. Locally observed durations remain the fallback for hosts that still tombstone. Timestamps live inside the existing turnLifecycle field, which old clients strip, and every working-state consumer keys on state === 'running', so no capability negotiation is needed. * native-chat: avoid stale working status on settled turns * test: align settled turn status expectations * Name settled lifecycle rows by their terminal state An interrupted or unverifiable turn must not read as completed for any consumer that renders status text raw. One shared helper builds the text for both providers from the lifecycle state. * test: deduplicate turn lifecycle suites Each behavior keeps one test; duplicated harnesses and restated cases go. * Key lifecycle rows to their user item and record the provider's measured duration A lifecycle row now names the user item that opened the turn by its provider key, so clients attribute timing explicitly and fall back to journal order only for rows from older hosts. A provider-initiated turn with no prompt can no longer claim the previous prompt's duration. When the provider measures the turn itself (Codex turn.durationMs, Claude result.duration_ms) the terminal row records it and clients prefer it over the host interval, so a turn shows the same number live and after a history restore. Host receipt times remain the live-counter anchor and the fallback. * Record a turn as a first-class journal item The turn record is now its own item kind rather than a status row carrying a lifecycle field: no text to misuse, and the fold matches the durable turn record other systems keep. Rows that carry it are stamped journal schema v3; every other row stays v2, so an older host keeps reading them and latches read-only at the first v3 row instead of truncating the epoch. Clients that predate the item would paint an unknown kind as a text bubble, so the host publishes the legacy status form to any client that does not advertise agent-session.turn-item.v1, through the same per-client seam background tasks use. The downgrade is transitional and goes once no supported release lacks the capability. The shared projection now renders unknown item kinds as nothing, so later kinds need no gate. One shared reader handles both forms for old journals and old hosts. * Preserve observed turn end across settlement retries * Retain turn attribution for loaded chat history * Preserve Codex exit receipt across close retries * Register completed turn duration reliability gate * Keep earlier turns through a Codex rewind and count a mid-turn attach from the real start Findings from an independent adversarial review of the typed turn record: - A Codex rewind adopted the provider's item list as the new epoch, and the provider never returns the host's own turn rows, so every duration before the rewind point vanished. The host's turn rows are now spliced back beside the item each followed, and recovery no longer expects the provider to prove rows it never owned. - The epoch row was stamped with the current schema version, so an older host latched read-only at row 1 of every new session, defeating the mixed version design. It carries no body and stays at v2; a stored-row test now reads SQLite directly, because the reader upcasts every row on read. - A send Codex folds into a running turn shares the opening prompt's provider key, and the alias map credited the duration to the later prompt. The earliest submission naming a key now wins. - The live counter anchored on first sight, so a client attaching mid-turn counted from zero. Published frames now carry the host's clock, the reducer keeps the last sample with its local receipt time, and both clients anchor on how long the host says the turn has run. * Correct turn duration gate assertion reference * Respect authoritative unknown native chat duration * Preserve unverifiable timing across older host upgrade * Record final completed turn duration reliability evidence * Fix the CI failures the merge left behind - A merged import list named the same module twice, which the native code quality plugin fails on. - A running turn is now reported by the host with no duration, so the settled map carries an explicit null for it; the hook test still expected the entry to be absent. - main gave the older-page action a cursor with a head-trim guard, so the retention test's epoch-only action no longer typechecks; it now passes an unbounded sequence, which is what the old shape meant. - The roster comparator moved into the extracted module, leaving its import unused in the reducer. * Split two files back under the line cap after the merge Merging main put both one effective line over 300, and the cap forbids a disable or a shave. The wire module's refusal vocabulary moves to its own file and is re-exported, so its consumers are untouched; the host's four thin mutation delegates move next to the functions they call. * Advertise the turn-item capability on every client transport Local IPC and mobile advertised it; the remote and web transports did not, so a desktop paired to a remote host, the CLI, and web silently ran on the legacy carrier forever and the canonical row was never exercised there. The renderer that paints it is the same build on every transport. * Update the web auth-frame expectation for the new capability --------- Co-authored-by: Merge Sim <sim@local>
…structured chat (stablyai#19681) * wip(native-chat): first-pass draft routing into structured chat (to be reworked) * refactor(native-chat): gather agent launch route inputs in one builder Every launch entrypoint assembled the route resolver's inputs by hand and they disagreed: only three of seven passed the project runtime blocker, so a WSL-pinned project was refused structured chat from the tab bar but admitted from the create dialogs. buildAgentLaunchRouteInput is now the one place that gathers host, capabilities, workspace kind, project runtime and TUI customization, and works for workspaces that do not exist yet. Also deletes the dead draft-prompt blocker from the shared resolver; the renderer stopped passing it and the main process never did. * refactor(native-chat): share one structured launch settle loop Five entrypoints copied the same loop around startStructuredAgentLaunch: start, claim a refusal fallback, await, branch on refusal or unknown. The copies drifted: direct work-item and full create reported an unexpected launch error as success, and resume handled neither refusal nor unknown. settleStructuredAgentLaunch now owns that loop and returns one settlement (structured, refused-then-legacy, cancelled, visibility-unknown, failed). Direct work-item, full create, folder workspace, both onboarding folder paths and vault resume consume it; each keeps only its own legacy fallback. Resume deliberately has no fallback. Unknown outcomes release the caller uniformly so a stale fallback closure cannot fire on a later reconcile. * refactor(native-chat): route the new-tab launcher through the shared settle loop The new-tab launcher fired its refusal fallback and forgot it: nobody learned whether the terminal fallback ran, and a visibility-unknown outcome was never surfaced. Its structured branch now runs through settleStructuredAgentLaunch with the terminal launch as the legacy fallback. launchAgentInNewTab stays synchronous; the result gains a structuredSettlement promise, and promptDeliveryResult keeps following the terminal fallback's delivery on refusal as it did through the callers bridge before. * refactor(native-chat): one legacy prompt delivery path and one trust preflight The direct work-item flow kept its own seed-and-paste copy of the legacy prompt delivery; it now uses deliverLaunchPromptToAgentTab with its own timeout notice supplied as a callback. Three private copies of the trust preflight (session continuation, worktree creation, folder workspace) fold onto preflightAgentTrust. The direct work-item pre-launch mark keeps its own entry because it differs in timing, not mechanism. * refactor(native-chat): run quick create through the shared settle loop Quick create was the last entrypoint driving the launch handle itself, because its cancel lifecycle is real: when the creation is abandoned the structured launch must be cancelled immediately so a staged prompt never reaches the provider. The shared loop now takes a cancellation hook with an eager subscription plus a post-await check; it cancels the launch once, unsubscribes on settle, and reports cancelled without running the fallback. Quick create keeps its two-branch legacy fallback and retire-on-late-cancel. Also updates the surface-caller census for the onboarding launch module that step 2 introduced. * fix(native-chat): open editable drafts in structured chat for eligible local Codex launches Route order asked the default-view-mode question first, and that decider applies the terminal mirror gate (a TUI cannot clear more than forty lines of prefilled draft), so a PR body over forty lines reached the plain terminal before structured eligibility was checked. Structured eligibility now comes first; the mirror gate applies only on the legacy branch. The structured draft seed writes the launch-draft store directly with no mirror gate, since a structured session has no terminal copy to fall back on. Closing a settled structured tab clears an unadopted seed. The structured session treats idle and loading as unsettled so the adoption hook takes its baseline from the loaded transcript. Each caller passes one delivery-mode value to both the route builder and the settle loop. The structured session component test is split with a shared harness so it stays under the test file line cap. * test(native-chat): make the structured session test harness type-portable * fix(native-chat): close review gaps in the shared launch settle loop - Claim a refusal fallback only when the caller supplies one, so vault resume no longer reports a terminal fallback it never opened. - A failed or cancelled direct work-item launch returns no tab id, so the caller never pastes the prompt into a setup shell. - Terminal fork activates with providesInitialSurface for structured launches and gates its toast on the settlement; the draft blocker deletion made fork route structured too. - A failed launch clears its draft seed. The failure toast moves to its own module to keep the launch-state file under the line cap. - Ratchet for settle-loop callers; cancel-during-fallback documented. - Restore the local agent label lookup that the pane-agent identity inventory expects instead of the inventoried helper. * fix(native-chat): resolve the agent label through one module * fix(terminal-pane): keep the fork dialog from reopening a created worktree A failed or unknown structured settlement returned false after the fork worktree already existed, so the dialog stayed open and a second click created another worktree. Unknown now closes the dialog (the launch badge already reports it); failed copies the context the way a null launch does. * chore: restore pnpm-lock.yaml to main (local pnpm rewrite slipped into a commit) * test(native-chat): stop asserting the deleted draft feasibility input The routing-authority test expected the shared predicate to receive isDraftPrompt; delivery mode is prompt metadata and never reaches feasibility now, so assert its absence instead. * refactor(native-chat): decide every agent launch route in one planner The route was still resolved at seven callers, each also calling the settle loop; two census tests only stopped an eighth. planAgentSessionLaunch is now the one production caller of the resolver and its launch() the one caller of the settle loop, and both censuses pin exactly that file. The funnel is two-phase because three sites need the route before the workspace exists and quick create persists its request for recovery: a plan exposes route before creation and launches with the created worktree id; a persisted quick-create request carries the verdict as data and re-enters through adoptAgentSessionLaunchVerdict without re-resolving. Delivery mode is fixed on the request once, so route and launch cannot disagree. * test(native-chat): pin the two adopters of a planned launch verdict * fix(native-chat): answer route readability from the repo when the worktree row is absent The planner's transcript-readability input dropped the repo-level connection fallback the direct work-item path still computes for its startup payload, so a route planned in the window right after workspace creation saw `undefined` — which reads as "not locally readable" — and downgraded grok/omp launches from native chat to a raw terminal. Only `undefined` ("cannot determine the host") now defers to the repo; a resolved `null` stays the local answer. * refactor(native-chat): answer structured feasibility with a query, not a launch plan Every rendered AI Vault row built a whole launch plan — execution-host lookup, project-runtime resolution, capability read, plus a plan object and a launch closure it threw away — to read one boolean off it. Feasibility and a launch decision are different operations, so the planner now exports the predicate for the first and keeps the plan for the second, and the census pins the query's callers separately. Settings arrive by argument, which makes the AI Vault callback's dependency on them real rather than a comment the linter contradicts. The plan's `explicitStructured` branch had that gate as its only caller and goes with it; the vault's launch already re-enters on an adopted verdict. * refactor(terminal-pane): fold the fork's trust preflight onto the canonical one `preflightForkAgentTrust` was a behavioural duplicate of `preflightAgentTrust`, whose signature now accepts a nullable agent and workspace path and so is a drop-in replacement. Its file is left holding only the launch-platform resolver — which is not a duplicate, since it returns an override rather than a default — so the file is renamed for what it now contains. * refactor(native-chat): cancel a structured launch through an AbortSignal The settle loop's launch cancellation re-derived the standard poll-plus-eager- event primitive that `AbortSignal` already is, so it now takes one. The eager semantics are unchanged: the loop still cancels on the abort event rather than only polling after awaits, so a staged prompt is discarded before it reaches the provider, and it drops its listener on settle instead of leaving the signal holding the closure. Quick create owns the controller and bridges its store subscription to it. A cancel that lands after the refusal fallback already opened a terminal now carries that surface on the settlement. It is the fallback's tab that exists, so reporting the pre-launch one handed the caller a workspace with no agent in it. * fix(native-chat): tighten quick create's structured launch settle path Four things the launch path got wrong once the settle loop owned the flow: - The abandoned-creation check now runs before the first-message rename flag is written, so a creation being torn down is no longer marked for a rename that will never happen (the order the pre-planner code had). - A cancel that arrives after the refusal fallback opened its terminal reports that terminal rather than the pre-launch tab. - `plan.launch` is called outside the caller's try, and nothing awaits that caller, so a throw there would strand the creation panel. It is now caught and reported the way a failed launch already is. - The launch route is a required argument instead of defaulting to `terminal-tui`, which would have silently reported success with no surface opened. Both callers already gate on the structured route. * fix(native-chat): give one launch identity one prompt delivery mode A caller joining a pending launch computed its outbox text from its own delivery mode, so an auto-submit caller landing on a draft launch enqueued text the first caller's seed was already showing in the composer: the user saw it and it was sent. The mode is now fixed by the caller that opened the launch, and a joiner delivers its text that way. Seeding also moved to where the coalesce decision is made, so a launch whose callers already settled as refused is not given a fresh draft — the refusal path early-returns, so nothing would ever clear it and it would outlive every tab. * fix(work-item): report a failed structured launch as a failed direct launch `launchWorkItemDirect` returned true unconditionally, so a structured launch that opened no surface still read as a started workspace. Callers hang irreversible follow-up work off that boolean — the fix-checks dialog fires `onLaunched` on it, which is documented as the home for host writes — so a launch with no agent tab now reports false, matching what full create does. The settle result says so explicitly rather than leaving callers to infer it from a null tab id, which `notLaunched` also produces. * test(session-tabs): pin the id a first structured publication is minted under The launch draft seed is keyed on `structuredAgentSessionTabId(sessionId)` before the tab exists, while the mirror mints ids with collision avoidance that can append a `:history-N` suffix. The two agree today only because a fresh session's base id is unique. Pin that where the id is actually minted, with the collision arm alongside it so the divergence the seed depends on staying away is visible rather than assumed. * test(native-chat): pin the route connection fallback on the un-mocked resolver The suite that covers the builder stages `getConnectionIdFromState`, so it can characterize the fallback but cannot catch a defect that lives in owner resolution itself. This one runs the real resolution over real store rows: two repos publishing the same worktree id on different hosts, which is the documented case where the owner cannot be named and `undefined` is returned. Red with both fix files at the previous head, green with them. Reverts the two caller pins added to the route census — the feasibility predicate is exported from the planner, which the census already permits, so it passes unedited and needs no permit clause. * fix(native-chat): keep the structured launch's own agent eligibility check Quick create's structured launch narrowed its guard to a bare `agent` presence check, so a creation carrying an agent that cannot hold a structured session reported itself cancelled once dismissed, where it previously reported that it had done nothing. Unreachable through both callers today, but it is the last local eligibility check in a module that otherwise trusts its callers for the route, so it is restored rather than left to the required-route typing — which says nothing about the agent. Also corrects two comments that called the quick-create request "persisted". It lives in renderer session memory and dies with the renderer; calling it persisted made the plan/adopt split read as restart recovery, when what it actually buys is a route decided before the worktree exists. * fix(native-chat): keep the structured feasibility query typecheck-clean The query threaded its narrow settings through the store, but the route store's settings must satisfy the full GlobalSettings that two of its resolvers require, so the narrow copy never fit. Ride the named settings on the built input instead: the caller still names them, so a React memo still depends on them, and no store-shaped object is needed. Also give the launch state its delivery mode unconditionally; the key is required, and a conditional spread makes it optional under exactOptionalPropertyTypes. * docs(native-chat): name the feasibility query's one remaining settings asymmetry The builder reads launch customization off the store while the routing gate reads the named settings, so one answer has two settings sources. It cannot diverge with the single caller passing the object the store already holds, but a PR about removing split sources should not leave that unstated. * fix(native-chat): keep a coalesced joiner's draft unsent joinLaunchDelivery stripped the joiner's delivery mode when the launch it joined had established none, and an absent mode reads as submit. A joiner that asked for a draft therefore had its text sent — the send-without- consent this PR exists to prevent. Fall back to the joiner's own mode only when nothing was established, so the first caller still wins otherwise. * chore: re-trigger CI GitHub created no workflow run for e935ea5 — the pull_request synchronize event was dropped. No content change. --------- Co-authored-by: Merge Sim <sim@local>
…tablyai#19912) * refactor(cloud): share PostgreSQL schema startup between services * feat(cloud): add durable native push notification gateway * infra(push): define dedicated gateway resources and operational checks * fix(push): bound cross-host admission and simplify gateway configuration * fix(push): validate deploy configuration and preserve topic-error registrations
…tablyai#19927) * fix(browser): restore the Chrome-shaped browser identity (STA-7147) stablyai#18749 replaced every browser partition's Chrome-shaped UA with Electron's stock one, so since v1.4.198 the embedded browser announces itself on every non-Google host as: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Orca/1.4.198 Chrome/150.0.7871.224 Electron/43.4.1 Safari/537.36 No browser sends that. Sites that re-check the identity holding a session reject it: users report being signed out of x.com, LinkedIn and "most websites," and at least one was signed out of LinkedIn in their own Chrome and met LinkedIn's "suspicious activity" SMS check -- server-side revocation, which reaches beyond our app. The repo already documented the mechanism in browser-google-auth-ua.ts: copied-in cookies "sent under a UA that doesn't match a real first-party browser get flagged by anti-fraud." That is why the Google auth-host switch exists; stablyai#18749 kept it for accounts.google.com and handed every other host an Electron identity. Restore the pre-stablyai#18749 session identity: strip the Electron and app tokens, and rewrite sec-ch-ua to match. Nothing in the cookie-import write path changed -- it never did; cookies were always written correctly and servers were refusing them. Deliberately KEPT from stablyai#18749, all independent of the UA: - anti-detection.ts stays deleted. Its premises were measured false on Electron 43 and its overrides are themselves published bot signatures. - No Runtime.enable into cross-origin iframes (the documented Cloudflare CDP tell). - No unconditional CDP debugger attach on every browsing guest. Known tradeoff, measured: this re-opens stablyai#13822. On the unmerged predecessor branch brennan/sta-3905-cloudflare-ua, commit 9f0a477 recorded the stock UA clearing dash.cloudflare.com 5/5 while every rewritten variant failed 12/12, and noted that adding client hints does not rescue it. So Cloudflare-gated sites will show verification failures again until a coherent-identity fix lands. That is a bounded, in-app annoyance; session revocation damages users' real accounts. A CDP Emulation.setUserAgentOverride with full userAgentMetadata -- which drives navigator.userAgentData as well as the headers, and was never tested -- is the candidate that could satisfy both, and is being measured separately. Tests: the real-Electron wire-identity test now asserts the stripped identity on ordinary hosts and Firefox on Google auth hosts. Ablation-verified: neutering cleanElectronUserAgent turns it red on the Electron-token assertion. Its fixture also gained an app name -- without one the raw UA carried no app token, so the Orca/x.y.z half of the cleaner was never exercised. * fix(browser): finish the identity revert in the files CI caught browser-session-registry.persistence.test.ts still asserted stablyai#18749's behaviour ("keeps the stock UA", "keeps the engine UA"), so the shipped code and its test disagreed. Caught by CI shard 4/8, not locally: I reverted four test files and went to typecheck without re-running the browser suite. Also restores the accurate wording that stablyai#18749 generalised away, now that the behaviour it described is back: - browser-google-auth-ua.ts: names the Electron/Chrome-shaped UA again as what anti-fraud flags, which is the reason the auth-host switch exists at all. - docs/browser/profiles.mdx: documents the cleaned Chrome UA default and the --no-ua-spoof escape hatch, which is real again. - tests/tools/google-signin-ua-probe.cjs: comments name the live handler. Deliberately left at stablyai#18749's version, because those changes stay correct with anti-detection.ts deleted: - browser-manager-viewport.ts: its comment no longer cites the retired addScriptToEvaluateOnNewDocument injection. - browser-webauthn-profile-delete.test.ts: its added webRequest mock is REQUIRED by the restored setupClientHintsOverride, so reverting it would break the test. * fix(browser): keep restored UA hints browser-owned --------- Co-authored-by: Merge Sim <sim@local>
…creens from their routes (stablyai#19675)
…stablyai#19854) * fix(native-chat): never adopt an unlisted model as the launch default `modelIsAdoptableAsLaunchDefault` is the sole gate on whether a model id may become the persisted `-m` launch flag for future native chats. For a catalog that does not set `discoveredModelsAreAuthoritative` — Claude and Codex — `!catalog.discoveredModelsAreAuthoritative` short-circuited the discovered branch to `true` for any id at all. So a raw launch flag (`worker-start --model claude-opus-5`) is seeded verbatim into the session record, and the first option write or model re-pick adopted it as the durable default. Every later native chat with that agent then launched `-m claude-opus-5` — an id neither the host CLI's list nor the catalog seed carries. Both branches now sit behind one precondition: the active model list or the catalog seed must carry the id. Ids that are carried keep their existing behaviour, including the authoritative-retirement and tracked-model rules. * docs(native-chat): state the launch-default precondition by id, not by vector A typed `/model` cannot introduce an unlisted id: matchNativeChatCatalogModelId returns only ids drawn from the list it is handed, so a never-seen id either collapses to a catalog id (claude `/model claude-opus-5` -> `opus`) or matches nothing (codex). It can only re-assert an id already in the record. The origins that do enter verbatim are the launch flag and an agent report -- applyNativeChatReportedSessionOptions writes `values.model` with no catalog matching. Say that instead, so the comment is true of the code as written and does not lean on the reconciled row that stablyai#19852 removes. Comment only; no behaviour change. --------- Co-authored-by: Merge Sim <sim@local>
…rovider echo (stablyai#19863) * fix(native-chat): settle a structured send on admission, not on the provider echo Sending a message in structured native chat raised "Message delivery is unconfirmed." with a Retry button on a message that had in fact been delivered. Measured across 14 days of local journals: 44 of 173 delivered sends (25.4%) tripped it. The dispatch path wrote the message to the provider, then waited a fixed 10s for the provider to echo the message's uuid back. That echo is emitted when the provider STARTS the turn, so a message queued behind a running turn cannot be echoed until that turn ends. Echo latency is bounded by the previous turn's duration, which is unbounded -- one send took 105 minutes. The 10s constant sat at the p75 of real echo latency, with the slowest clean send at 9.76s, a margin of 0.24s. No constant can work: the wait was measuring the wrong event. The false banner was not cosmetic. It invited a Retry, and Retry bypassed the operation ledger to redeliver. One message reached the model five times through that path. Dispatch now returns as soon as the transport write completes and writes no dispatch row; the submission stays `pending`, a neutral state, and the provider's echo settles it `accepted` through the late-settlement channel whenever the turn ahead of it ends. Delivery doubt is reachable only from process facts -- a refused write, a dead child, a dead host -- never from elapsed time. Retry re-delivers only where the recorded reason proves the message never reached the provider. The list is deliberately fail-closed: refusing a legitimate retry costs the user a re-type, while allowing an illegitimate one sends the model a second copy of their message. A refused entry now leaves the outbox with an explicit notice instead of parking at the head, where it would have wedged every message queued behind it. The send-response classification moves to a pure module beside the existing outbox reconciler, so both writers of an entry's state now live together and the decision is unit-testable rather than reachable only through the hook. Scope and known gaps: - Codex carries the same 10s stopwatch. It has no late-settlement channel, matches waiters by queue order rather than identity, and has no waiter lifecycle at all, so there was no safe subset to land here. A marker constant records the debt and deletes itself when that lands. - A message refused re-delivery loses its standing delivery notice and leaves only a transient error line. A passive "waiting to be accepted" affordance is the follow-up. - The restart reconciler that would decide a dead child or a dead host on evidence rather than refusing them is fully written and has never had a production caller. Wiring it is the next change, and it removes the re-type cost above. * fix(native-chat): harden structured dispatch settlement * fix(native-chat): preserve dispatch recovery evidence * fix(native-chat): preserve pending send compatibility * fix(native-chat): satisfy native import audit * fix(native-chat): bound legacy send settlement --------- Co-authored-by: Merge Sim <sim@local>
* feat(desktop): integrate native mobile push delivery and lifecycle * fix(desktop): preserve notification replay policy and review invariants * fix(desktop): correct notification locale namespace and auto-ack tests
* fix(sidebar): let an agent row show the provider's own session title The row resolver never read `aiVaultTitle`, so a terminal agent's tab showed the title from its transcript while its sidebar row showed the scraped live title — two names for one session. Placed at the same rank the tab strip uses. * fix(sidebar): scope provider titles to their sessions * fix(dashboard): refresh retained agent tab metadata --------- Co-authored-by: Merge Sim <sim@local>
…valid (stablyai#19746) * fix(editor): make markdown images inline so a paragraph stays schema-valid Image was registered as a block node while paragraph is content:'inline*', but the markdown pipeline nests an inline image as a paragraph child. Schema.nodeFromJSON does not validate content, so the editor built a schema-invalid document that rendered fine and threw on the first step that reassembled the paragraph - i.e. on the user's next keystroke. Report 0e46c048 (1.4.198, macOS): RangeError "Invalid content for node paragraph" from checkContent via Node.replace, tearing down the editor.rich-markdown boundary. Register Image as inline and override paragraph's parseMarkdown so a lone image is not hoisted out of its paragraph. Also fixes the same crash class reachable through details/summary. Markdown output is byte-identical. * fix(editor): keep a fenced code block intact when an image is inserted into it Making the image node inline meant it could no longer be fitted into codeBlock (content:'text*', marks:''), so inserting one with the cursor inside a fence made ProseMirror close the block at the insertion point: the remaining code escaped as plain prose and the language attribute was lost, and autosave wrote that markdown to the user's file. The pre-fix block image split the fence into two intact blocks instead. Resolve the insert content against the target position: when an inline image cannot be fitted where the caret sits, wrap it in a paragraph so ProseMirror splits the block and both halves keep their ``` fencing and language. Prose insertion is unchanged. Every production insert path now shares that resolution - the toolbar picker, the slash command and the clipboard-screenshot paste through insertRichMarkdownImageFromPath, plus the GitHub/GitLab composer's image-URL insert - each with a regression test. Also guard the unchecked cast of Paragraph.config.parseMarkdown: a Tiptap upgrade that drops the field would otherwise turn every paragraph parse into a TypeError and take the whole editor down, instead of degrading to parseInline. Four of the new round-trip cases asserted only on getMarkdown(), which walks the document without running NodeType.checkContent and so emits byte-identical output from a schema-invalid document - they passed on the pre-fix code. roundTripMarkdown now runs doc.check(), the list-item and table-cell case performs a real edit, and the standalone-image case types beside the image. All twelve cases now fail on the merge-base. Adds an Electron e2e spec driving the real renderer: a paragraph image and a toggle-summary image each survive a keystroke, and Bold over a selection spanning the image keeps it. --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Neil <neil@stably.ai>
…tablyai#19908) * refactor(ai-vault): move the surrogate-safe slice to shared, one implementation `sliceAtCodeUnitLimit` lived in src/main/ai-vault, and src/shared never imports src/main, so a shared consumer could not reach it. Rather than add a second copy, it moves to src/shared and ai-vault imports and re-exports it, leaving every existing importer of that module untouched. Separated from the feature that needs it: this is the only change here to a subsystem the rest of the branch does not touch. * feat(native-chat): give an agent session one place to hold its name Adds the normalized conversation name and the record field that stores it, so Orca has a durable note that it already named a session and does not name it again on a later acquisition. No name is generated yet, and nothing displays this field: the AI Vault path stays the home for the name a user sees. - `agent-session-conversation-name` bounds and flattens the text once, at 200 characters, cutting on a character boundary. - `AgentSessionRecord.conversationName` carries it, validated on load. - `setAgentSessionRecordConversationName` sets or clears it, unfenced: the name is a durable note, not ownership, so writing it never contends with the writer lease. * refactor(runtime): move reserve-owner orchestration next to its admission logic Pure move, no behavior change. `reserveOwner`'s transaction body sequenced decisions that all live in agent-session-reservation-admission and then applied the winning one; it now sits there as `commitAgentSessionReservation`, and the store keeps the transaction boundary and a one-line delegation. Takes agent-session-record-store.ts from 300/300 to 279/300, which is what the conversation-name field needs to land. Every existing test passes unedited. The module header said nothing in it mutates; that is now qualified rather than left false, since the committer writes the state it is handed. * feat(runtime): let the store set a session's conversation name `setConversationName` is the one writer for the record field, so a producer never reaches the record shape itself. It normalizes at the boundary, so no caller can persist a name the loader would then reject as unreadable. Unfenced by design: the name is durable memory that Orca already named this session, not ownership, so naming never contends with the writer lease. No name generation and no provider code yet. * refactor(runtime): reuse the shared default chat label on a replacement tab The replacement tab open-coded `'Claude Chat' : 'Codex Chat'` while every other publisher already calls `defaultAgentChatLabel`. One writer for the placeholder. * fix(native-chat): reject noncanonical stored conversation names --------- Co-authored-by: Merge Sim <sim@local>
…nheriting them (stablyai#19945) * fix(release): prune optional natives before the linux arch floor check The linux-arm64 release build failed packaging, and retried three times: [verify-linux-glibc-floor] 1 bundled native binary is built for the wrong architecture (target arm64): resources/node_modules/@parcel/watcher-linux-x64-glibc/watcher.node is x64, expected arm64 (from its own path) `afterPack` ran `verifyLinuxGlibcFloor` at its top, before `prunePackagedRuntimeNodeModules`. A cross-build intentionally installs every optional native variant, so at that point the arm64 slice still carries the x64 `@parcel/watcher` package that the prune exists to drop. The check was reading a file that was never going to ship. Move the floor check below the prune so it inspects the binaries actually packed. Same ordering as 35f1b0e on the v1.4.197 release branch, which shipped green and was never merged back to main. (cherry picked from commit 6400598) (cherry picked from commit a883e17) * fix(release): restore the minified telemetry constant fallback The macOS, Windows, and both Linux release builds all failed on "Verify telemetry constants present in app.asar", blocking publish-release: ::error::BUILD_IDENTITY constant missing or unexpected value in dist/mac-arm64/Orca.app/Contents/Resources/app.asar The verifier printed no context sample, because the string `BUILD_IDENTITY` does not occur anywhere in the shipped bundle at all. stablyai#17527 added `minify: 'oxc'` to the main bundle, so oxc renames the module-local `const BUILD_IDENTITY` to a short identifier. `BUILD_IDENTITY_RE` keys off the literal name and therefore cannot match a production bundle. `MINIFIED_TELEMETRY_RE` matches the adjacent injected identity/key pair instead, which survives renaming. stablyai#11019 created these patterns without that fallback, and its own comment predicted this exact failure if minification were ever enabled on the main bundle. The fallback was written on the v1.4.197 release branch in 35f1b0e and never merged back to main, so main has never been able to verify a minified bundle. Verified against a real local bundle built with the release env vars (ORCA_BUILD_IDENTITY=stable, ORCA_POSTHOG_WRITE_KEY=phc_...): the bundle does not contain "BUILD_IDENTITY"; both the narrowed and the widened BUILD_IDENTITY_RE fail; MINIFIED_TELEMETRY_RE matches and recovers identity=stable plus the write key. (cherry picked from commit 39c3e44) (cherry picked from commit d0bbe44) * fix(skills): stop asserting readdir order in the skill root walk test The Windows skill-sharing release gate — which blocks publish-release — failed on this test, wedging the 1.4.198 cut. The implementation is correct; the assertion was not. `findSkillFiles` pushes results in `readdir` order and dedupes visited directories by realpath, so of the 32 junctions pointing at one target exactly one survives. The assertion hardcoded both the array order and which link won. `readdir` order is filesystem-dependent: APFS and ext4 return SKILL.md first, while NTFS enumerates its filename index alphabetically on the uppercased name, where "LINK00" sorts before "SKILL.MD" (L=0x4C < S=0x53). Windows therefore returned the same two paths in the opposite order. Assert the contract instead: the real file is present, exactly one link-routed path survives dedup, and nothing else does. (cherry picked from commit 9a832e9) (cherry picked from commit 505b85c) * test(release): pin ported release gate fixes * style(skills): apply pinned test formatting --------- Co-authored-by: Neil <neil@stably.ai> Co-authored-by: Merge Sim <sim@local>
…tablyai#20447) On Windows, pnpm shortens the virtual store directory to @VScode+windows-process-tre_<hash>, cutting into the package name before the @, so the @VScode+windows-process-tree@* glob matched nothing and the addon recompiled on every Windows job. node-pty escapes this because its truncation lands after node-pty@, which the glob still matches. Widening the prefix to @VScode+windows-process-tre* matches both the full name kept on macOS/Linux and the truncated Windows one.
…tablyai#19651) * build(macos): run native module builds concurrently * fix(build): terminate sibling native builds when one fails Address coderabbit review: concurrent builds kept writing native artifacts after a sibling reported failure. Track spawned children, kill remaining siblings on first nonzero exit, and forward SIGINT/ SIGTERM to all children. * fix(build): process-group teardown and prefixed output for parallel native builds Address second coderabbit round: - Detached process groups + negative-pid kill so SIGTERM reaches swift/ swiftc descendants, not just the direct pnpm child (they could keep writing artifacts after fail-fast) - Signal handlers preserve the received signal (SIGINT no longer becomes SIGTERM for children) and are removed before re-raising, so the parent actually dies instead of looping through terminateAll - runPnpmScript settles only on close, never on error alone, so Promise.all cannot exit while children are still running - Per-module output prefixes ([computer]/[keyboard-layout]/[notification- status]) match what the PR description always claimed; interleaved swiftc errors are now attributable - Windows path untouched (early return before any of this runs) execa/p-limit were considered and rejected: no new runtime deps for a build script, and detached process groups give strictly stronger cleanup than execa's direct-child kill. * fix(build): memoized handler removal and external-vs-sibling signal split Second-round coderabbit findings on 24392a0: - Registration now uses the memoized handlerFor() instances so removeListener actually removes them (inline arrows were never registered, so the parent looped through terminateAll and hung) - externalSignal is set only by the parent's own signal handlers; a sibling's fail-fast SIGTERM no longer masquerades as an external signal, so settle() resolves Promise.all with the failing module's exit code instead of leaving top-level await unsettled (exit 13) - Also fixes a TDZ crash: handlerFor() was invoked at registration time before the signalHandlers const initialized Verified: sibling fail-fast resolves failer=7 with no survivors; external SIGINT kills children then the parent exits 130; real concurrent macOS build green. * Wait for native build cancellation before exiting * Clean up native builds when output streams fail * fix: bound native build waits, forward SIGHUP, honour output backpressure - Bound the per-child close wait: two seconds after a child exits, reap its process group and destroy its pipes so a descendant that inherited stdout/stderr cannot hang `pnpm build:native` forever. - Handle SIGHUP alongside SIGINT/SIGTERM so a terminal hangup reaches the detached compiler sessions instead of orphaning them. - Pause a compiler's output stream when the launcher's stdout/stderr reports backpressure and resume on drain, so prefixed output no longer buffers without bound. - Run build-native-for-platform.test.mjs in the computer-e2e mac-native-owner-smoke PR job and trigger that workflow on launcher changes; the tests are darwin-only and no other PR job runs on macOS. - Report the first failing child's status: re-raise its signal, or use its exit code instead of Math.max over cancelled siblings. * fix(native-build): keep output when reap timer overlaps backpressure; fail on ignored re-raised signal The descendant reap timer started on every child 'exit' and fired even when 'close' was late only because the launcher paused the pipe for its own stdout backpressure, destroying pipes with compiler output still queued. Arm the countdown only while the pipes are actually draining: clear it on 'pause' and re-arm on 'resume' after exit. Write the reap notice to stderr since stdout is the stream that may be blocked. Re-raising a child's fatal signal is a no-op when Node ignores it (SIGPIPE), so set a non-zero exit code first; a failed build no longer exits 0. Tests: stall the launcher's stdout consumer past the reap timeout and assert every kernel-accepted compiler line still arrives; kill the computer build with SIGPIPE and assert the launcher exits 1. --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Neil <neil@stably.ai>
…0266) * perf: skip WSL discovery when filtering native-only paths * fix: skip the AI Vault running-distro probe on WSL-less hosts - getAiVaultWslHomeDirs, the sibling in the same Promise.all as the native-path filter, still spawned wsl.exe unconditionally on win32; gate it on the cached installed-distro list so a host with no distro performs no probe when only native Codex homes are configured. - Hosts with a distro installed keep probing from that sibling, so the running-distro last-known-good cache is still warmed by the listing and a later probe outage falls back to the observed list, not []. - Add a test against the real wsl module asserting zero wsl.exe spawns across the whole listing Promise.all, plus the warmed-cache fallback. * fix(ai-vault): gate WSL home discovery on the cached distro list, not a probe `listWslDistrosAsync()` resolves `[]` when the `wsl.exe` probe is rejected, so a transient failure made `getAiVaultWslHomeDirs()` conclude "no WSL distros" and skip discovery. That narrowed the allowed-roots set `ai-vault-delete` and `ai-vault-subagent-list` validate against, wrongly rejecting WSL-hosted paths. Gate on `hasCachedWslDistros()` / `getCachedWslDistros()` instead: a pure cache read that only skips discovery once a successful probe has reported zero user distros. It also never probes, so the AI Vault listing cannot be the first to cache `[]` and flip a configured distro to "missing" in runtime resolution. * test(ai-vault): drop the type assertion tripping the casting gate check-changed-code-quality runs config/oxlint-code-quality-casting.json with assertionStyle:'never' over changed lines, and `args as string[]` in the new wsl-probe spy failed it. Narrow through Array.isArray instead, which is also honest about execFile's argv being optional. cached-session-list-wsl-probe + cached-session-list: 9/9 pass; tc:node clean; changed-code quality gate passes. --------- Co-authored-by: Orca Worker <orca-worker@localhost> Co-authored-by: Neil <neil@stably.ai>
…ai#20420) * Reduce native dependency installs to the host platform * Remove install policy documentation * Guard cross-arch packaging and scope release installs to the runner electron-builder only logs a warning for a missing extraResources source, so a host-only install silently shipped a foreign-arch slice without its natives — `pnpm build:mac` on Apple Silicon produced an x64 DMG with no sherpa-onnx-darwin-x64 and no @parcel/watcher-darwin-x64. The previous beforePack hook covered only win32. - Add assertPackagedNativeVariantsInstalled, an arch-aware check over the target's sherpa-onnx, @parcel/watcher, and (on Windows) node-gyp addons. beforePack now runs it for every platform, with remedies split: another architecture comes from install:release, the os:win32 addons need a Windows host. - Drop --os from the release installs. Every packaging job already runs on a runner whose OS matches its target, so only the macOS lanes need extra breadth, and only on CPU for their x64+arm64 config. Windows and Linux packaging return to a plain host-only install. - Add --frozen-lockfile to install:release so a bare run cannot rewrite the lockfile. - Restore the install policy reference doc and the CONTRIBUTING note, plus the rationale comments dropped from the runtime contract test. - Gate the packaging-closure assertions on whether the Windows addons are installed rather than on the host OS, so a cross-arch install exercises them off Windows too. - Make the workflow contract test read `run:` steps as well as retry-action commands, and enforce host-only scoping on the non-macOS packaging lanes. - Remove the unreferenced install measurement script; its numbers live in the policy doc. * Track the install policy doc and index it from AGENTS.md docs/** is ignored behind a per-file allow-list, so the new reference doc was only committed via git add -f and future edits would be skipped. Add it to the allow-list and give it an AGENTS.md entry like every other tracked reference doc, so the host-only install rule is discoverable before someone packages a second architecture. * Route Windows-lane removals through the retrying helper Adding these four specs to the PR Windows lane pulled them into the windows-lane-tree-removal-boundary ratchet, which failed on 20 raw recursive removals. On Windows a bare rmSync races a handle the OS has not released, throwing EPERM after the assertions already passed and reporting a green test as a lane failure. * Adapt the packaging guard to the vendored Windows registry addon main vendored windows-native-registry as the workspace package @orca/windows-registry (stablyai#20438). A workspace link resolves on every host, so including it in the installed-Windows-addons checks proved nothing. @vscode/windows-process-tree is the only os: win32 npm addon left, so it alone decides whether the win32 resource plan resolves.
…tablyai#20291) * perf: deduplicate unlimited vault scans once * perf: release discarded vault aliases during unlimited scans * fix: bound per-session bookkeeping in unlimited vault scans - Drop the per-session alias-key string, wrapper object and positions array the accumulator retained for every parsed row; index winning positions by the row's own sessionId instead (~430 B -> ~45 B per session at 50k rows). - Add a --expose-gc retention test asserting a 50k mostly-unique load-all corpus stays under 128 B of bookkeeping per session while matching dedupeCodexSessionsBySessionId exactly. * perf(ai-vault): bound per-row bookkeeping in CodexSessionCollection Key winners by the row's own sessionId string so an unlimited scan retains no alias-key string per live row (301 -> ~115 B/row measured over 50k rows), and split into a per-alias-key map only for the rare id that spans several hosts, namespaces, or rollout names, so admission stays O(1). Fold the PR's CodexSessionAccumulator into the collection main already routes every scan through, and rerun its scanner-level tests against that single class.
…20212) * perf(tooling): reuse directory entry types in source scans * fix(source-scan): stat DT_UNKNOWN dirents so untyped directories are still walked `readdirSync(..., { withFileTypes: true })` can hand back a Dirent whose type the filesystem did not report. For that entry every predicate is false, so the readdir-type fast path treated a real directory as a file and silently dropped its subtree from every ratchet guard. Fall back to `statSync` whenever the entry is neither conclusively a file nor a directory, keeping the no-stat fast path for ordinary entries. Also make the two readdir-order assertions in the walk test order-independent; `scanSourceTree` returns raw readdir order, which differs on tmpfs. * test(source-scan): unit-test the stat fallback via an extracted helper The fabricated-Dirent readdir mock could not satisfy both gates at once: vi.mocked(readdirSync) resolves to Node's Dirent<NonSharedBuffer> overload, so the mock needed a type assertion, and stablyai#19462's casting gate rejects new ones on changed lines. Removing the cast then failed tsc. Extract directoryEntryNeedsStat and test it directly with a structural probe. No mock, no cast, no top-level await, and the DT_UNKNOWN case is pinned: removing the fallback fails 'stats an entry whose type readdir could not report'. --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Neil <neil@stably.ai>
* fix(skills): evict removed runtime discovery cache * fix(skills): retire removed runtime cache entries using pending scan identity * fix: rescan mounted skill consumers when a runtime re-pairs under the same id - Fold the pairing revision into useActiveSkillDiscoveryRuntimeTarget's selector so a same-id re-pair yields a new runtime target and every mounted useInstalledAgentSkillNames effect re-runs instead of holding the retired peer's installed list after the module cache is evicted. - Reset hook-local result/loading state on runtime target identity change, which also bumps the refresh generation so an in-flight scan issued to the retired peer can no longer commit its result into React state. - Add mounted-hook regression tests covering the re-pair rescan and the in-flight stale-scan fence. * fix(skills): reset discovery state via render-adjusted state, not a ref write React Doctor flagged the render-phase write to stateResetInputRef. React can discard a render after the write, in which case the next render sees "already reset" and keeps painting the previous target's skill list until a rescan. Track the reset inputs in useState and adjust it during render instead, which React replays safely. Also drop the `as never` / `as GlobalSettings` casts from the tests this PR added, since main now enforces consistent-type-assertions on changed lines. --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Neil <neil@stably.ai>
…n steps (stablyai#20152) Two defects that change nothing about when an existing schedule fires. stablyai#16303: evaluateDueRuns awaited each row with no catch, so one unreadable schedule skipped every later due automation in that tick. Each row is isolated now; a poison record writes one folded skipped_unavailable run explaining itself and the tick continues. A renderer send that throws is closed out as dispatch_failed rather than mislabelled as an unreadable schedule. stablyai#15895: step validation only checked integer >= 1, so a step wider than its field degraded silently to a single value and still passed validation. Oversized steps are refused at input time only, bounded by the count of distinct values a field holds, so day of week rejects */8 while */7 stays legal. Runtime parsing stays lenient so rows saved before the gate keep running the cadence they have. isValidAutomationSchedule now answers only 'acceptable as new input'; a new isRunnableAutomationSchedule answers 'can Orca still run this', and the editor uses it so a legacy row opens intact and can be renamed without re-authoring a schedule that is still firing. Verified: 34/34 corpus expressions fire identically to main. Fixes stablyai#16303 Fixes stablyai#15895
…ilence (stablyai#20155) `terminal wait --for tui-idle` returned satisfied in ~0s while an agent was mid-turn. The shared title detector defaults a name-only agent title to `idle` so the sidebar can clear a stale spinner, and the wait accepted that stored value as completion. Rank the evidence instead. An explicit idle marker in the agent's own title or a known ready prompt settles the wait; a fresh first-party OSC 9999 status saying working/blocked/waiting vetoes it; a name-only title is a last resort that settles only once the stream has also gone quiet. The rank is derived at read time from `lastOscTitle` rather than stamped onto the record, because `syncWindowGraph` rebuilds leaves from an explicit field list and would drop a bespoke provenance field on any renderer publish. Two things the ranking alone gets wrong are handled here too. A quiet non-shell foreground process no longer proves idle on a pane where Orca launched a known agent — that is an agent still booting, and resolving on it is what let `dispatch --inject` lose the prompt (stablyai#9976). And the idle poll re-reads the live leaf each tick, because a record captured at registration stops advancing and its frozen `lastOutputAt` makes the quiescence gate pass while the pane streams. The demotion is scoped to agents that go on to announce rest explicitly. Grok, Copilot, Aider, Mimo, agy and OpenCode emit their name and nothing more at rest: a real idle Grok pane repaints its banner about four times a second forever, so demanding quiescence from it left no settle signal at all and the wait ran to timeout. The design is Brennan Benson's, from stablyai#14642, which won a cross-review against stablyai#6012, stablyai#6555 and this branch's earlier approach; it is ported here only because that branch shares no git history with main and cannot be merged. Neil's stablyai#6555 first drew the explicit-vs-ambiguous line the ranking rests on, and Revofusion's stablyai#6012 first identified that a single title sample cannot prove completion. Fixes stablyai#6011 Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> Co-authored-by: Revofusion <syed@moonai.org>
…tablyai#20149) Escape is ambiguous at the source for Claude, OMP, Pi and Prime Agent: the same key closes an overlay and cancels a turn, and which one it meant is focus state only the TUI holds. Nothing downstream can recover it, so for these agents a plain Escape is never evidence a turn ended — the provider's own hook decides. Ctrl+C is untouched, and no other agent type changes. The renderer skips the round-trip and main re-checks the same rule, so a stale or direct inference request cannot route around it. A navigation Escape does not clear a Ctrl+C already waiting to settle: Escape is not a retraction. Fixes stablyai#13547 Fixes stablyai#9208 Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Co-authored-by: Rod Boev <rod.boev@gmail.com>
…0310) * fix(mobile): reuse the current workspace on notification taps * revert(mobile): restore notification setting hint
…ai#20398) * fix(sidebar): observe agent titles before workspace activation Reuse parked terminal watchers for eligible live tabs in never-mounted workspaces, with initial title catch-up and capability-driven admission. Preserve existing watcher cleanup and avoid allocating watcher sets for empty workspaces. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix: reconcile background watchers when remote coverage changes Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test: type-check terminal watcher fixtures without casts Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
) * Add compile-time RPC params catalog parity gate Check each registered handler against its catalog params type in both directions, with explicit exceptions for the three uncatalogued schemas. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(rpc): keep the params generator off its own output The parity gate imports the generated catalog for types, and it lives under RPC_DIR, which indexableModules() scans for shared imports. That re-added OUTPUT_PATH after line 46 removed it, so the generator bundled and require()d the committed catalog. A catalog referencing a renamed or deleted shared export then crashed regeneration — in exactly the state that requires regenerating. Reproduced before and after: with a dangling reference injected into the catalog, `generate:rpc-params-catalog` threw; it now rewrites the file. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
…awns (stablyai#20285) * Widen Windows shim ratchet to detect package bin spawns Follow local program expressions into node_modules/.bin while preserving the existing literal check, roots, and allow-list. Document static-analysis limits and cover unsafe and resolver-based invocations. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(scripts): fold dot segments before matching node_modules/.bin The predicate joins call arguments textually, so a literal '..' segment hid a path that resolves into node_modules/.bin at runtime. Folds '.' and '..' (and Windows separators) first. A '..' that genuinely escapes .bin still does not match. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * style(scripts): use .at(-1) in the dot-segment fold oxlint's prefer-at rule; the repo-wide lint gate is an error, not a warning. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
…e meaning (stablyai#20133) * refactor(native-chat): give each structured dispatch state exactly one meaning `unknown` meant five different things. Only one of them was genuine ambiguity. A transport write that the provider's input pump never took is provably undelivered -- which is what `rejected` already means. It was recorded as `unknown` anyway, and a one-entry allowlist then existed solely to teach Retry that this particular `unknown` was safe to re-deliver. Collapsing that case into `rejected` deletes the allowlist and turns a predicate into an invariant: Retry never re-delivers an `unknown`, with no exception to reason about. The four states now each assert one thing -- `pending` written and awaiting, `accepted` the provider has it, `rejected` provably did not happen, `unknown` genuinely cannot tell. A fail-closed guard is the right default here because the asymmetry is severe: refusing a legitimate retry costs the user a retype, while allowing an illegitimate one sends the model a second copy of their message. Also fixed, found while auditing every reader of `rejected`: - The renderer printed `submission.reason` verbatim, so a broken pipe put the internal token `provider_write_failed: broken pipe` on screen in destructive red. The journal reason is unchanged -- it is the durable evidence and the transport-versus-content discriminator -- but the screen now gets copy that names the cause and says the message is safe to resend. Content rejections still show the provider's own words. - The fallback copy "Message was not accepted" read as a content refusal. A null reason now yields "Message was not sent.", which asserts only what every rejection shares. - A refused worker-start preamble threw a plain Error out of the dispatch path. It now throws `OrchestrationError('dispatch_preamble_undelivered')` so a coordinator can tell "we could not send it" from "we sent it and something else broke" without parsing prose. Retain/discard behaviour is unchanged; only the verdict's legibility improves. Two behaviours improve as a consequence rather than by design: a provably undelivered message no longer blocks conversation commands, and no longer leaves the session reading as "working" in chat and in every session list. Not addressed here, and named rather than implied: a message left `unknown` by a dead child or a host restart still has no recourse but retyping. The restart reconciler that would decide those on evidence is written and has never had a production caller. Parking the refused entry instead would reintroduce the head-of-queue wedge removed in stablyai#19863, so it is not an option. Note for whoever edits `journal-reducer.ts` next: it sits at 297 of its 300 counted lines. The next statement added there needs a split, not a shave. * fix(native-chat): close two gaps review found in the rejection taxonomy Both are narrow and both were real. A journal written before a refused write became `rejected` still holds that submission as `unknown` with the transport marker. The predicate this change replaced excluded exactly that shape from provider-echo matching; the state-only check that replaced it does not, so on replay such a row could claim the echo of a later, genuinely delivered send of the same text and attach the delivery to the wrong message. Fail-closed still prevented any re-delivery, so nothing duplicated — but the wrong submission was credited. Replay now excludes the legacy shape too. And the content-versus-transport split had a third case neither side covers: a local capacity refusal is neither the provider explaining itself nor a frame that failed to leave. It fell through to the verbatim branch, so `claude structured dispatch queue is full` reached the screen — the same class of leak this change set out to fix, one reason short of being caught. Internal reasons now get copy; only a provider's own words are shown as written. Each is pinned by a test that fails with its guard reverted and passes with it restored. * fix(native-chat): preserve dispatch refusal across clients * fix(native-chat): rotate immediately rejected retries * docs(native-chat): correct rejection taxonomy reference * docs(native-chat): align mobile retry comment * docs(native-chat): clarify unknown replay semantics * fix(native-chat): keep a mobile send's operation id when delivery is unknown Mobile released the retained operation id whenever a send came back `unknown`, so the user's next send of the same text went out under a fresh id. A fresh id has no ledger row, so the host treats it as a first delivery and dispatches it -- even though `unknown` is the one answer that says the provider may already have the message. That is the duplicate this branch exists to remove, reintroduced on the client that has no outbox. Which case that was matters. Mobile only ever sees `unknown` from ack-loss (`isRpcDeliveryUnknown`: "the host may have processed it and only the ack was lost"), because the mapper reported every `ok` result as `accepted` without reading `dispatchState`. So the rotation fired exclusively where delivery was ambiguous and never where it was provably refused, which is the inverse of the rule this branch establishes. Retaining the id is what makes a retry safe, and it costs no liveness: `performSend` answers a second request under a recorded id from the journal and never puts it back on the wire, so a reused id delivers when nothing landed and replays when something did. Rotating can only ever add a second copy. The retention stays bounded by the host's admission window, which `retainStructuredSessionOperationId` already enforces. `retryUnknown` goes with it: the host ignores it for delivery, and all it does is skip the cached answer to re-read the same row. Keeping the id exposes what the rotation was hiding, so fix that too: a replayed `unknown` comes back `ok`, and mobile called it `accepted` and cleared the composer as if the message had landed. `dispatchState` now decides, in one pure function: accepted/pending sent, and the id is spent rejected provably did not happen and terminal in the reducer, so reusing the id could only replay that rejection: spent, and the next attempt is a first delivery under a new id unknown keeps its id Reading `dispatchState` at all is a pre-existing defect, fixed here because the false "sent" cannot be removed without it, and scoped to the send path. `mutate`'s rotation for prompt/option/cancel plans is untouched. The rejection copy is the desktop's notice, so an internal reason (`provider_write_failed: ...`) still never reaches a person. Tests: the hook test that was flipped to assert a rotated id now pins the opposite -- one id across an ack-loss and two `unknown` replays, each reported `unknown` rather than `accepted`. The send fixture grew the durable submission row a real host returns; without it every send test asserted against a shape that cannot express the bug. * fix(native-chat): enforce fail-closed structured send replay * fix(native-chat): align retry and mobile RPC contracts * fix(native-chat): keep transient admissions retryable * test(tab-bar): expand nested create menu in harness --------- Co-authored-by: Merge Sim <sim@local>
…story (stablyai#20139) * feat(native-chat): decide a restart-stranded send against provider history `markPendingSubmissionsUnknown` flips every surviving `pending` submission to `unknown` on attach and stops there. The module written to finish the job describes the intended two-step in its own header -- "Every surviving `pending` becomes `unknown` and is then matched against provider history" -- and only the first step ever shipped. `reconcileSubmissions` has been imported by exactly one test file and nothing else. So a message stranded by a dead child or a host restart had no recourse but retyping: Retry correctly refuses to redeliver something that may already be with the model, the outbox entry drops, and a transient error line is all that remains. This wires the second step, so those are decided on evidence instead of refused. Caller placement is the design decision, because where it runs determines what a consistent history boundary can mean. It runs in `attachJournal`, immediately after the sweep: attach happens after the record store's CAS hands this host the lease and before a provider child starts, so nothing can append to provider history while it is read, and the window stays valid until the resume consumes it. The three other settlement sites can all be overtaken by a newly started child before the read is acted on. The history source is the Claude project JSONL for the handle chain's provider session id -- definitionally what a resume replays, which is what makes absence meaningful. Boundary consistency reuses `proveClaudeTranscriptBranchFromJsonl` rather than inventing a check: a fork, a compacted log and a truncated tail each already throw there, and each maps onto `boundaryConsistent: false`. A null leaf uuid is also false, because there is no anchor to prove a start from. Two guards were needed that the reconciler cannot enforce itself, because Claude echoes no client message id and only the fingerprint pass can fire: - A transcript records a pasted image as base64, and the block decoder drops it silently for want of a url or path. Such a record would enter the window advertising a text-only fingerprint, where an unrelated text-only submission with identical text could claim it. The window now inspects raw content parts before decoding and excludes any record a part would be dropped from. - A submission carrying an image-ref path can never match a transcript that keeps only base64. Without a guard it matches nothing by construction rather than by absence and falls straight through to `not_delivered`, and a Retry would then redeliver an image already sent. Only text-only bodies are handed to the reconciler. Both guards fail a named test when removed. Limits, stated rather than implied. The exact-match tier needs the provider to echo our id, which Codex does and Claude does not, so Claude resolves by fingerprint alone -- and two identical prompts deliberately reach `ambiguous_match` instead of guessing. Repeated one-word prompts therefore stay unknown by construction. This decides what it can prove and refuses the rest, which is the intended contract, not a shortfall in the wiring. Found while doing this and not fixed here: the block decoder silently dropping base64 images has a blast radius beyond reconciliation and deserves its own change. * fix(native-chat): harden restart history reconciliation * fix(native-chat): keep Claude adapter within lint budget --------- Co-authored-by: Merge Sim <sim@local>
…ing the creation launch route (stablyai#19819) * fix(native-chat): wait for the runtime capability probe before resolving the launch route A worktree created before the renderer's hydration-gated capability refresh runs read the local capability set as null, which resolveStructuredNativeChatSupport treats as a blocker, silently degrading structured native chat to the legacy terminal-backed route. Creation submits now await ensureLocalRuntimeCapabilities(), which probes the local runtime when no answer has landed yet, so the route resolves on an actual answer. Fixes stablyai#19154 * fix(native-chat): await the capability probe in the work-item direct launch route too prepareDirectWorkItemAgentLaunch is the fourth creation-flow route owner and already async; a pre-hydration submit-after-ready launch (fix-checks) read the unprobed cache as unsupported and silently degraded to legacy. Draft-delivery launches were unaffected (draft-prompt blocks structured before the capability check). Same shape as the three creation-submit sites. * fix(native-chat): keep the capability probe starting synchronously The broken-bridge hardening wrapped the probe in Promise.resolve().then(...), which deferred window.api.runtime.getStatus() by a microtask. The session-tabs restore deliberately overlaps its inventory RPC with this refresh and relies on the probe already being in flight when refresh returns, so the deferral broke it. The bridge call is synchronous again; a synchronous throw becomes a rejection instead, which is what the wrapper was actually for. * fix(native-chat): hydrate local runtime capabilities at renderer boot The capability cache's only writer was `useLocalStructuredSessionTabsSync`, gated on workspaceSessionReady + terminalStartupRestorationReady + the experimental flag. Every `resolveAgentLaunchRoute` reader treats an unanswered cache as "unsupported", so the answer arriving seconds late is what produces the bare-terminal create in stablyai#19154 — awaiting the probe at a route decision guards four call sites but leaves the window open for the three readers that are synchronous and cannot await. Start the probe from the renderer boot chain, ungated, so the answer is cached before any launch route is resolved. The per-call-site awaits stay as the backstop for the residual window and for re-probing after a failed probe. Also: hoist the full-creation probe above its cancel gate so the gate stays adjacent to createWorktree; pin the retry-after-failure, concurrent-ensure and missing-bridge contracts; drop a stale microtask tick and correct two comments that no longer described the code. * test(native-chat): pin the cancel gate around the capability probe The probe added an await to two composer creation paths. Full creation had no gate between the route decision and createWorktree, so the earlier revision opened a window where a dismissed composer still created a worktree; the hoist that closed it was unpinned. Quick creation already gated immediately before runBackgroundWorktreeCreation, so its inline await is safe — pin that too, since nothing asserted it. Both tests fail against origin/main (no probe) and the full-creation one fails against the pre-hoist revision. * fix(native-chat): close the folder-create cancel window the probe opened The probe added the first `await` inside `submitFolderWorkspaceCreate`. On `main` that function ran straight through to `createFolderWorkspace` with no suspension of its own, so its caller's `isSubmissionCancelled()` gate and the create call sat in the same turn. With the probe inline, a composer dismissed while the probe is in flight still creates the folder workspace and launches an agent — the same defect the full-creation hoist fixed on the git path. Resolve capabilities in `folder-submit-orchestration` above its existing gate and hand them down, so the create path's prefix is synchronous again. The parameter stays optional: a caller without a cancel gate keeps the probe. Both new tests fail against `origin/main` and against this branch's previous head; the cancel-window one still fails with its probe-pending assertion removed, so it pins the create, not just the probe. * refactor(native-chat): require pre-resolved capabilities on the folder create path The cancel-window fix in f492064 left its invariant -- a caller that gates on cancellation must resolve capabilities above its gate -- enforced only by a comment, because `hostCapabilities` stayed optional with an inline probe as the fallback. A future caller that owns a cancel gate and forgets the parameter would silently reopen the window twice fixed already, and nothing would catch it: the caller census test pins `resolveAgentLaunchRoute` callers, not this function's, and `exactOptionalPropertyTypes` is off so even an explicit `undefined` is legal. Make it required and drop the now-unreachable inline probe. The sole production caller already passes it, so runtime behaviour is unchanged: the old ternary never evaluated its `await` when a value was supplied. `null` keeps its meaning -- probed, genuinely unknown -- and still degrades to the legacy route; only absence becomes impossible. The launch-route test that covered the removed probe is replaced by one pinning that `null` contract with the cache and the bridge both holding the structured capability, so only the handed-in value can produce the legacy outcome. The cases in the sibling suite are not about the route, so they go through one typed wrapper that supplies the unknown answer rather than repeating it 21 times. --------- Co-authored-by: Merge Sim <sim@local>
…workspace goes (stablyai#19970) * fix(worktrees): retire the chat tab of a chat with no child when its workspace goes Deleting a workspace left a chat tab behind for every structured session that had no attached provider child at the time, and that tab came back at the next launch pointing at a workspace that no longer exists. A provider child is scoped to a VISIBLE pane, not to a tab: the hold that keeps one is `enabled: isVisible && isWorktreeActive`, and dropping the last hold evicts the child after the release grace. So the sweep's liveness predicate selected only "the chat that is the visible pane in the active workspace, or was moments ago" — which means deleting a workspace from the sidebar while a different one is active left every chat in the target invisible to the sweep, and the `live.length === 0` early return did nothing at all. The durable reference is `visibleSessionIds` in the agent-session record store. Both purges a removal already performs miss it: the renderer drops `unifiedTabsByWorktree` and the main process drops the workspace metadata, and neither touches that index. Startup replays it, restores the session from it and republishes the tab. Worktree ids are path-derived, so a later workspace created at the same path inherits the old chat. Splits the two concerns the sweep conflated in one list. Liveness still decides what to CLOSE and what to refuse over, unchanged. Membership — the same fenced record filter minus the liveness clause — decides what to RETIRE, and covers exactly the complement of the close list so each session's tab is handled once. Retirement runs from `killAllProcessesForWorktree`, past every point that can refuse, not from the structured sweep itself: that sweep is joined BEFORE the unstopped-PTY verdict so a structured refusal can outrank a terminal one, and a tab retired there would still be ahead of a gate that can refuse the whole removal — leaving the workspace in place with its chats gone. * fix(worktrees): retire tabs across all teardown outcomes * test(worktrees): type teardown fixtures --------- Co-authored-by: Merge Sim <sim@local>
…tablyai#20277) * feat(ai-vault-search): define public contract and service seam * feat(ai-vault-search): add IPC runtime relay and web transports * fix(ai-vault-search): register search IPC at the core handler site ai-vault.ts was two lines over the 300-line max-lines limit; the search handlers belong with the other register*Handlers calls anyway. * fix(ai-vault-search): withhold degraded-root paths from relay status Status carried local filesystem paths over the relay while hits redact theirs. redactStatusForTransport applies the same policy at the same boundary: relay callers keep each root's reason and the array length as the count, so the type only makes root optional. * fix(ai-vault-search): close diagnostic path leak and remove test casts * feat(ai-vault-search): carry an execution host id and per-host outcomes on hits * feat(ai-vault-search): route desktop search by execution host scope, including runtimes * feat(preload): accept an execution host scope on session search * feat(web): answer only for the paired runtime on session search * docs(ai-vault-search): describe execution-host routing and the all-hosts merge * test(ai-vault-search): cover every host scope, the all-hosts merge and wire compat * fix(ai-vault-search): resume every host mid-page so a merged page never drops a hit * fix(ai-vault-search): decode the merged cursor with a schema instead of casts CI's type-aware audit refuses type assertions; a zod record validates the per-host entries and yields the typed map without one. * refactor(ai-vault-search): defer cross-host merged search
) * refactor(mobile): migrate settings reads to RpcOperation Replay the settings slice on the landed RPC foundation after rebasing onto main. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): refresh task parity snapshots after main rebase Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): correct rebased declaration parity hash Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): account for main task declaration Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): preserve raw RPC rejection timing Return the transport promise directly and interpret replies separately so sibling Promise.all rejection order cannot change. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): refresh parity hashes after timing fix Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): use operation interpreter after raw request * test(mobile): refresh settings migration parity hashes Refresh hook and statement parity hashes for the two task declarations whose settings reads now use RpcOperation request and interpretation. Changed declarations: - useMobileTasksRuntimeHydration: settings.get replaced by settingsRead request/interpret. - useMobileTasksWorkspaceCreateActions: settings.get response handling replaced by settingsRead request/interpret. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
…s jumping (stablyai#20493) * fix(mobile): stabilize native chat tail following * refactor(mobile): give native chat one tail-follow owner Extract the streaming scroll contract into use-mobile-native-chat-tail-follow, so intent and geometry have a single writer instead of a state/ref pair hand-synced at five call sites. No behaviour change: the existing guards pass untouched. * fix(mobile): fence native chat tail follow through momentum * fix(mobile): repin chat at measured tail --------- Co-authored-by: Merge Sim <sim@local>
…ablyai#20280) * fix(mobile): preserve delivery ambiguity across transport cutover Let physical close settle requests and retain its error as the cutover cause, copying only an existing delivery-unknown mark. Pin sent and unsent caller outcomes and both cutover predicate carriers. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): pin the RpcClient.close() settlement contract close() was declared `() => void` with no stated obligation. That was harmless while migrateTo rejected pendings itself; now that it does not, close() is the retiring generation's only settlement path, so a type-compatible implementation that leaves a request pending strands its caller for good. States the obligation on the declaration and pins it for both trackers the real implementations reject through. Dropping the delivery-unknown flag, dropping the relay mark, or leaving pendings in the map each fail a test. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): read the cutover cause without a type assertion main's new casting gate rejects `(error as Error).cause`; narrow instead so the assertion still distinguishes a missing cause from an unmarked one. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Require padded file-write payloads to end on a Base64 quartet boundary. Cover both RPC methods and padded final upload chunks, and document client compatibility evidence. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
…t-branch commands The guard asserted that no git command runs for an open PR, using "no git at all" as a proxy for "no default-branch resolution". Remote-name listing is a separate concern, so allow it and keep every other command forbidden; the symbolic-ref/rev-parse resolution this issue is about stays unreachable.
|
Upstream stablyai#18455 merged into stablyai/orca on 2026-09-14, so this portfolio mirror is no longer needed. |
Upstream
Summary
Description GitHub and GitLab issue/PR resolvers no longer spawn
git remote get-url upstreamon origin-only clones. The four unconditional upstream-first resolvers now list remotes once (cached against the local git-config signature) and skip theupstreamprobe when thatNote
innocarpe/orcamainuntil the upstream PR is merged.