* fix(resource-manager): never destroy a session Orca cannot prove is idle (#8459)
Resource Manager decided a session was an "orphan" from the absence of a
renderer binding, then force-killed it with no prompt. Absence of a binding is
not evidence a session is idle — during restore the binding map is legitimately
empty, and deferred SSH sessions never appear in it at all. Live agent sessions
were destroyed this way, losing unrecoverable work.
Three gaps, one rule: only positive evidence authorizes destruction.
- `pty:listSessions` dropped `agentSessionOwners` at the IPC boundary, so the
renderer could not see the one fact that proves work is running. It now
reports `hasAgentOwner`, typed once in `shared/pty-listed-session.ts` so the
main handler, both preload surfaces, and the renderer cannot drift.
- The binding index ignored `deferredSshSessionIdsByTabId` — sessions restore
knows are live on an SSH host but has not reattached. No other binding source
can see them.
- The bulk-kill handler filtered sessions separately from the button's count,
so the set killed could differ from the set advertised. Both now call
`selectUnboundDaemonSessions`.
The single-row kill path had the same defect: it skipped confirmation whenever
`bound` was false. `requiresKillConfirmation` now also holds for agent-owned
sessions, and snapshot-derived rows carry ownership across from the daemon list
rather than reporting `false`.
* fix(resource-manager): distinguish unprovable ownership from proven absence
Adversarial review of the previous commit found it committed the same class of
error it was fixing: it collapsed "no agent owns this" and "this provider cannot
tell me" into one boolean `false`, and both destructive paths read that as proof.
A daemon generation below the claim protocol, an older SSH relay, or the
in-process local fallback all list no owners for a session that may well have
one. `pty.ts` already encodes the rule at :613 — "only providers that serialize
claims may make listing absence authoritative" — and the new IPC row ignored it.
So after upgrading with a legacy daemon still holding a live agent terminal,
bulk cleanup would have destroyed it: exactly #8459, one layer down.
`hasAgentOwner: boolean` is now `agentOwnership: 'present' | 'absent' | 'unknown'`,
derived via `providesAgentSessionOwnerListings`. Only `absent` authorizes
destruction, so `unknown` protects and confirms.
Second defect, found independently by four review lenses: the deferred-SSH
bindings reached the bulk selector but not `mergeSnapshotAndSessions`, because
the merge call site re-listed the binding fields instead of reusing the object.
A deferred SSH session therefore rendered `bound: false`, and its single-row kill
skipped confirmation while bulk cleanup correctly spared it. The call site now
spreads `resourceSessionBindings`, and a parity test fails if any binding field
is re-listed inline — the drift itself is now impossible to reintroduce quietly.
The e2e ownership assertion was also weak: it checked only that a boolean
arrived. It now asserts the exact arm, and that the live local provider reports
`absent` rather than `unknown`, so a degenerate all-unknown implementation fails.
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(remote): accelerate shared-control and pane recovery on resume/online
Narrow #8255 onto current main after #9774: fire pending shared-control
reconnect timers and pane recovery backoffs on system resume and browser
online, without replacing the per-pane recovery state machine or reconnect
banner UX.
* test(remote): cover online and occluded-resume recovery triggers
* fix(remote): centralize recovery acceleration
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* feat(plugins): Orca plugin system — kernel, content packs, panels, workers, marketplace v0 (experimental)
Adds Orca's experimental plugin system behind a settings flag: a
supervised kernel, declarative content packs (VM recipes, commands and
keybindings, language packs), sandboxed iframe panels, forked worker
hosts, and a Git-backed marketplace v0 with consent, provenance and
kill-list enforcement.
Theme, icon-theme and terminal-theme contributions are deferred to a
follow-up pass.
* fix(plugins): make unsupported marketplace listings unreachable by key
findPlugin() backs preview/install/previewInstalledUpdate via
requireListing(), so filtering only listPlugins() hid the catalog card
while leaving the dead install path reachable one click later.
* fix(plugins): fan Pi session-only status out to plugin subscribers
The providerSessionOnly early-return in applyNormalizedStatus emitted to
onAgentStatus (main-window fanout) but skipped enrichedStatusListeners, so
plugins subscribed to agent.status.changed silently missed every Pi
session_start event. Route both emit sites through one helper so a future
early return cannot drop the plugin tap again.
Co-authored-by: Orca <help@stably.ai>
* plugins: drop dead code and hoist duplicated trust-boundary patterns
Cleanup pass over the P1 diff, no behavior change:
- Delete `readPluginTreeSnapshot`/`readSnapshotFile` and their types, plus
the now-vestigial `directories`/`signal` plumbing in `collectFiles`.
- Delete `resolveContainedPluginDirectory` (no callers).
- Delete `plugin-content-load-pool.ts`; it reimplemented the existing
`mapWithConcurrency`, whose index arg also removes the pairing wrapper
in `buildPluginList`.
- Hoist `PLUGIN_CONTENT_HASH_PATTERN` and `PLUGIN_COMMIT_PATTERN` into
the install-lockfile module; 11 sites hand-rolled these identically.
- Point the new reliability gate at the PR instead of gitignored docs
paths, matching every other gate's link form.
* fix(plugins): retry plugin state renames on Windows AV/EPERM locks
Six plugin write paths (lockfile, provenance, current pointer, kill
list, marketplace cache, staged install dir) did a plain rename, so an
antivirus or indexer holding the target open surfaced as a failed
install. The repo already retries this hazard for issue #1507, but only
through a sync helper; these paths are all async.
Adds one bounded async retry + atomic write used by all six, and trims a
consent-provenance header that restated its own JSX.
* test(plugins): cover the Windows rename retry path
The retry loop shipped untested: both existing cases hit the non-retry path,
and the temp-cleanup test passed identically with the `finally` removed.
Mock `rename` to queue errno codes so CI can exercise locks it cannot provoke.
Co-authored-by: Orca <help@stably.ai>
* fix(plugins): pin bundled plugin resources to LF
Windows CI checks out with autocrlf, so the byte-hashed launch tree arrived
as CRLF and verify-packaged-plugin-resources rejected it — the packaged build
could never pass on Windows. Reproduced locally: CRLF yields the exact CI
error, LF verifies clean. Files are already LF, so nothing renormalizes.
Co-authored-by: Orca <help@stably.ai>
* test: guard the bundled-plugin LF pin against a CRLF checkout
The byte-hash mismatch only surfaced in Windows packaging CI. Assert the
.gitattributes pin and that a CRLF tree is rejected, so a regression fails
on any platform instead of waiting for a packaged Windows build.
Co-authored-by: Orca <help@stably.ai>
* ci: trigger packaged-build check on bundled plugin resource changes
The launch tree is byte-hashed during packaging, but no trigger path covered
it — so the CRLF fix for that check would not have re-run the check. Add the
resources, verifier and .gitattributes paths that can break packaging.
Co-authored-by: Orca <help@stably.ai>
* perf(plugins): rebuild the panel frame only when its baked theme values change
The revision keys the panel iframe, so every bump destroys the sandboxed
frame and its in-panel state. It counted root attribute mutations, but
--workspace-sidebar-live-width is written every rAF of a sidebar drag, so
dragging with a panel open blanked it ~60x/sec. Compare the two values the
shell actually bakes in instead.
Co-authored-by: Orca <help@stably.ai>
* test: stop pinning a plugin name in the CRLF guard
The CRLF case rewrites every launch file, so the reported mismatch is
whichever plugin sorts first. P2 adds theme plugins that sort ahead of
orca-navigation-shortcuts, which broke the assertion there.
Co-authored-by: Orca <help@stably.ai>
* style: drop stray blank lines left by the rebase resolutions
Both sides of the agent-hooks and orca-runtime conflicts contributed a
trailing blank, which oxfmt rejects. Whitespace only.
Co-authored-by: Orca <help@stably.ai>
* test(plugins): stop the startup budget failing on machine load
P95 runs 16-34ms idle but exceeds the 50ms bound under full-suite
parallelism, so the gate flaked. Widen it to catch an order-of-magnitude
regression instead; the no-worker/no-plugin-code assertions are the real
guarantee. Verified a 400ms regression still fails.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(daemon): respawn on PTY write dropped to a dead daemon socket (STA-2373)
DaemonPtyAdapter.write() sends keystrokes via fire-and-forget client.notify().
When the daemon dies (retirement, crash, kill), the socket disconnects and the
notify is silently dropped — no rejection reaches withDaemonRetry, so the
dead-endpoint respawn never fires and the attached pane freezes. Only a
request/reply RPC (e.g. createOrAttach from opening a new terminal) detected
the death and forked a replacement.
DaemonClient.notify() now reports delivery; a dropped write to a still-active
session drives the shared respawn coalescer directly (reconnecting the
permanent client before releasing the temporary adoption lease, mirroring
withDaemonRetry's ordering), so the pane self-heals like the createOrAttach
path. Cross-platform + SSH-safe: no platform assumptions, pure adapter logic.
Complements (does not duplicate) #8426, which fixes the adjacent in-daemon bug
where a thrown node-pty write no longer marks the handle dead. That is
daemon-side; this is the app-side dropped-notify that never triggered respawn.
* fix(daemon): restore adapter state after dropped-write respawn
* fix(daemon): recover writes after endpoint respawn
* fix(terminal): remount panes after daemon death
* fix(daemon): recover sibling panes after daemon death, not just the written one
When a daemon dies, its dropped-write respawn only remounted the pane whose
write detected the dead endpoint. Sibling panes (alive at death but not typed
into) were left frozen: stale prompt pixels, silently-dropped input, no live
child, and no recovery even on later keystrokes — the exact STA-2373
frozen-typing symptom on non-triggering panes.
DaemonPtyAdapter now fans a write-unavailable signal out to every active
session when it recovers from a dead endpoint, emitted while the sessions are
still in activeSessionIds so the renderer's liveness gate still reads them
live. pty.ts forwards each to the existing pty:writeUnavailable channel, so all
panes remount + re-attach through the same path the written pane already used.
Adds a revert-sensitive regression test: with two sessions and only one
written after the daemon dies, the sibling must also be signaled to recover.
* revert(format): drop repo-wide oxfmt churn unrelated to STA-2373
A review pass ran `oxfmt --write .` across the tree, pulling seven files
with no bearing on the dead-daemon respawn fix into the PR diff. Restored
to origin/main byte-for-byte so the diff carries only the respawn change.
* fix(daemon): snapshot active sessions before the write-unavailable fan-out
A listener that kills a pane mutates activeSessionIds mid-iteration, which
can skip the very sibling the fan-out exists to reach. Matches the snapshot
fanoutSyntheticExits already takes.
* fix(daemon): re-arm dead-endpoint recovery on every daemon death
The respawn-storm latch was only released once every awaiting session
rebound. Background sessions have no mounted pane, so nothing ever calls
createOrAttach for them and they hold the awaiting set non-empty forever
— latching the fan-out off after the first death and silently making the
whole fix one-shot. Re-arm on the disconnect event instead, which fires
once per established connection, so the storm guard still holds within a
single incident.
* fix(daemon): route the write-unavailable fan-out through the pty router
Main subscribes on the routed provider, and DaemonPtyRouter is the live
localProvider whenever a legacy daemon socket exists — the common case
when an in-place update bumps PROTOCOL_VERSION with terminals running.
It forwarded write but not onWriteUnavailable, so the fan-out reached no
listener and only the written pane recovered: STA-2373 unfixed, silently.
Also stop rejecting writes on adapters that cannot respawn. Legacy
adapters have no respawn, so the remount reattaches to nothing and
rebuilds the pane empty, losing scrollback the user could still read —
worse than the pre-existing silent drop. And guard the renderer's
write-unavailable handler on ptyId like its sibling data/replay handlers,
so a transport that rebinds without detaching cannot remount a healthy
pane.
* fix(daemon): route the write-unavailable fan-out through the degraded provider
DegradedDaemonPtyProvider is the live localProvider in degraded launch
mode and main subscribes on it, but it forwarded onData/onExit/onReplay/
onBackgroundStreamEvent and not onWriteUnavailable — so the fan-out
reached no listener and siblings stayed frozen. Same defect as the router,
one provider over.
The file sat at its max-lines ceiling, so make room by reusing one
combineUnsubscribes helper across the three places that already repeated
that loop rather than bumping the limit. Forward to the daemon adapters
only: the local fallback has no dead-socket problem.
* refactor(daemon): share the listener-fanout unsubscribe combination
Adding onWriteUnavailable to both provider wrappers left each file at
exactly 300/300 lines, so the next line anyone added would have broken
max-lines with no sanctioned escape hatch. Both already repeated the same
combine-unsubscribes loop, so lift it into one module: duplication drops
and each file gets its headroom back.
* fix(test): stop the fake emitter colliding with the private adapter emitter
DaemonPtyAdapter.emitWriteUnavailable is private, so declaring a public
member of the same name on a mock intersected with DaemonPtyAdapter
collapsed the whole type to never — one collision produced 54 typecheck
errors, taking out pre-existing assertions in both files too. Rename the
fake to triggerWriteUnavailable and declare onWriteUnavailable on
ProviderMock, which IPtyProvider does not carry on this branch.
vitest does not typecheck, which is why a red build sat behind a green
suite.
* feat(source-control-ai): add {linkedIssue} recipe variable for commit and PR prompts
Custom commit-message and pull-request recipes can now reference the GitHub
issue linked to the workspace, so a template like "Fixes #{linkedIssue}" lands
the closing trailer without the user retyping the number.
- register `linkedIssue` on the commitMessage and pullRequest actions only,
with the VARIABLE_INFO entry the chip hover card requires
- substitute unconditionally via `formatLinkedIssueTemplateValue` (empty string
when nothing resolves) so the token never survives into a prompt; enrich the
draft context conditionally via `withLinkedIssueDraftContext` so unlinked
workspaces keep their existing context shape
- attach at the 7 call boundaries (runtime commit x2, runtime PR shared, IPC
commit x2, IPC PR x2); the pure git gather stays pure
- validate the renderer-supplied worktreeId against the request path and repoId
before any meta read, comparing SSH paths as raw strings so a Windows host
cannot rewrite a remote POSIX path
- built-in prompts are unchanged; no GitLab dual-read and no default trailer
* fix(source-control-ai): resolve {linkedIssue} adversarial review findings
Addresses 13 of the 14 findings from the {linkedIssue} code review
(6 minor, 8 nit, 0 critical, 0 major); Issue 5 (GitLab provider naming)
is deferred to design Open Question 3 as product expansion.
Behavior:
- Dialog previews the workspace's real linked issue instead of the
synthetic 123, in both the chip hover card and the plan preview, so an
unlinked workspace previews the `Fixes #` it will actually generate.
Settings dry-runs stay fully synthetic.
- Reject non-positive, fractional and unsafe-integer issue numbers at the
IPC resolver via a shared isLinkedIssueNumber predicate, so corrupt meta
never reaches a draft context (previously -7 rendered `Fixes #-7` and
1e21 rendered `Fixes #1e+21`).
- Fail closed on an empty-string repoId instead of skipping the cross-check.
Structure:
- Split the variable registry into source-control-ai-action-variables.ts
and re-export it, restoring max-lines headroom with no consumer churn
and no lint disable.
- Constrain withLinkedIssueDraftContext to contexts declaring linkedIssue.
- Move the misplaced shared imports into their import group.
Docs and tests:
- Document that the IPC id/path validator guards relay/CLI/future callers,
not the renderer (whose path is id-derived), and rename the three tests
that read as proof of a protection that cannot fire.
- Add PR-side coverage that was missing: three git:generatePullRequestFields
handler tests, a built-in PR prompt no-leak guard, and the runtime PR
unlinked case.
- Replace the coincidental '42' assertion with a fixture-unique sentinel.
- Type the runtime worktree fixture with satisfies, which surfaced and
fixed pre-existing drift in its git sub-object.
- Add an e2e case covering the preload -> main -> meta -> template chain.
Co-authored-by: Orca <help@stably.ai>
* fix(source-control-ai): resolve {linkedIssue} adversarial re-review findings
Addresses all 8 findings from the {linkedIssue} code re-review
(2 minor, 6 nit, 0 critical, 0 major); none deferred.
Behavior:
- Revert the variableOverrides parameter on planSourceControlTextGeneration.
Its result is a Save/Generate gate, not a preview, and the recipe it
validates is saved repo- or globally scoped -- so rendering it against the
active workspace disabled both buttons with "Command input is empty." for a
{linkedIssue}-only template on any unlinked workspace, blocking a global
settings write. Validation is synthetic again; chip previews are unchanged.
- Make the chip hover card additive instead of either/or. A supplied preview
now appends a "This workspace" sample below the description and Example
rather than replacing them, so the GitLab-empty and dangling `Fixes #`
warning survives on the two dialogs where recipes are actually authored.
basePrompt keeps its preview-only shape, where the preview is the content.
Structure:
- Drop the registry re-export from source-control-ai-actions.ts and move the
last two consumers onto source-control-ai-action-variables, so one import
path per symbol keeps a grep of the registry's consumers complete.
- Split the registry/helper suites into source-control-ai-action-variables.test.ts
so each test file mirrors its module.
Tests:
- Cover the Save/Generate gate at the canRunGeneration level for a bare
{linkedIssue} recipe on linked and unlinked workspaces, with a negative
control proving the buttons can still be disabled.
- Cover the chip hover card directly; the dialog tests mock it away.
- Guard the PR mismatched-id test with toHaveLength(1) so it cannot pass
vacuously on an unrelated early return.
- Add an unlinked-workspace e2e case (saw-issue:empty), which is what
distinguishes a real resolver from one that always returns a number.
Spec now runs green: 3 passed.
- Rename the dialog test that claimed a synthetic-fallback assertion it did
not make, and route its renders through one shared helper.
Docs are worktree-local (.gitignore:84 ignores docs/**): the design doc's
plan-preview and chip-surface claims, the manual QA rows, and both reviews'
statements about pre-existing PR-handler tests are corrected there.
* fix(source-control-ai): make the {linkedIssue} e2e guard and dialog test falsifiable
The e2e unlinked case extracted the echoed issue with `ORCA_E2E_ISSUE=(\d*)`,
which matches zero digits in front of an unexpanded `{linkedIssue}` and reported
it as `empty` — so the case that exists to catch a literal token surviving into
a prompt passed on exactly that regression. Capture the whole line instead: a
literal now arrives as `saw-issue:{linkedIssue}` and fails, verified by dropping
the substitution key for unlinked contexts and watching the case go red.
Also drop the inert `not.toContain('Command input is empty.')` assertion — that
copy is click-driven `generationError` state and this suite renders statically,
so it could never fail; the claim it reached for is carried by the plan test.
Rename two plan tests off the "plan preview" framing the design now rejects.
Local review artifacts (design doc, implementation notes, final review) were
swept to match the tree in the same pass; they are gitignored here.
* Resolve {linkedIssue} from live metadata, not cache
Resolved worktrees are cached for a second, causing commit and PR
generation to use stale linked-issue state. Hosts now implement
getWorktreeLinkedIssue to provide fresh issue metadata by worktree id,
with proper fallback for unlinked workspaces. Updates both commit
message and PR field generation paths; includes integration and e2e
coverage.
* Keep cached linkedIssue when metadata is unavailable
Return undefined from getWorktreeLinkedIssue when live metadata cannot be read
(store not ready), distinguishing it from null (unlinked). The caller now falls
back to the cached worktree value instead of treating unavailable as unlinked.
Also extract the linked-issue echo generator as a shared e2e test helper.
---------
Co-authored-by: Orca <help@stably.ai>
* feat(codex): surface a stalled config sync instead of failing silently
Why: the mirror keeps serving the last synced settings when ~/.codex/config.toml
is missing, blank, or unreadable. That is the right call for data safety, but it
is invisible — a downed WSL distro or an unhydrated cloud-synced home leaves
"Orca ignores my config edits" with no log line and no UI to diagnose.
Status is derived on demand from the same predicates the mirror uses, so the two
cannot disagree. The stall is logged once per episode rather than on every launch
and quota poll, and the Codex account section names the file and what to do.
* fix(codex): latch an unreadable source and stop over-claiming recovery
An unreadable source throws out of the mirror, so reporting only on the success
path left that stall latch-less: it logged the raw failure on every launch and
quota poll while its reason never reached the surfaced status. Report from the
catch path too.
The clear message also claimed the source was "readable again", which is false
when the stall ended because the runtime config was removed rather than because
the source came back.
Restoring console.warn now happens in afterEach — an inline mockRestore is
skipped by a failing assertion, and the leaked spy made every later case in the
block fail spuriously.
* fix(codex): latch the stall promotion hits first, and scope it to the host
Review round 1 findings:
- The unreadable-source latch still never fired in the steady state. Once a
baseline exists, promotion reads the source before the mirror does, so it
throws first and `!promotionPlan` returned before any reporting — logging a
reasonless failure every launch and quota poll, which is exactly what the
previous commit claimed to fix. Report from that branch too. The test only
passed because its fixture had no baseline; it now seeds one first and fails
without the fix.
- The banner named the host's ~/.codex while a WSL or per-account runtime was
selected, whose real source is a different file entirely. Gate it to the host
scope, matching how the sign-in warning is already gated.
- Three new translate keys were missing from the locale catalogs, failing the
localization gate in `pnpm lint`.
- The registrar mock was never asserted, so deleting the registration left the
suite green.
- `codexConfigSyncStatus` hung off the `agentHooks` namespace despite having
nothing to do with agent hooks; moved to its own `codexConfigSync.status`
while it is still a four-file change.
* fix(codex): report sync health for the home the selection actually mirrors
Review round 2:
- The status resolved the shared runtime home, but the system default now runs
Codex directly against ~/.codex and managed accounts get their own home. So a
stalled per-account mirror showed no banner at all, while a stale shared home
could warn about a config the active lane never reads. Resolve the mirrored
home from the current selection, and report synced when the lane has no mirror
to fall behind.
- The round-1 report on the promotion failure path could clear the latch on a
pass where no mirror ran, claiming a recovery that never happened and
silencing every later pass. Only ever latch a stall there; leave clearing to
the path that actually mirrored.
* fix(codex): refetch sync status when the active Codex account changes
Review round 3:
- Resolving the status per selection made the fetch account-dependent, but the
effect was not keyed on the active account. Switching accounts left the banner
describing the previous one — and switching INTO a stalled account showed
nothing at all, which is the silence this change exists to remove.
- Pin the home resolution itself: it had no direct test, and its shared-home
path was a hand-copied literal that could drift from the real helper and
silence the banner with every other test still green.
- Narrow the handler's dependency to the one method it calls, which also drops
an `as unknown as` cast from its test.
- Skip the chmod-based test on Windows, where a read-only directory does not
block writes so the scenario cannot be constructed; matches the convention
already used in config-settings-promotion.test.ts.
* chore(codex): restore the handler docstring and isolate the resolver suite
Round 4 returned clean; these are its two non-blocking nits.
Narrowing the handler param left its JSDoc stranded above the new type, so the
function had no hover doc. The resolver suite also read the developer's real
CODEX_HOME and shell rc, so anyone exporting one would see it fail locally.
* fix(window): reflow on macOS occlusion-reveal so the bottom bar is not clipped
Fixes STA-2383.
On macOS the window is background-throttled while hidden; on occlusion-uncover only 'focus' fires and its handler runs webContents.invalidate() (the setSize jiggle is skipped to avoid SIGWINCH-ing terminals). invalidate() repaints but does not reflow, so the app-shell h-dvh root keeps a stale dynamic-viewport height and the StatusBar is clipped below the viewport ('no bottom bar'); a manual resize restores it.
Fix: renderer relays a genuine hidden->visible reveal (visibilitychange) to main via new ui.notifyWindowRevealed IPC; main runs the same proven forceRepaint. Occlusion-gated (no per-Cmd+Tab SIGWINCH regression), darwin-scoped, sender-guarded, cleaned up on close.
Test plan: vitest createMainWindow + web-preload-api green. Recommend on-device macOS occlusion QA before merge.
* fix(window): preserve user resizes during reveal repaint
* fix(app): bound the wake/quit paths implicated in the phone-session-ended freeze (#9447)
- relay-transport: waitForClose now times out (5s) so a half-open post-sleep
socket can't wedge runtimeRpc.stop()
- will-quit: race teardown against a 20s deadline so app.quit() always runs
(Force Quit was the only escape when any teardown member never settled)
- terminal-fit-restore: local restoreTerminalFit invoke gets the same 15s
bound as the remote path so the held-fit modal buttons can't pin disabled
* fix(app): close wake recovery timeout gaps
* fix(relay): drop late frames after forced teardown
* fix(relay): fence detached socket callbacks
* fix(app): close timeout resource gaps
* fix(relay): detach retired mobile transports
* fix(types): exclude absent stat overloads
* fix(runtime): expire wedged terminal restore dedupe
* fix(runtime): keep restore retries on one reclaim
* chore(skills): refresh bundled skill manifests
* fix(window): fence quit acknowledgements by request
* fix(relay): bound revoked device socket cleanup
* fix(status-bar): seed Resource Manager closed badge from daemon inventory
The closed chip counted tab/layout PTY wake hints (inflating terminal
count) and never fetched memory until the popover opened (showing "—").
Cache listSessions for the badge, seed memory on session ready, and drop
the wake-hint closed selector.
* fix(status-bar): update session inventory ref in an effect
CodeRabbit/React Doctor flagged mutating sessionInventoryRef during
render; keep the write in useEffect after commit.
* fix(status-bar): follow daemon session lifecycle events
* perf(status-bar): skip inventory scans for known PTYs
* perf(status-bar): bound resource inventory refreshes
---------
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* feat(dashboard-popout): size the terminal dialog's PTY to the dialog grid
The popout agent-terminal dialog rendered the pane's serialized frame at its
original cols/rows and CSS-scaled it down to fit. The dialog now claims the
PTY grid for its own box through the remote-desktop viewer registry: the PTY
reflows to the dialog's dimensions (crisp, unscaled), the main-window pane
parks at the held grid like any remote viewer hold, and closing the dialog
releases the claim so the pane reclaims its geometry. A phone-driven PTY
keeps the floor; the dialog then falls back to the scale-to-fit rendering.
Any grid change under a live preview stream (fit landing, host reclaim,
phone takeover) now pushes a resync so the dialog repaints from a snapshot
at the new grid — this also fixes garbled dialogs when the pane resized
while a dialog was open.
* fix(dashboard-popout): harden terminal grid claims
* perf(dashboard-popout): bound terminal preview resize work
Allow the existing "Open in" entries to launch a configured VS Code
launcher against an SSH-backed worktree via Remote-SSH:
code --remote ssh-remote+<authority> <remote-path>
- Split the blanket SSH/runtime block into a capability model: file
managers and non-VS Code launchers stay local-only (disabled with
"Local only" metadata); a recognized VS Code command is enabled and
forwarded with connectionId over a typed object IPC.
- Main process stays authoritative: rejects active/owned runtimes,
resolves the SshTarget from the persisted Store, derives the authority
(config alias, or username@host on port 22, or ssh-alias-required on a
non-default port), validates POSIX/Windows absolute remote paths without
local stat/normalize, and rejects non-VS Code and compound commands
before spawn.
- Authority and remote path are passed as separate argv; getSpawnArgsForWindows
remains the cmd/bat shim boundary and fails closed on metacharacters.
- Same capability rules across the worktree menu, Explorer overflow, and
the source-control entry context menu.
Refs STA-2386
Closes#9999
* feat(mobile-pairing): surface unpaired-device auth failures instead of silent 4001 loops
Desktop: when a phone repeatedly fails direct-transport E2EE auth with a
token missing from the device registry (pre-v1.4.106 pairing-path bug left
desktops that regenerated their registry rejecting paired phones forever),
throttle to one notification per session and show an actionable toast
pointing at Settings -> Mobile to re-pair.
Mobile: map a bare 4001 close onto the existing auth retry budget (the
encrypted e2ee_error is undecryptable when the desktop keypair changed, so
the close code is the only surviving signal) instead of looping the generic
reconnect forever, and make the auth-failed verdict say 'Pairing invalid -
re-pair with your desktop' instead of a bare 'Auth failed'.
* fix(mobile-pairing): handle stale keys and startup notification races
* fix(mobile-pairing): isolate auth notification failures
* fix(mobile-pairing): keep recovery alert actionable
* fix(github): load PR diffs for Enterprise remotes
* fix(github): encode PR content paths by segment
* Fix PR review actions failing on GitHub Enterprise remotes
- Threads GitHub host identity (not just owner/repo) through the client,
work-item-details, issues, and RPC layers so gh commands target the
correct Enterprise server instead of silently falling back to github.com
- Adds a shared github-api-repository helper to resolve/host-qualify repo
identity consistently across REST, GraphQL, and CLI shorthand calls
- Scopes the gh rate-limit breaker and singleton rate-limit snapshot by
host/runtime so a github.com block or probe can't affect GHES or WSL
- Coalesces concurrent host-auth probes and paginates PR file fetching
beyond 100 results
- Propagates `host` through renderer PR caches, checks-panel keys, and
preload IPC types so Enterprise and github.com data never collide
* Route gh host qualification through runner options instead of argv sniff
Move GHES/GH_HOST resolution from parsing --hostname/--repo out of gh argv to an explicit options.host passed through ghExecFileAsync, since SSH-backed repos spawn gh with no cwd and argv sniffing couldn't reliably detect the target host. The runner now injects --hostname and qualifies --repo/-R at spawn time from options.host, and rate-limit scoping/guards use the same explicit host instead of inferring it. Also adds a shared githubRepoIdentityKey helper to keep cache/store keys consistent with the new host-aware repository identity.
* Fix gh CLI GHES host pinning and rate-limit scope leaks
- Pin `--host` on every gh call site so a process-level GH_HOST can't
silently redirect requests, and qualify `-R`/`-R=` repo shorthand
alongside the existing `--repo=` handling.
- Check the target scope for an active rate-limit block before each
WSL/native or host fallback retry, not just on the initial attempt,
so a blocked scope can't be hit again through a fallback path.
- Compute idempotency once per call instead of re-deriving it after
fallback reassigns args.
* Fix GitHub Enterprise host identity loss across PR/work-item paths
- Thread `host` through mobile PR RPC params, IPC work-item lookups, and
RPC schemas so GHES identity survives the renderer/mobile/main boundary
instead of silently falling back to a same-named github.com repo.
- Qualify `--repo`/`-R` args for github.com too (not just GHES), since
gh resolves bare shorthand against a process-level GH_HOST that can
redirect pinned github.com commands.
- Cache `getOriginGitHubApiRepository` to avoid a per-call uncached
`git remote get-url` round trip on connection-backed repos.
- Add a local-fork fallback in `getWorkItemDetails` so PRs living on a
base repo (not visible via the origin slug) still resolve via cwd.
- Centralize the github.com-vs-GHES host predicate in
`isDefaultGitHubHost` so cache keys, quota scoping, and identity
checks can't drift out of sync.
* Make repository identity host-aware across all GitHub surfaces
Generalize the auth-gated enterprise resolver to any remote and build a
cached hosted-identity family (origin/issue/candidates/source) on top of
it, then migrate every github.com-only consumer: Tasks listing/counting,
branch-to-PR discovery, push targets, fork upstream, issue operations,
Projects, web links, avatars, and PR-link facts. Scope the rate-limit
breaker probe per runtime:host and classify WSL UNC cwds correctly.
Co-authored-by: Orca <help@stably.ai>
* Fix expected slug to include host field in GitHub PR link test
Updates the smart-source paste-intent test fixture to match the
repository slug shape that now carries a `host` field, keeping GHES
host identity intact through the paste-intent parsing path.
* Surface per-host gh auth state for GitHub Enterprise
diagnoseGhAuth accepts the host a surface needs credentials for, scopes
the account/scope diagnosis to that host, and reports whether gh has any
login there; GhAuthErrorHelp renders host-qualified login/refresh
commands so an unauthenticated GHES host stops masquerading as a
github.com scope problem. Also fixes the mobile paste-intent expectation
for host-carrying parsed links.
Co-authored-by: Orca <help@stably.ai>
* Bound GHES identity caches and preserve non-default ports in host identity
Cap the origin-repo and host-auth caches like ownerRepoCache; keep ports
from remote/link URLs so GHES on a non-default port is a distinct
identity; make positional github.com slugs explicit against GH_HOST;
compare work-item sources by host-aware identity key; bail cwd-less
branch lookups when no repository candidate resolved; thread host
through the renderer work-item slug lookup.
Co-authored-by: Orca <help@stably.ai>
* Thread GitHub host through issue detail requests
Incorporates ghes-issue-host-support (ed6bb96ef): one hosted issue
repository identity is resolved before the details fan-out so comments,
timeline, participants, and mention lookups cannot drift across hosts,
with SSH guards so unresolved issue/PR repositories never fall through
to gh's default host.
Co-authored-by: Orca <help@stably.ai>
* Scope remaining GitHub rate-limit accounting
* Resolve typed PR lookups across hosted repository candidates
getWorkItem's PR path probes upstream-then-origin hosted candidates
instead of origin alone, so fork checkouts resolve the base repo's PR
with the right host; issue detail resolution reuses the up-front hosted
identity and keeps the SSH unresolved-host guards.
Co-authored-by: Orca <help@stably.ai>
* Refactor GitHub repository execution setup
* Carry host on smart-submit link intents
Co-authored-by: Orca <help@stably.ai>
* Carry the project host on GitHub item dialog origins
Co-authored-by: Orca <help@stably.ai>
* Keep GHES web ports but drop SSH transport ports in host identity
Supersedes PR #9118 on this branch: http(s) remote ports identify the
Enterprise web/API endpoint and are preserved, while ssh/git transport
ports (including ssh.github.com:443) never leak into gh's host identity.
Replaces the ssh.github.com:443 special case with the structural
protocol split and ports the PR's parsing test suite.
Co-authored-by: Orca <help@stably.ai>
* Support GitHub Enterprise diffs and mutations with host-scoped caches
Parse GitHub host identity from work-item URLs and carry it through PR/issue mutations, labels, and assignments. Bound rate-limit and scope-probe caches (1024 and 512 entries) to prevent unbounded growth when interacting with multiple GHES instances. Normalize repository identity keys to include host so github.com and GHES slugs don't collide in cache and equality checks.
* Support GitHub Enterprise diffs and mutations with host-scoped caches
- Carry host identity through PR mutations and reads so fork PRs on
different GHES instances don't collide in cache or state tracking.
- Validate host authentication before routing requests to unconfigured
Enterprise servers; ambient credentials must never reach untrusted hosts.
- Scope rate-limit guards and spend tracking per host so GHES quota stays
independent from github.com quota.
- Respect explicit --hostname arguments in gh CLI calls ahead of GH_HOST or
ambient defaults, so breaker state follows the actual request target.
- Detect implicit WSL runtimes from UNC paths for consistent host auth and
execution-options scoping across mobile and desktop clients.
* Support GitHub Enterprise work-item diffs with host-scoped execution
Enterprise PRs must use their selected host consistently across diff, comments,
and file-content loads. Validate repository slugs before authenticated execution
to prevent path-injection via renderer overrides. Scope project browsing cache
and rate-limit tracking by host to prevent cross-host pollution. Use parsed
URLs as authoritative over ambient hosts for project resolution.
* Support GitHub Enterprise work-item diffs with host-scoped execution
Preserve host identity on PR/issue work items throughout the mutation and diff
pipeline so Enterprise instances (including ported endpoints like
github.acme.test:8443) can execute mutations without ambiguity. Rate-limit gh
commands by the pre-qualified --repo host, cache auth state per ported host,
and surface Enterprise hosts in project metadata and error messages.
* fix(review): drop dead rateLimitGuard/noteRateLimitSpend re-export
Both callers (project-view.ts, mutations.ts) moved to the host-scoped
repositoryRateLimitGuard/noteRepositoryRateLimitSpend; the bucket-only
re-export in internals.ts had zero importers left.
Co-authored-by: Orca <help@stably.ai>
* fix(ci): split Enterprise host work-item tests under max-lines
Move GHES/SSH host-routing cases out of work-item-details.test.ts so
the suite stays within the 800-line test max-lines budget.
* test(github): align mocks with host-scoped repository resolution
- Route origin repository resolution through getOwnerRepoForRemote, not getOwnerRepo, to match production path
- Pin github.com host on origin results so host-less fixtures pass host gate in resolveGitHubApiRepository
- Add generation-based invalidation to prevent stale slug-cache writes from in-flight resolutions
- Fix ref-sync race in ProjectPicker: use useLayoutEffect so committed tree owns browse cache key
- Defer handledCrossRepoUrlRef assignment in SmartWorkspaceNameField until resolution succeeds
- Update Enterprise host routing: found work items must not silently fall back to default host when unresolved
- Normalize GHES avatar URLs: accept explicit port 443 as canonical form, not a fallback trigger
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* feat(codex): backfill managed-home sessions into the real Codex home once per host
Orca-launched Codex sessions currently land only in the Orca-managed
runtime home, so the user's own `codex resume` picker and app history
never see them (#4444, #8612). Backfill the managed sessions tree into
the real ~/.codex/sessions/YYYY/MM/DD layout once per host:
- hardlink first (one physical rollout log), copy as the cross-volume
fallback; existing target files are always skipped, nothing in either
home is deleted or moved
- idempotent; per-file failures leave the completion marker unset so the
next startup retries cheaply
- JSONL audit log of every link/copy/failure under
<userData>/codex-session-backfill/
- honors the custom Codex session source home override, mirroring the
existing system->managed bridge
WSL managed homes are distro-local and need an in-distro variant; that
is a follow-up.
* feat(codex): flag-gated system-default real-home routing scaffolding
Staged internal flag (default OFF, no settings UI): route the SYSTEM-DEFAULT
Codex account at the user's real ~/.codex instead of Orca's managed runtime
home. Flag OFF is byte-identical to today; managed (multi-account) selections
are unchanged in either state.
Routing (flag ON + host system default = no managed account):
- CodexRuntimeHomeService.prepareForCodexLaunch / prepareForRateLimitFetch
return null so the PTY/env layer injects no managed CODEX_HOME and the
rate-limit fetcher + auth-presence gate fall back to ~/.codex (the background
poller stops spawning Codex against the managed home — the #5370 auth war).
- buildPtyHostEnv strips only a nested-Orca-inherited Orca-owned override
(CODEX_HOME matching the private ORCA_CODEX_HOME marker), preserving a
user-set CODEX_HOME. Shell-ready re-exports already no-op without the marker.
- The headless commit-message Codex path strips the same inherited override.
Hook install for the real-home lane (append-last into ~/.codex/hooks.json,
trust via the app-server client) lands with the trust plumbing; the managed
hook install is skipped for this lane meanwhile.
Credit @jellychoco (#8606) for the native-home routing direction.
Depends on the codex trust-rpc-grant plumbing for the real-home hook installer.
* fix(codex): strip the daemon-inherited Orca CODEX_HOME override for real-home routing
The daemon spawns PTYs from its own inherited environment and honors only
spawnOptions.envToDelete, so mutating the sparse env object was not enough to
strip an Orca-owned CODEX_HOME the daemon already carries. Add the strip to
envToDelete for both daemon host-spawn paths, preserving a user-set CODEX_HOME.
Verified live via CDP against a sandboxed dev instance (flag ON): an
Orca-spawned pane reports empty CODEX_HOME/ORCA_CODEX_HOME, so Codex resolves
its own ~/.codex. Adds daemon-path unit coverage (strip Orca-owned, preserve
user-owned, no-op when flag OFF).
* fix(codex): harden one-time session backfill
* test(codex): cover staged cross-volume install
* feat(codex): app-server trust-grant client, capability cache, and grant ledger
Short-lived codex app-server JSON-RPC client (hooks/list + config/batchWrite,
the same pair the Codex TUI 'Trust all' flow calls), run in a bundled
ELECTRON_RUN_AS_NODE entry so synchronous launch prep can block on it with a
hard deadline and guaranteed child reap. Capability cache modeled on
GitCapabilityCache, scoped per execution host (native vs each WSL distro),
with a narrow unknown-method/missing-subcommand unsupported predicate. The
grant ledger records verified grants so steady-state launches skip the RPC.
* fix(codex): grant managed hook trust via codex app-server RPCs in install/refresh
Host and WSL installs now grant trust for Orca's managed status hooks through
codex's own hooks/list -> config/batchWrite -> re-list verify, scoped to
exactly the managed entries; the previous computeTrustedHash lane is the
unchanged fallback for incapable/erroring CLIs. getStatus and the removal
paths recognize ledger-recorded codex hashes so drift between codex's real
algorithm and the replica no longer misreports or strands trust. SSH remote
install is untouched by design.
* test(codex): cover app-server trust grant client, cache, ledger, and lanes
* test(codex): cover commit-message real-home override strip/preserve
Adds the two cases for the headless commit-message Codex env under real-home
routing: a nested-Orca-inherited Orca-owned CODEX_HOME is stripped, and a
user-owned CODEX_HOME is preserved.
* test(codex): WSL grant-lane coverage — in-distro invocation and fallback parity
* feat(codex): real-home hook installer trusted via the codex app-server grant client
With the real-home flag ON and the system-default selection, install Orca's
status hook into the user's real ~/.codex before any pane spawns:
- entry APPENDED LAST per managed event: codex hook trust keys are positional
(source:event:group:handler), so appending keeps every user entry's position
and trust record intact; user entries and unknown top-level hooks.json fields
are preserved verbatim
- trust is granted exclusively through the codex app-server client
(hooks/list + config/batchWrite, verified by re-list); Orca never writes
[hooks.state] into the user's real config.toml itself
- if the grant lane is unavailable (old binary, unsupported RPC, verify
failure), the appended entry is rolled back byte-exactly and the host keeps
the managed-home lane end to end (PTY env, rate limits, commit messages)
via a lane gate on the runtime-home service
- one-time pristine backup of the user's hooks.json under Orca's userData;
a rolling .bak sits next to the file (existing atomic writer)
- hook opt-out sweeps Orca entries from the real home and drops Orca-owned
trust records; flag-off downgrade re-arms the existing legacy system-home
sweep, which removes the entry and its trust keys cleanly
- the legacy system-home sweep is suppressed only while the real-home lane
owns ~/.codex/hooks.json, so managed installs cannot delete the entry
* fix(codex): resolve the trust-grant entry without requiring electron
The grant bridge is reachable from plain-Node CLI entries, where the
plain-node entry guard rejects any chunk containing require("electron").
Resolve the bundled session entry from __dirname (root chunk and chunks/
layouts) with an app.asar -> app.asar.unpacked rewrite for packaged runs,
instead of electron's app path APIs.
* fix(codex): keep session backfill off main thread
Use asynchronous, sequential filesystem operations for the one-time rollout backfill, and avoid repeated target-directory probes. Treat inaccessible managed session roots as retryable failures instead of writing a false completion marker.
* fix(codex): harden app-server trust grant fallback
* fix(codex): install cross-volume session backfill copies atomically
On a real Codex home whose filesystem supports no hardlinks (exFAT/FAT,
some network mounts), the staged cross-volume copy was installed with a
non-atomic copyFile(..., COPYFILE_EXCL) straight into the final
rollout-*.jsonl name. An install interrupted mid-copy (app quit, crash,
ENOSPC during the deferred run) could strand a truncated rollout that the
next run then skips as already-present, defeating the staging design's own
guarantee that a failed copy never leaves a partial session behind.
Install the fully-staged copy with an atomic rename instead, guarded by an
existence re-check so it keeps the never-overwrite contract (and the rename
source is the same immutable managed rollout, so any clobber would be
byte-identical). Cover the no-hardlink-support target and an interrupted
install that must leave no partial in the user's sessions tree.
* fix(codex): resolve grant entry from __dirname so plain-node CLI entries stay electron-free
The build guard rejects any electron require reachable from plain-node
entries; the bridge now maps app.asar to app.asar.unpacked by string
replacement instead of consulting electron app paths. CLI typecheck project
lists the new trust-grant module graph.
* fix(codex): harden trust grant reconciliation
* fix(codex): restore trust config permissions on rollback
* fix(codex): harden real-home routing cleanup and retries
* fix(codex): preserve unicode trust RPC responses
* fix(codex): preserve remote env and complete real-home cleanup
* fix(codex): preserve real-home lane invariants
* test(terminal): isolate replacement idle reset assertion
* fix(codex): preserve real-home dotfile links
* fix(codex): preserve verified trust grants across launch prep
* fix(codex): preserve dangling config symlinks on rollback
* fix(codex): don't revoke a just-granted WSL home on a false 'missing' probe
The async wsl.exe canonical-path settlement could report the runtime home
'missing' immediately after a verified RPC grant (a false negative — codex
had just written and re-listed trust there), which drove the reconciliation
'remove' branch to delete all six granted [hooks.state] tables, leaving a bare
[hooks.state] the launching pane read as 'hooks need review'. A 'missing'
settlement now revokes only when no successful install ran this generation; a
genuinely moved home still resolves to a different path and reinstalls.
* test(codex): model codex config/batchWrite faithfully on Windows
The grant-lane stub simulated codex by calling Orca's upsertHookTrustEntries,
which writes both separator variants for a Windows key (a fallback-lane compat
shim real codex never does) — fabricating duplicate tables and whitespace the
RPC path never produces, so the byte-stable and no-duplicate assertions failed
on win32. Replace it with a single-variant, blank-line-separated writer that
matches the real 0.144.x binary's output.
* feat(codex): collapse duplicate session listings across Codex roots
Backfilled/bridged rollouts are hardlinked into both the real ~/.codex and
Orca's managed runtime home, so AI Vault listed each session once per root
(#7521). Dedup candidates by rollout file name pre-parse and parsed sessions
by session id post-parse, keeping the canonical root: host real home first
(unprefixed resume), then the managed runtime home, then other homes. Applies
to local, WSL, and SSH-remote scans.
* feat(codex): background sqlite index heal for backfilled sessions
Codex's own state-DB metadata backfill is one-shot, so rollouts hardlinked in
by Orca's session backfill never become visible to Codex's DB-driven surfaces.
Extract the app-server stdio JSONL transport into codex-app-server-session
(shared with the trust-grant client) and add a bounded, resumable background
pass that drives Codex's lazy indexing via thread/read per backfilled session:
recent-first, batched onto one short-lived server per batch with small
concurrency, ledger + marker so steady-state startups are a no-op, stop-aware
on quit, and capability-aware on CLIs without the app-server surface.
* fix(codex): preserve session identity during dedup heal
* fix(codex): preserve user trust during real-home cleanup
* fix(codex): harden real-home heal boundaries
* fix(codex): fail closed on unsafe backfill install
* fix: harden real-home hook cleanup
* fix(ai-vault): preserve execution boundaries and reap children
* fix(codex): narrow app-server unsupported detection
* fix(codex): bound user hook trust rebase retries per host
The rebase lane ran a codex app-server session on every launch prep while a
host was stuck (CLI without app-server support, or keys hooks/list cannot
match). Gate the transaction on the shared capability cache and add the same
5-minute transient cooldown the grant lane uses, so sweep and legacy-cleanup
retries cost plain fs reads instead of a codex session per pane spawn.
* fix(codex): enforce real-home resume and heal boundaries
* fix(codex): establish real-home lane before cleanup
* fix(codex): stop index heal before delayed spawn
* fix(codex): protect symlinked rolling backups
* fix(ai-vault): preserve resume env deletion through drag
* fix(codex): strip inherited Codex homes on mobile real-home resume
The mobile resume surface types a bare real-home codex resume into a
freshly created pane, but never asked for CODEX_HOME/ORCA_CODEX_HOME
deletion at pane spawn, so an agentDefaultEnv-pinned or daemon-inherited
Codex home rerouted the resume away from the user's real ~/.codex while
the same session resumed correctly on desktop. Share the deletion helper
from the AI Vault resume builders and forward it through the mobile
launch and session.tabs.createTerminal call.
* fix(codex): gate session migration on real-home lane
* fix(codex): stop session backfill after opt-out
* fix(codex): keep session heal failures retryable
* fix(codex): keep session migration state recoverable
* fix(codex): retry republished missing session heals
* fix(codex): preserve hook symlink trust path
* fix(codex): disambiguate POSIX trust paths
* fix(codex): align hook trust source paths
* fix(codex): harden trust grant lifecycle
* fix(codex): restore envToDelete on client invocation type after base reconcile
* test(codex): type child.stdout as PassThrough for oversized-output write
* Assemble RC: reconcile app-server transport API across PRs
Unify on the object RPC surface from the index-heal transport (#8921) while
preserving the default-home env strip (#8828) and the narrowed missing-app-server
capability signal (#8847): adapt the user-hook-trust-rebase consumer + tests,
port envToDelete stripping into the shared session, and route stderr
classification through the canonical capability-signal module.
* RC: enable system-default real-home routing by default (flag ON)
Flip codexSystemDefaultRealHomeEnabled to default ON for this RC's staged
rollout (a user can still opt out by setting it false, which stays byte-identical
to managed-home behavior). This is the only intended behavior difference between
the RC branch and the individual PRs. Updates the two tests that assumed the
prior OFF default.
* fix(codex): snapshot hooks.json bytes+parse in one read to close real-home clobber race
The install/sweep/legacy-cleanup paths parsed hooks.json, then did a separate
later read to capture the previous bytes for the pre-write generation guard.
A concurrent save (second Orca instance or the user editing the file) could
land between the parse and that second read and be silently overwritten.
readHooksJsonWithRaw returns the raw bytes and parse from a single read so the
guard compares against exactly what it parsed. Adds a regression test that
mutates hooks.json mid-RPC and asserts the sweep aborts without clobbering.
* fix(codex): sanitize managed account config trust
* fix(codex): guard OAuth add for custom providers
* fix(codex): persist outgoing managed tokens before real-home lane takeover (PR-C)
prepareForCodexLaunch returns null early for the real-home / system-default
lane before syncForCurrentSelection runs. If a managed account is still
recorded as synced when the selection has dropped to the system default
(nulled without a sync pass, or auto-deselect on missing managed auth), a
Codex-refreshed token stranded in the shared runtime home is never persisted
to its canonical per-account home -> token loss.
Read the outgoing managed account's refreshed token back before the real home
takes over. The real-home lane implies host === null, so running the
managed->system-default transition restores only Orca's runtime mirror from
~/.codex and never writes the real ~/.codex. It is a no-op once the selection
has already been reconciled, so the normal select path does not double-write.
* fix(codex): preserve refreshes across all default transitions
* feat(codex): show system-default/real-home account identity in switcher (PR-B)
The account switcher modeled the system-default Codex account as
activeAccountId:null with no identity fields, so the null row rendered
blank ("System default" / generic subtitle) even though its effective
login is whatever ~/.codex/auth.json currently is.
Add a CodexSystemDefaultIdentity descriptor {hasAuth, authKind, email,
providerAccountId, workspaceLabel} to CodexRateLimitAccountsState,
resolved live and READ-ONLY from ~/.codex by the accounts service and
returned from listAccounts()/getSnapshot(). The settings switcher now
renders the null (system-default) row as that real identity: the OAuth
email when signed in, "Custom provider — no usage tracked." for
env-key/custom-provider logins (auth.json with OPENAI_API_KEY, or an
OPENAI_API_KEY env with no auth.json), and the generic fallback when
signed out. Identity is host-scoped (per-distro WSL keeps the generic
label). Orca never writes ~/.codex; managed-account switches only touch
Orca-owned homes, so the system-default identity stays a stable,
displayed source of truth. Usage already routes to the real home via
getSystemCodexHomePath, so the switcher now attributes it to a real face.
Tests (sandboxed temp homes only): OAuth email/provider resolution,
api-key auth.json and env-key (no auth.json) as custom-provider,
signed-out, and select/deselect of a managed account never mutating
~/.codex/auth.json.
* fix(codex): parse multiline provider pins in OAuth guard
* fix(codex): harden managed trust sanitization
* fix(codex): harden system-default identity rendering
* feat(codex): give each managed account a self-contained CODEX_HOME; retire shared mirror (PR-E)
With the real-home flag ON, a host managed account now launches directly
against its own codex-accounts/<id>/home instead of the shared runtime
mirror + auth.json hot-swap:
- codex-home-paths: syncSystemCodexResourcesIntoManagedHome links system
resources into any managed home (ownership-marker discipline; never
symlinks into / mutates ~/.codex).
- runtime-home-service: prepareForCodexLaunch / prepareForRateLimitFetch /
syncForCurrentSelection route the per-account home directly and skip the
shared-home hot-swap + token read-back; each home keeps its own auth in
place (fixes GAP-5 concurrent auth race). Session discovery scans every
per-account home.
- hook-service / hook-trust-promotion: install/getStatus/refresh accept a
runtimeHomePath so hooks + RPC-granted trust land in the per-account home.
- service: config mirror into a self-contained home uses the trust-
preserving merge so granted hook/project trust survives account switches.
- codex-session-root-dedup: rank codex-accounts/<id>/home as canonical
managed alongside the shared runtime home.
Flag-OFF and the system-default real-home (null) lane are unchanged; the
nested-Orca CODEX_HOME===ORCA_CODEX_HOME daemon strip (#5370) is preserved.
Sandboxed tests only; ~/.codex is never mutated.
* fix(codex): validate per-account home ownership
* fix(codex): keep managed rollouts discoverable across real-home opt-out
WI-4 lossless migration/rollback validation for pre-E shared-mirror managed
accounts. Session discovery gated the per-account home scan on the real-home
flag, so opting back out (flag OFF) hid every rollout an account accumulated
while the flag was ON — the data stayed on disk but vanished from the AI Vault
until the flag flipped back on.
Scan a managed host home whenever it holds a sessions/ tree, independent of the
flag; a never-enabled install keeps its homes credential-only so opt-out stays
byte-identical to today. Forward migration was already lossless (the shared
mirror is always scanned) and the opt-out credential read-back already refuses
to overwrite a fresher per-account token; add tests locking all three
invariants. Sandboxed tests only; ~/.codex is never touched.
* fix(codex): migrate stranded shared auth on E takeover
* test(e2e): isolate Electron from developer Codex home
* test(codex): add real-account validation harness
* fix(codex): finish C and E matcher composition
* fix(codex): bound validation harness shutdown
* test(codex): isolate hook lifecycle user data
* test(codex): cover realistic account-home migration
* fix(codex): keep standalone home tripwire active
* test(codex): fingerprint system auth in validation reports
* fix(codex): bind managed homes to account ownership
* fix(codex): normalize Windows trust source identity
* fix(codex): make Windows trust upgrade transactional
* test(codex): use TypeScript pipeline for validation scripts
* test(codex): run validation modules through native node
* test(codex): allow slow Windows tripwire startup
* fix(codex): survive lingering Windows codex login processes in add-account
On Windows, codex login can keep running (with descendants) after it has
written auth.json, holding OS handles on the per-account managed home
(log/codex-login.log). That made doAddAccount's post-login cleanup fail
with ENOTEMPTY (rmSync) and left an orphaned codex-accounts/<id>/home.
- runCodexLogin now watches for auth.json on Windows and force-kills the
login process tree (taskkill /t) if it lingers past a short grace
period; the forced exit is treated as a successful login. The 120s
timeout path also kills the whole tree instead of only the direct
child. macOS/Linux behavior is unchanged.
- safeRemoveManagedHome now removes homes with rmSync maxRetries /
retryDelay (mirroring the local-worktree-filesystem Windows policy)
and no longer lets a cleanup failure mask the original add error.
- run-codex-real-account-validation.mjs accepts --temp-parent /
ORCA_CODEX_VALIDATION_TEMP_PARENT so the disposable root can live
outside %USERPROFILE% on Windows, and fails with an actionable message
before creating anything when the temp parent is inside the primary
home. The real-home guard is unchanged.
* fix(codex): preserve managed-account MCP .credentials.json on per-account-home migration (#8440)
Codex file-mode MCP OAuth tokens live in $CODEX_HOME/.credentials.json,
keyed by MCP server URL with no account identity of their own. The legacy
shared-mirror -> per-account-home migration only carried auth.json, so an
existing managed account with authed MCP servers had its tokens stranded on
upgrade and silently needed re-auth.
Carry the shared mirror's .credentials.json into the same identity-proven
per-account home alongside auth.json: only into the single uniquely-matched
active account (no cross-account leak), only when the destination has none yet
(never clobber a newer file the account authed in its own home), atomic 0600,
absent-source no-op. New MCP auth already lands in the per-account home since
that home is CODEX_HOME.
* fix(codex): preserve Windows reauthentication login flow
* test(codex): build real-account validation harness cross-platform on Windows
The harness built its app with execFileSync('npx', ['electron-vite', ...]),
but npx resolves to a .cmd shim on Windows that execFileSync cannot launch
(ENOENT), so the harness could not build its own app there and required
--skip-build with a prebuilt out/main/index.js.
Extract resolveElectronViteBuildCommand(repoRoot): it runs the repository-local
electron-vite JS entry (node_modules/electron-vite/bin/electron-vite.js) with
the current Node binary (process.execPath), which resolves identically on
macOS, Linux, and Windows with no shell. It throws a clear error if the local
entry is missing (install deps or pass --skip-build). --skip-build behavior is
unchanged.
Add regression coverage asserting the build command uses process.execPath and
the repo-local JS entry (not npx), and that a missing entry fails clearly.
* fix(codex): version the MCP creds migration independently of the auth marker
The auth carry and the MCP .credentials.json carry (#8440) shared one
existence-only v1 marker, so any build that stamped the auth-only marker
first would strand the MCP store forever. The MCP carry now concludes via
its own per-account-mcp-creds-migration-v1.json marker and runs even when
the auth marker is already present; ordering is code-enforced instead of
landing-discipline-enforced.
Also isolate per-account read failures: one stale or deleted account home
no longer aborts the whole migration. The broken account stays in the
unique-identity ambiguity gate via its stored fields but is never read or
written, so the active account still migrates.
* fix(codex): fail corrupt managed auth.json without echoing credential bytes
A raw JSON.parse SyntaxError from loadOAuthCredentials could carry auth
file fragments into logs and the add/reauth error surface. Throw a
sanitized error instead; filesystem errors still propagate unchanged.
* fix(mobile): give the pairing runtime a disposable home for the E2E boot guard
The main-process guard now refuses to start with ORCA_E2E_USER_DATA_DIR
set but the real user home, and this was the one caller not updated —
the temporary pairing runtime crashed before emitting its pairing URL.
* test(codex): canonicalize harness containment guards and retry cleanup
Resolve symlinks before the disposable-root containment checks so a
symlinked temp parent cannot smuggle the throwaway home inside the
primary home, and give the final cleanup rm Windows retry/force so a
briefly lingering codex handle cannot strand the credential-bearing
root.
* test(codex): add lane-aware containment mode to the real-account harness
The Windows gate-D run proved strict zero-event whole-profile containment
is structurally unreachable with the real-home flag ON: system-default
spawn sites deliberately delete CODEX_HOME so native codex resolves the
real ~/.codex, and on Windows the binary ignores the USERPROFILE sandbox.
Its own volatile runtime churn (root sqlite/WAL/SHM, tmp/, log/) is the
shipped Phase-1 design, not a candidate defect.
--lane-aware-containment records those designed events without aborting
while every other real-home write — auth.json, config.toml,
.credentials.json, hooks.json, sessions/, anything unknown — remains a
hard violation and still aborts the run. Default behavior is unchanged
(strict); the absolute zero-event claim stays carried by macOS runs,
where HOME does sandbox native codex.
* test(codex): allow the real-account harness to pin the real-home flag off
--system-default-real-home off seeds and env-pins the flag OFF so every
codex spawn gets an explicit managed CODEX_HOME and native codex never
resolves the OS profile. This is the only Windows configuration where the
strict zero-event whole-profile tripwire is reachable, and it matches the
stable-rollout default; flag-ON runs keep lane-aware classification.
* test(codex): correct the flag-off harness comment to kill-switch rationale
The rollout ships all codex-home changes at once (no phased rollout), so
flag OFF is the emergency kill-switch lane, not the stable default.
* test(e2e): canonicalize the isolated E2E home path
The disposable HOME lives under os.tmpdir(), whose spelling is an alias
on CI (macOS /var symlink, Windows 8.3 RUNNER~1). Git canonicalizes
worktree paths, so worktrees created under the aliased home never
matched the app's listing — golden core flows and the packaged
crash-survival harness failed with 'worktree created but not found in
listing'. Resolve the home to its canonical spelling at creation in
both the e2e helper and the packaged-app driver.
* fix(codex): address CodeRabbit review on the landing PR
- carry envToDelete through the mobile agent-resume startup plan so a
real-home Codex resume cannot inherit an ambient CODEX_HOME
- strip Orca-owned Codex overrides in the commit-message WSL fallback,
matching the host fallback
- strip ELECTRON_RUN_AS_NODE in the computer-e2e driver like every other
home-isolation caller
- drop the unused hooksEnabled parameter from isRealHomeCodexHookLaneUsable
* feat(codex): ship real-home routing unconditionally, remove the rollout flag
The codexSystemDefaultRealHomeEnabled setting is gone from types and
constants and the helper no longer consults settings — the system-default
real-home lane and per-account homes ship for everyone in one release.
This also un-strands profiles that rc-era builds stamped with false (the
setting had no UI, so every stored false was a seeded artifact that would
have silently kept those users on the legacy mirror forever).
The ORCA_CODEX_SYSTEM_DEFAULT_REAL_HOME env override survives strictly as
a test-rig control: the containment harness pins the legacy lane for
strict zero-event Windows runs, e2e home isolation pins lanes inside
disposable homes, and the legacy-lane test suites now route their
per-test lane selection through it.
---------
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
Collapse multi-line explanatory comment blocks into single-line "why" statements
per AGENTS.md ("Document the Why, Briefly"): drop restatements of the code and
mechanism narration; keep the non-obvious reason, external refs, and directives.
Comments-only — verified no code changed via a Babel/esbuild comment-strip
token-equality gate against origin/main; typecheck and oxlint clean.
Area: shared, cli, relay, preload. 26 files changed, 1039 insertions(+), 3300 deletions(-).
Co-authored-by: Orca <help@stably.ai>
* perf(renderer): stop full durable-state save on every top-level view switch (#9002)
Persist activeView in a tiny profile-scoped sidecar instead of mutating the monolithic recovery snapshot. Active-view-only updates now bypass the broad UI normalization and durable save scheduler, while a 100ms atomic writer coalesces rapid switches and a synchronous shutdown checkpoint closes the immediate-exit race. Legacy state remains a migration and downgrade fallback.
Coordinate renderer shutdown capture through one guarded checkpoint so workspace sessions and the active-view preference both survive graceful reloads, restarts, and quit cancellation.
Add a persistence-boundary test proving the sidecar stays below 64 bytes while orca-data.json remains byte-for-byte unchanged, plus repeated Windows Electron restart coverage and a path-normalization-safe restart fixture.
* harden active-view sidecar: prototype-safe validator, race-free async swap, independent shutdown flush
- isTopLevelView uses Object.hasOwn so a corrupt sidecar can't smuggle
inherited keys (constructor/__proto__) through as a valid view.
- writeAsync guards the generation check and rename synchronously (renameSync)
so a shutdown flushOrThrow can no longer interleave and let a stale async
rename clobber the freshly-written view.
- shutdown checkpoint flushes the durable store and the active-view sidecar in
independent try/catch blocks so one store's failure can't skip the other.
Added regression tests for all three.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* fix(ssh): clear stamped agent status on disconnect
Batch transient cleanup by accepted SSH connection authority and use a monotonic cutoff so reconnect replay wins over delayed clears. Preserve pane launch, resume, acknowledgement, and retention metadata.
Caveat: legacy or renderer-owned rows without an accepted connection stamp are intentionally left to existing pane/PTY teardown; clearing them by host would be ambiguous.
* docs(ssh): explain stale status watermark
* fix(ssh): preserve status ordering after restart
* Add native chat skill and command picker with host-aware discovery
Adds a unified, keyboard-first skill and command picker to native chat that:
- Uses agent-native invocation syntax (slash for Claude/OpenClaude/Grok, dollar for Codex)
- Discovers skills only on the pane's execution host (local, WSL, SSH-unavailable, or runtime)
- Groups or separates commands and skills per agent configuration
- Deduplicates by canonical path but preserves visibility through all contributing roots
- Handles IME composition, loading states, and errors without claiming PTY-level control
- Records picker telemetry (open, item accepted, send classification, discovery outcomes)
- Extends shared agent profiles to define per-agent skill grammars and source ownership
* Remove obsolete reference and design documentation
Clean up stale design specs, implementation plans, and investigation notes from
docs/reference/. These documents predate the current implementation and are no
longer actively maintained or referenced by the codebase.
* Extract shared skill discovery utilities and add skill invocation envelo
- Move skill comparison and source classification to shared module for native/WSL reuse
- Extract display text sanitization to prevent control/zero-width character spoofing
- Add native-chat command envelope parser and surfacer for skill invocations
- Extend discovery timeout backstop to account for WSL metadata read sequence
* Localize skill picker UI for Spanish, Japanese, Korean, Chinese
Translate skill picker UI strings including commands, skills, loading
states, error messages, and scope labels for the new skill picker feature
across four language locales.
* Fix skill picker bugs and improve code robustness
- Fix i18n plural handling: rename `count` to `sourceCount` to prevent unintended plural-key resolution in localized strings
- Fix skill discovery array mutations: copy `root.providers` to prevent bugs during dedup merge
- Fix image attachments being silently dropped when message text starts with /skill or agent prefix
- Extract `quoteBashString` utility for WSL command code reuse across builders
- Add line-separator safety characters (0x2028/0x2029) to skill display filter
- Remove stale doc reference links and clarify inline comments
* Add reference docs for git compatibility and headless Linux server setup
Track previously untracked operational guides in `docs/reference/` that
explain Git binary compatibility requirements across host types and how to
run `orca serve` on headless Linux. Update AGENTS.md and README.md to link
to these references.
* Restructure mobile pairing setup into a clear stepped layout
* Rework mobile pairing UI: radio-style path selector with sign-in gate fo
* fix(mobile): resolve mobile-pairing review findings
Applies the code-review findings for the reworked pairing UI:
- Clear a displayed Relay QR on sign-out in both MobilePage and MobilePane
(the rework dropped the wasSignedInRef watcher, leaving a stale Relay QR
next to a "sign in required" prompt). Anywhere stays selected; the QR
re-mints as local-only. [#1, #3]
- Extract useMobilePairingConnectionMode so both panes resolve the saved
preference identically instead of duplicating the state + resync effect. [#6]
- Delete ~24 i18n keys the rework left unreferenced; sync locale catalogs
to parity. [#4]
- Give the connection-path radiogroup roving tabindex + arrow-key nav and
document why it diverges from SettingsSegmentedControl. [#7]
- Add MobilePane.test.tsx (previously untested safety logic) and a
MobilePage saved-local-only restore test. [#2, #5]
Verified: 22 targeted tests pass, typecheck + oxlint clean, localization
catalog/coverage and max-lines ratchet green.
* fix(mobile): resolve adversarial-review findings for pairing UI
Address accepted findings from the mobile pairing UI rework:
- MobilePane: add a request-generation epoch so a late getPairingQR
response can't paint a stale Relay QR after sign-out, a mode switch,
or an address change; arm rotation when discarding a pending mint.
- MobilePage: on sign-in, upgrade a signed-out local-only fallback QR to
Relay (invalidate + rotate-regenerate); handle null->connected too.
- Extract useMobilePairingQrInvalidation so both sign-in/out edges and
cross-window preference syncs invalidate/re-mint the QR consistently.
- MobilePane: clear + rotate the QR when the selected address changes
(manual pick or refresh-driven) so it can't encode the old endpoint.
- MobilePairingConnectionOptions: guard the Sign in CTA on configured;
show an Unavailable panel on unconfigured builds instead of dead CTA.
- Drop the duplicate sign-in helper from MobilePairingSetupSection.
- Remove dead i18n keys (title, recommended, signInToGenerate) and add
relayUnavailable across all locale catalogs.
- Add tests: deferred sign-out/mode-switch races, sign-in upgrade,
cross-window sync, unconfigured build, arrow-key radiogroup.
TODO left for the relay-label-honesty finding: getPairingQR does not
expose the actually-encoded mode when an automatic offer degrades to
local-only, so the mismatch can't be surfaced without a new return field.
* fix(mobile): resolve relay-pairing deep-review regressions
- MobilePane: invalidatePairing now clears loading so a superseded
mid-flight generate can't wedge Generate disabled forever
- MobilePage: stop auto-minting a local-only QR under the Relay label
when signed out with Anywhere; gate Step 2 auto-generate and the
Generate button on a shared canMintMobilePairingOffer helper, align
with Settings, clear QR + loading on sign-out, mint Relay on sign-in
- qr-invalidation: clear pairQrDataUrl (and loading) on every
invalidation path so a stale QR can't stay scannable during rotation
- Strengthen MobilePane/MobilePage tests for the aligned behavior and
stuck-loading coverage
- Translate relayUnavailable in es/ja/ko/zh
Co-authored-by: Orca <help@stably.ai>
* fix(mobile): refuse to mint QR when signed-out with Anywhere selected
Replace silent degradation of Anywhere mode to local-only QR with explicit
refusal. Add canMintMobilePairingOffer guard across all mint paths (address
change, network invalidation, connection mode switch). This ensures the UI
honestly encodes the selected pairing path. Improve keyboard focus visibility
on the path selector by adding a persistent focus ring.
* fix(mobile): detect and flag Relay provisioning degradation
When Relay provisioning fails during automatic (Anywhere) pairing offer creation, the offer silently degrades to local-only. This confuses users who selected Anywhere expecting cellular capability.
Add connectionMode field to the pairing offer result to expose what the offer actually encodes. The UI now flags degradation when the offered mode mismatches the user's selection.
Also move credential rotation logic to the main process: rotate when requested mode differs from the pending token's encoded mode. This ensures QR codes displayed under an old policy can't pair under a new one, and windows reminting after preference sync converge on one token.
Rename MobileRelayBetaAvailability → MobileRelayBetaNotice.
* test(mobile): verify pairing codes don't flash during Relay mint
Assert that the pairing QR and URL remain hidden while the Relay mint is
pending, preventing confusing intermediate states when signing in unlocks
Relay.
---------
Co-authored-by: Orca <help@stably.ai>
* feat(ssh): download folders from remote explorer
* fix(ssh): harden remote folder downloads
* Add missing getRepo stub to worktree cwd test mock
Restoring headless mobile tabs looks up the repo for the active
worktree id; the mock lacked getRepo, so the test only passed
incidentally. Add it explicitly and return undefined since wt-1
is a worktree id, not a registered repo.
* Enable SSH folder downloads, gated for system-SSH connections
- Folder downloads require SFTP, unavailable on system SSH (which
offers only raw file operations). Add supportsFolderDownload flag
to gate the feature in the UI layer.
- Reject symlinks at directory-entry level, preventing tree escapes
and eliminating unnecessary stat calls.
- Check abort signal before opening dialog for better responsiveness
when renderer closes.
- Log cleanup errors without re-throwing to preserve underlying
transfer failures.
* Gate SSH folder downloads to SFTP-capable connections
Enforce fail-closed gating and add Windows path traversal validation to
ensure downloads are only available when explicitly supported and safe.
* Gate SSH folder downloads to SFTP-capable connections
Enforce fail-closed gating and add Windows path traversal validation to
ensure downloads are only available when explicitly supported and safe.
* fix(ssh): keep provider types under max-lines after main merge
Move FolderDownloadOptions next to the SFTP download implementation and
narrow IFilesystemProvider.downloadFolder options to AbortSignal only so
types.ts stays within the 300-line oxlint budget when merged with main.
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* feat(terminal): add flag-gated render-desync sentinel for WebGL panes
Detects the buffer-clean/render-stale glyph garble class in the field: per
visible WebGL pane, compare the cells the xterm buffer says hold glyphs
against the ink actually present on the canvas, sampled in the same task as
a forced synchronous redraw so a divergence proves the render model/atlas is
wrong rather than a missed present. A trip requires the same screen cells to
stay divergent across three samples (real desync is pinned; scroll lag moves),
then records a webgl-render-desync breadcrumb, stashes evidence (canvas PNG +
buffer text) for bug reports, and runs the same shared-atlas recovery a tab
reveal performs, so a stuck-garbled pane self-heals within seconds.
Off by default; arm on any build via
localStorage.setItem('orca:render-desync-sentinel', '1') and reload.
* fix(terminal): invalidate glyph cache on atlas replacement
Reproduce the WebGL atlas identity mismatch with two live terminals and force cached geometry to rebuild whenever a different shared atlas is attached. Persist flag-gated render-desync evidence and retain the investigation tooling used to validate the field signature.
* fix(terminal): harden render desync diagnostics
* docs(reliability): clarify Linux WebGL evidence gap
* Replace assistant-prose heuristic with explicit turn lifecycle markers
Extract provider-authored turn boundaries (completion, interruption) directly
from Claude/Codex transcripts so the chat view knows when work ends without
guessing from message presence. Reconciles live hook state with transcript
lifecycle: when a terminal boundary lands, it settles a dropped Stop hook
instead of letting prose mislead the UI into showing 'working' after done.
* fix(review): cover Claude terminal stop_reasons and RPC lifecycle frames
Treat max_tokens/stop_sequence/refusal as completed markers so capable hosts
do not stay working after a dropped Stop, and assert lifecycle payloads on
runtime subscribe/read frames plus mid-turn non-terminal stop_reason cases.
* test(native-chat): clarify that lifecycle field is optional
Add type assertion and comment documenting that lifecycle field is optional and can be omitted in truncation-gating test fixtures.
* fix(native-chat): settle status on interruption despite working subagent
When Claude's turn is explicitly interrupted, the session should show ready
immediately — even if background subagents are still running. Add an
interruption check before consulting the hook's working-subagents flag so
interruptions take precedence. Also normalize omitted lifecycle timestamps
to null instead of leaving them undefined, and add test coverage for both
cases.
* fix(native-chat): settle loading spinner on explicit turn boundaries
Explicit transcript turn-lifecycle markers now fully replace the prose-fallback
settlement path. Remove the now-unused `turnLifecycleCapable` flag and wire
lifecycle to suppress spinner even when hook status lingers. Refine Claude
lifecycle detection to distinguish terminal stops from mid-turn tool_use rows,
exclude harness noise from new-generation detection, and apply clock-skew slack
over SSH/relay. Serialize PTY sends per line to prevent rapid prompts from gluing
before Enter, clearing unsubmitted input on cancel. Update working suppression to
detect epoch rollovers so interrupt+next-turn without a ready gap resets the
spinner correctly.
* fix(agent-status): clear answered Claude question waits at answer time
An answered AskUserQuestion left the amber "waiting" indicator on sidebar
rows and tabs until the agent's next tool hook or turn end — unbounded
linger while the model thinks or streams after the answer (measured 17s
for a 1000-word reply, 44s for 3000 words).
Root cause is an event-shape change: newer Claude reports the
AskUserQuestion wait as PermissionRequest (not the PreToolUse shape #7852
special-cased), so the wait inherited real-permission stickiness and
shouldKeepClaudePermissionVisible swallowed the answer-time
PostToolUse(AskUserQuestion) working event — the identity match can never
succeed because the question's PermissionRequest carries no inheritable
tool_use_id. That silently undid #8311 for questions.
Two scoped changes, both keyed on the tool name rather than the hook
event name:
- Sticky permission hold now exempts AskUserQuestion waits, so the real
answer-time hook (when Claude sends one) clears the wait as #8311
intended.
- New guarded inference for the hook Claude may never send: the submit
keystroke (Enter or digit quick-select) into a pane whose fresh status
is a waiting AskUserQuestion synthesizes the post-answer state, exactly
mirroring the existing interrupt inference (renderer baseline capture,
main-process re-validation, listener lead-state sync so child-driven
refreshes cannot resurrect the dismissed question).
Real permission waits (other tools) keep their sticky semantics; batched
input and pastes never match the submit classifier.
Verified live against a real claude CLI: waiting -> working within ~50ms
of both Enter and digit answers, question card dropped, unanswered
questions still hold amber, permission stickiness covered by tests.
* fix(agent-status): guard question answer inference
Keep multi-question, multi-select, and free-text selector interactions waiting until the full prompt is submitted. Wire native-chat answers into the same guarded inference only after every paced runtime write succeeds, with cancellation and delivery-failure coverage.
* chore(skills): refresh manifest for rc.2
* fix(agent-status): verify native chat answer delivery
* fix(agent-status): await verified question delivery
* fix(agent-status): pin native-chat answer baseline before delivery
The native-chat question-answered inference read the live pane status at
settle time (after the paced send + remote acceptance, which can span
seconds on SSH). If a replacement AskUserQuestion became current in that
window, the settle callback minted a fresh baseline from the new question
and the server cleared *its* wait — dismissing a question the user never
answered.
Capture the answered question's baseline before delivery and have the
inference getter return it, so the server re-validates against the pinned
baseline and rejects a changed status — the same capture-then-revalidate
contract the terminal keystroke path already uses. Also hoist the
shouldStepNativeChatAskAnswer predicate to a single evaluation.
Regression test swaps the live status between sendAnswer and settle and
asserts the answered question's baseline is used (fails against the prior
live-read getter).
* Add safe skill freshness detection
* Accept observed copy-mode rail outcomes
* chore(skills): regenerate snapshot artifacts for the merged guide content
The rebase onto main picked up the reviewed guide fixes (#8624), so the
current manifest hashes and a new appended snapshot generation must
match those bytes; the registry keeps all prior snapshots so existing
installs classify as outdated rather than unrecognized.
* fix(skills): canonicalize snapshot file order and guard released history
Historical snapshots kept git ls-tree byte-order while the working-tree
walk and runtime observation use the sorted depth-first order, so any
future multi-file skill would misclassify older installs as unrecognized
and churn spurious registry revisions; all producers now share one
canonical order (no digest changes for today's single-file packages).
Also rejects executable files from shipped skills (Windows observation
cannot see execute bits, which would misclassify pristine Windows
installs) and adds an explicit append-only invariant for released
snapshots so a generation-logic change cannot rewrite them silently.
* fix(skills): throttle focus rescans and correct self-blocked placement copy
Every window focus re-read and re-hashed all installed packages, and the
nudge and panel each forced their own trailing rescan for one event; a
15s cooldown plus a shared invalidation latch keep one bounded scan per
event while install-change events stay immediate. Bundle artifacts are
now loaded once per run instead of re-parsed on every scan. A read-only
or otherwise unsupported outdated placement now explains that it blocks
itself instead of blaming a phantom sibling placement; the supported
topology set moved to shared so eligibility and copy cannot drift.
* feat(skills): move freshness surfacing to a lingering toast and update modal
The Skills page has been unreachable since its toolbox menu entry was
removed (#4535), so surfacing freshness there buried the feature behind
its own nudge. The nudge now lingers until acted on (ignoring it records
nothing; only the explicit close persists dismissal keys) and opens an
update modal hosting the pre-filled editable terminal, an honest
current/blocked summary, and the per-placement rows in a collapsed
Details section. A compact 'Check for skill updates' row in CLI settings
is the manual re-entry point. Skills page restored to main; design-doc
surfacing section records the venue decision.
* fix(skills): mount update dialog inside the link-routing provider and fold freshness into the setup rails
The dialog hosts a live terminal pane that requires the link-routing
preference context; mounted outside the provider it crashed the renderer
the moment an eligible update existed (caught by live QA — unit tests
mock the terminal). It now mounts inside the provider behind its own
recoverable boundary.
The separate 'Check for skill updates' settings button is gone: the
setup rails' own pill now carries freshness (Update available / Up to
date, falling back to Installed for blocked or unrecognized copies and
for non-local runtimes the local-only scan cannot vouch for), and
Re-check refreshes both installation detection and the freshness
inventory. Wired for the CLI, Orchestration, Computer Use, and
Per-Workspace Environments rails.
* fix(skills): use the sleek scrollbar style in the update dialog
* chore(skills): regenerate manifest for merged main (v1.4.142-rc.1)
Main advanced to 1.4.142-rc.1 with a v1.4.141 release, so the embedded
appVersion and release mapping were stale on the PR's merged tree. Only
appVersion and the new release entry change; no snapshot digests move
(released history preserved).
* fix(skills): bound and batch freshness work
* fix(skills): harden freshness integrity checks
* fix(skills): accept observed copy topology outcomes
* chore(skills): regenerate manifest for current main
* fix(skills): preserve update terminal lifecycle
* chore(skills): regenerate manifest for current main
* fix(skills): fail closed on stale freshness scans
* chore(skills): regenerate manifest for current main
* fix(skills): preserve freshness safety under focus churn
* feat(skills): group the update modal by skill with plain-language status
The Update skills modal now lists only skills that will update or that can't
(with why), grouped by skill with their install locations nested underneath —
no more one row per placement.
- Statuses collapse to "Update available" / "Can't update" at the skill level.
- A location's problem is a chip (Duplicate, Unrecognized, Inaccessible, Read
only, In a repo, External/Broken link, Plugin cache) with a hover tooltip
that explains what it means for the user and what to do.
- Up-to-date, unrecognized-only, and unreadable-only skills are hidden; a
current/unrecognized/etc. location only appears when it explains a shown skill.
- Copy is de-jargoned (drops "copy"/"placement"/"snapshot"/"official copy") and
names the mechanism as the npx skills update command, not "Orca's update".
- Rename the section to "Update details"; drop the unreachable newer-known state.
Renderer-only: derivation is a pure module (groupSkillFreshness) with unit tests;
no IPC or main-process change. Locales updated for all five languages.
* chore(skills): regenerate manifest for current main (v1.4.143-rc.0)
* feat(skills): don't let a duplicate block the update; clearer skipped copy
- Eligibility: a clean standalone duplicate no longer poisons the whole
name — the canonical copy still updates and the duplicate is flagged;
a duplicate-only skill stays unoffered.
- Update modal: "Can't update" -> "Skipped" with a reason-specific
sentence (edited/read-only/in-a-repo/plugin/link); chips describe only
the location state; footer "Check now" -> "Re-check".
- Settings sidebar nav pills go amber "Update available" when a skill is
updatable, matching the setup cards.
- Localized new strings across en/es/ja/ko/zh.
* chore(skills): regenerate manifest for merged main (v1.4.144-rc.1)
* feat(native-chat): add native chat view across mobile
* fix(native-chat): address review findings and CodeRabbit threads
Correctness:
- Restore an independent initial readSession seed and surface initial-drain
errors as snapshot frames so the chat view can never strand on 'loading'
- Pair mobile tool results to calls by ordinal FIFO (parallel calls no longer
misgraft results); clear a pending ask only when its own call resolves
- Show a new streaming reply immediately (same-turn suppression, not length)
- Delegate mobile noise filtering to the shared harness-injected classifier
- Admit soft-leaving mobile clients in beginMobileInputFloor (parity with
mobileTookFloor) so grace-window writes aren't dropped
- Self-heal a stale 'working' status once this turn's reply lands
- Catch RPC rejections in mobile file-open helpers; guard sanitizeToolInput
key collisions; settle web/runtime transports on unrecognized first frames
and forward snapshot errors
Perf:
- Throttle the mobile streaming bubble (50ms) so per-part status frames stop
re-parsing the whole accumulated markdown
- Short-circuit markdown path detection on dot-less or oversized runs
(quadratic backtracking guard)
UX/minor:
- Wire hold-mode dictation through the native chat composer
- Allow scoped-package (@) paths in file-path detection
- Move caret after mid-text autocomplete insertion; index-prefixed ask option
keys; single scroll-to-end effect; bounded wait + toast when image attach
races a resubscribe; count-based pending reconciliation; cache-hit search
cancels stale debounce; chat-tab toggle wins over in-flight preference load
- Share shouldStepNativeChatAskAnswer between desktop and mobile; import
block guards/source priority from shared instead of local copies
- Defensive non-positive transcript limits; test strengthening (TTL expiry,
post-unsubscribe stale frame, lease readiness, filtered console.error)
* refactor(native-chat): share desktop/mobile chat logic in src/shared
Extract the parity-mirrored native-chat modules into shared implementations
both surfaces re-export: ask parsing (registry, parseAskFromStatus,
extractPendingAsk, formatAskAnswer), answer stepping offsets/scheduler, diff
detection/parsing, harness-noise filtering, tool fold/pair/split, and tool
summaries. Removes the hand-synced copies and their stale Metro comments.
Divergence reconciliations take the safer side of each: diffs truncate at
120 lines/32KB everywhere (desktop previously unbounded), tool-run summaries
cap at 3 parts with bounded-depth previews, nameless tool calls are skipped,
and basenames split on both separators.
Also: settle and kill every sibling quick-open pass when one reaches
maxResults (main rg/git and relay git; relay rg already did) so a capped
search cannot leave a scan walking a huge tree; fold window-bounding into
the shared merger's applyAppend; localize the web 'Pair a host' snapshot
error.
* fix(native-chat): address CodeRabbit follow-ups on shared modules
- Attachment lease gate re-checks connection/target/tab after the bounded
wait, so a tab/host switch or disconnect mid-wait can't send into a stale
terminal; a moved-away target drops silently like the pre-wait guard and
only an unrecovered lease surfaces the toast. Adds hook tests.
- extractPendingAsk parses transcript tool-calls through the same
registered-parser + canonical-shape fallback as live status, so a custom
question tool that rendered live survives reconnect/replay.
- Direct unit tests for the shared ask parser (FIFO ordering, fallback,
malformed payloads) and tool-summary bounded preview (depth/collection
caps, circular refs, basename/command branches).
* fix(native-chat): treat initialLimit 0 as a valid empty window
Both engine guards used truthiness, so an explicit zero limit skipped the
bounded tail reader and fell back to an unbounded incremental read. Latent
only (every caller clamps positive), hardened for consistency with the
tail reader's non-positive-limit handling.
* fix(mobile): native-chat composer lock UX + send-failure feedback
- Distinguish input-lock reasons: transport 'disconnected' shows Reconnecting…
instead of mislabeling a reconnect as locked-by-another-client
- Guard the composer lock behind a 600ms hold so connState blips / lease
hand-offs don't flicker the placeholder; unlock stays instant
- Surface a rejected send inline above the composer (a bottom toast hides
behind the keyboard); auto-dismisses after 4s
- waiting-session hint invites the first message instead of implying the
agent is still starting
* test(mobile): sync answer-send pacing test to the 500ms advance buffer
Missed in merge 8fe3c391c, which carried main's NATIVE_CHAT_ADVANCE_BUFFER_MS
300->500 (#8568) into the shared stepping module that mobile derives from.
* fix(mobile): restore terminal stream after chat cold start
* fix(native-chat): harden retries, optimistic sends, and file scans
* fix(mobile): deliver AskUserQuestion answers by option number (STA-1860)
Port #8840's fix to the mobile native chat: the Ask card now tracks
per-question option INDICES (+ free text) and the answer-send hook drives
Claude's arrow-navigate selector with buildAskAnswerKeys keystroke groups —
option numbers, next-tab arrows, Enter — paced one selector step apart, instead
of pasting label text that the selector ignores (which silently committed the
default option). Non-Claude agents keep the pasted-label path via the
selection-based formatAskAnswer.
Backcompat: keystrokes are built client-side and written through the EXISTING
terminal.send passthrough with enter:false — the same contract the permission
card already uses — so an older desktop runtime (SSH/relay included) replays
them verbatim; no RPC/contract change in either update order. Free text is
newline-sanitized because terminal.send has no paste framing.
Drops the now-unused formatCompleteAskAnswer from the shared module.
* fix native chat send and runtime races
* fix mobile native chat formatting
* fix(native-chat): mobile empty state matches desktop copy
Mobile showed a single generic line ('Send a message to get started') where
desktop shows a titled two-line empty state naming the agent ('Start a chat with
Claude' + 'Ask Claude to inspect code, explain output, or make a change.'). Align
them from one source of truth so they can't drift again:
- Extract the agent-type label map + formatAgentTypeLabel to
src/shared/agent-type-label.ts (desktop re-exports; mobile imports).
- Add src/shared/native-chat-empty-state.ts with the canonical English copy;
desktop uses it as its i18n fallbacks (localization unchanged — en/es/ja/ko/zh
keys still win), mobile substitutes the agent label and renders it directly
(mobile ships English only).
- Mobile: render title + subtitle for waiting-session AND ready-but-empty (both
are 'start a chat'), error copy for errors; keep the loading spinner.
Live-verified on the iOS sim against a pn-dev of this branch. typecheck node/web
+ mobile tsc clean; 30 mobile + 428 desktop/shared native-chat tests green.
* style: oxfmt the empty-state parity test (line wrap)
---------
Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* Add native macOS menu bar status item with settings toggle
Extend the Windows-only system tray into a shared status-item module
so macOS gets a template menu bar icon (Open/Settings/Check for
Updates/Quit), a theme-aware attention dot, and a "Show Menu Bar
Icon" appearance setting. Also fixes a startup race where a tray
"Settings…" click could fire before the renderer's ui:openSettings
listener attached, by queuing a one-shot pending-open-settings intent
the renderer consumes on mount.
* Fix Retina blur, race conditions, and menu-label duplication in tray Set
- Rebuild the tray attention icon's @2x representation since toBitmap only
read 1x pixels, blurring the glyph on Retina displays
- Fix premultiplied-alpha math so light-glyph tinting uses per-pixel alpha
instead of a flat 0xff, keeping antialiased edges valid
- Always push ui:openSettings and leave a longer-lived pending flag, since
there was no reliable signal that a renderer's listener was attached
- Preserve tray attention state across macOS menu-bar hide/show toggles
instead of resetting it on tray destroy
- Route macOS tray creation through syncMacMenuBarIcon so startup and the
live toggle share one visibility policy
- Reuse app-menu translation keys for tray Settings/Check for Updates and
drop the now-duplicate tray-scoped locale strings
* Make menu bar icon default on and platform-independent in settings
Previously showMenuBarIcon was hardcoded to darwin-only in both the
default settings and the sanitize/load paths, so a profile written on
macOS lost its opt-out when touched from another OS. Store the raw
boolean everywhere and let darwin-specific consumers decide whether
to act on it, so the value round-trips unchanged across platforms.
* Fix Settings menu click being silently dropped after a slow cold rendere
Replace the 60s pendingOpenSettings TTL with an untimed intent, since a
cold renderer start can outrun any fixed timeout and cause the flag to
expire before the click is consumed. webContents-id scoping plus
consume-on-read still prevent the intent from leaking to an unrelated
renderer. Adds a test covering the queued-before-mount pull path.
Adds self-hosted Jira Server/Data Center support (personal access token or classic username + password) alongside Atlassian Cloud, fully addressing the older-instance ask in #6676. Takeover of #7724 (@wquintal's original PAT implementation), brought current with main and hardened via a multi-agent adversarial review.
Fixes#6676.
Co-authored-by: William Quintal <williamquintal95@gmail.com>
* Route new-tab link clicks to Orca tabs instead of popup windows
- Adds an isolated-world click/auxclick listener that relabels
browser-native new-tab intents (target=_blank, cmd/ctrl-click,
middle-click) with a private frame name, so setWindowOpenHandler can
distinguish them from opener-dependent window.open() calls without
breaking OAuth popups.
- Wires matched clicks through to a new browser:open-link-in-orca-tab
IPC payload carrying foreground/background activation intent, so the
renderer opens a worktree tab instead of a native window.
- Adds unit and e2e coverage for modifier/middle-click routing,
cancellation/rewrite handling, and cleanup on guest teardown.
* Route new-tab link clicks to Orca tabs and stop background popups
Plain target=_blank clicks (main frame and iframes) now navigate the
current Orca tab in place instead of opening a new browsing context,
while explicit new-tab gestures (Cmd/Ctrl-click, middle-click,
Shift+modifier) route into Orca tabs via one-use private frame names,
including child frames. Drops the foreground/background frame-name
split and the activate flag now that all routed links always open
active — no more silent background popups from a plain link click.
* fix(mobile): background-mount never-mounted terminals on subscribe so they don't render blank (STA-1840)
A mobile terminal.subscribe to a tab the desktop never mounted this session —
a workspace the desktop isn't currently showing, a cold-activation-deferred tab
(#8597), or a cold-parked tab — has no attached PTY, so the runtime has no
headless emulator and the initial snapshot is empty. The mobile terminal then
renders blank (repro: on the phone, scroll to an old workspace whose terminals
the desktop unmounted; they stay blank).
When a mobile subscribe hits an empty snapshot (or resolves no PTY), the runtime
now asks the renderer to background-mount that tab so the PTY attaches and the
live data stream — already subscribed on that path — delivers its output.
A never-mounted workspace has no renderer-graph leaf, so its terminals are
surfaced to mobile via synthetic pty:<ptyId> handles that carry no real UI
tabId. The runtime passes the ptyId through the mount request and the renderer
resolves the owning tab from the persisted tab model (resolveTerminalTabIdForPtyId),
falling back to a whole-worktree background mount if it cannot. The request is
idempotent for already-mounted tabs and never unmounts a live pane, so the worst
case is the same blank as before — no regression.
Validated on an iOS simulator with a two-workspace desktop: opening the
workspace the desktop was not showing rendered a fully blank terminal without
the fix and the live shell prompt with it.
Tests: runtime mount-request gating (real-tab / pty-form / no-ptyId / unknown),
subscribe-handler empty-snapshot-vs-content, and the ptyId->tabId resolver; all
608 RPC method tests pass.
* test(mobile): mock onRequestTerminalTabMount in useIpcEvents tests (fixes CI for #8811)
* fix(mobile): harden blank terminal recovery mounts
* perf(mobile): avoid redundant terminal recovery mounts
* fix(mobile): ignore stale terminal mount requests
* fix(mobile): skip recovery mounts for closed streams
* fix(mobile): reject ambiguous terminal mount ownership
* fix(mobile): preserve terminal handle through recovery mount
* fix(mobile): recover legacy terminal history safely
---------
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
* feat(git-status): batch, cancel, and cache git status polling to cut idl
- Add a single duty-cycled refresh scheduler (activity debounce + 60s
safety timer) replacing multiple overlapping intervals, so status
polling no longer runs near-continuously on large repos (#7983).
- Let safety refreshes reuse cached numstat line counts instead of
re-running diff --numstat every cycle, invalidated by head change,
known mutations, and a bounded TTL.
- Thread AbortSignal/request-token cancellation through IPC, RPC, and
relay layers so a superseded or backgrounded git:status call is
killed instead of finishing wastefully.
- Fix automatic upstream/status apply ordering so a slow, older
refresh can no longer clobber a newer result, and so an earlier
refresh still applies when a later one fails.
* Fix aborted git status scans being mistaken for completed empty results
- An aborted scan/numstat pass now always rejects instead of silently
resolving, so a cancelled request can't look like a valid empty status.
- Stop clearing the line-stats cache key on abort, since an aborted pass
never wrote to it — clearing was evicting a concurrent scan's healthy
snapshot and forcing a redundant numstat recompute.
* Fix aborted git status scans resolving as completed results
Cancelled scans could still resolve with partial or stale data instead of
rejecting, letting callers treat an aborted refresh as a valid status. Also
stop counting aborted scan duration toward catch-up refresh pacing, which
was stretching the next refresh interval by the full length of a cancelled
(often slow) scan.
* Add cancellable, generation-aware git status polling to cut stale scans
- Route git.status through an abortable subscription per requestToken so
cancelStatus can actually abort the remote scan instead of being a
no-op, preventing wasted work and stale responses overwriting fresher
state.
- Bump the git status polling generation on push-target changes so an
in-flight refresh against the old remote/branch can't apply stale
upstream data to the new one.
- Guard the stale-conflict poller against writes after unmount.
- Retire pre-purge line-stat scans in the cache so an older in-flight
scan can't repopulate a key after a token-scoped purge.
* fix(issues): replace cursor-based pagination with page-number Search API
Problem
=======
Issue pagination (#8649) had two bugs:
1. Pages 6-16 were unreachable — clicking page 16 highlighted page 5;
clicking 6/7 did nothing. The old cursor-based approach
(updated:<CURSOR) broke with Search API's relevance sorting —
pages after the first few returned no items even though more
issues existed.
2. Issue numbers appeared out of order on loaded pages (e.g. #1082
between #1308 and #1499), because client-side sort used
updatedAt instead of issue number.
Root Cause
==========
The pagination used two separate GitHub API strategies:
- Initial page 0 load: REST endpoints (repos/:owner/:repo/issues,
repos/:owner/:repo/pulls) sorted by updatedAt
- Subsequent pages: Search API with cursor (updated:<DATE)
These two sources returned items in different orders, causing items
to go missing or appear on wrong pages across page boundaries.
Solution
========
1. Unified on GitHub Search API for all pages — initial load and
pagination both use search/issues?q=...&page=N, eliminating the
REST-vs-Search inconsistency.
2. Changed from cursor-based (update:<DATE) to page-number-based
pagination (page=N), which the Search API supports natively.
3. Switched client-side sort from updatedAt to issue number
(sortWorkItemsByNumber), matching GitHub's default Issues view.
4. Parallelized page fetches in handleLoadNextPage — clicking page
16 now fetches all intermediate pages concurrently (~2s) instead
of sequentially (~30s).
5. Cleaned up dead legacy gh issue list / gh pr list code path,
extracted quoteForSearch helper, shortened overlong comments.
Files changed: 11 files, +140/-127 lines
Closes#8649
* chore: remove unrelated merge formatting
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* fix(mobile): detect and repair overriding Windows Firewall Block rules for pairing
Firewall inspection now reports an overriding inbound Block rule as
blocked instead of false success, the UAC repair removes only
conflicting rules for the current Orca executable, TCP pairing port,
and Private profile before recreating the scoped allow rule, and the
notice re-inspects Windows policy after repair instead of optimistically
reporting success. Stale focus-triggered inspections can no longer
overwrite a newer result during UAC elevation.
Fixes#8371
* fix(mobile): inspect the ActiveStore so GPO firewall rules are visible
Without -PolicyStore ActiveStore the NetSecurity queries read only the
local persistent store, so a GPO-applied Block rule was invisible and
the post-repair re-inspection could report a false success on managed
hosts.