* Show agent session history on mobile
Bring the desktop "Agent Session History" panel to Orca Mobile as a
per-worktree screen: browse past agent transcript sessions across the
host with scope tabs (Workspace/Project/All), search, grouping, session
cards, and tap-to-read message previews.
The transcript scan previously ran only over Electron IPC, so mobile
could not reach it. Expose it over the runtime RPC protocol mobile
already speaks (aiVault.listSessions) so the scan runs on whichever host
owns the transcripts — correct for local and SSH/remote hosts. Both the
desktop IPC handler and the new RPC method share one cache, so opening
the desktop panel and the mobile screen never double-scan.
The pure filter/group/display logic is lifted into /shared (the renderer
re-exports it) so the standalone mobile package can reuse it. Mobile
narrows scoped tabs client-side by cwd path-prefix because the host scan
treats scope paths as a widening union.
Resume-from-mobile is intentionally a follow-up.
* Fix mobile agent history list rendering and RPC authorization
- Authorize aiVault.listSessions in the mobile RPC allowlist so the
mobile client's call is not rejected before dispatch (without this the
screen could never load sessions at runtime).
- Name each SectionList section's rows `data` (the field React Native
reads) instead of `cards`, fixing a type error and silent empty-section
rendering.
* Address review feedback on agent session history
- Match quoted repo:/path: search operator values so labels and paths
with spaces match (e.g. path:"/Users/ada/My Project").
- Hold a scoped tab in loading until the worktree list resolves instead
of firing an unscoped fetch that briefly shows unrelated host history;
proceed once loaded even if the worktree is absent (no stuck spinner).
- Clear cached host capabilities on disconnect/host-switch and failed
status.get so a capability-gated action can't linger for a host that
doesn't support it.
- Cover the real OrcaRuntimeService codex-home forwarding path and the
quoted-operator parser with tests.
* Hide redundant mobile current worktree badges
Co-authored-by: Orca <help@stably.ai>
* Resume agent sessions from mobile history (#6969)
Co-authored-by: Orca <help@stably.ai>
* Adapt merged seams to main's lint and reply-sender hardening
Co-authored-by: Orca <help@stably.ai>
* Cap mobile project-scope paths to the aiVault RPC bound
Co-authored-by: Orca <help@stably.ai>
* Share the aiVault scopePaths bound between the RPC schema and mobile
Co-authored-by: Orca <help@stably.ai>
* Guard shared AI Vault inflight cleanup against concurrent key replacement
The extracted cache module's .finally() cleared inflight tracking
unconditionally, dropping the if (inflightKey === key) guard its sibling
outer cache kept: an older scan resolving after a different-key scan
replaced the tracking would null the newer scan's dedup slot, so a
re-request started a duplicate transcript rescan. Mirrors the sibling
guard; the regression test flushes a macrotask so a reverted guard fails
fast on the call count instead of hanging.
Co-authored-by: Orca <help@stably.ai>
* Harden aiVault.listSessions contract and gate mobile header entry on capability
- Clamp scopePaths (64) instead of rejecting, cap limit at 2000, and make
executionHostId optional so mobile can omit it; restamp per caller.
- Retain successful mobile terminal-create mutation ids for 60s so resume
retries dedupe after transient socket drops.
- Gate the session-header Agent History action on the aiVault.v1 capability
(mirrors the host-list action) so old hosts never show a dead-end entry.
- Fix stale contract comments (scopePaths clamp semantics; filters move
includes quoted repo:/path: operator parsing).
* Add subagent field to session test fixtures after #7423 merge
AiVaultSession.subagent became required on main; the five fixtures added on
this branch predate it. Top-level scanned sessions carry null.
---------
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
* fix(jira): group Jira issues by status in task page
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* feat(jira): order grouped statuses by agile board columns configuration
* test(jira): add comprehensive tests for Jira issue grouping functionality
Add test coverage for the new Jira issue grouping features including:
- Grouping issues by status name
- Sorting sections by agile board column configuration
- Falling back to alphabetical sorting when no board config exists
- Collapsed groups state management
- Filtering issues based on collapsed state
- Backend tests for getProjectStatuses API with various scenarios
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* test(jira): add backend tests for getProjectStatuses and RPC routing
Add test coverage for the new getProjectStatuses backend functionality:
- Returns empty array when no clients are available
- Returns statuses from project statuses API when no board configuration exists
- Orders statuses by agile board column configuration when available
- Handles missing status IDs gracefully by falling back to unordered list
- Clears token on auth errors
- Returns empty array on operational errors
- RPC routing for jira.getProjectStatuses method
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(jira): harden status grouping
---------
Co-authored-by: Andres Van Reepingen <andres.vanreepingen@datacamp.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* fix(terminal): reset PTY delivery accounting across renderer lifecycle resets (frozen panes)
Panes froze permanently because main's PTY->renderer flow control leaked
its unacked in-flight byte counters and pending backlog across renderer
reloads/crashes: the lifecycle reset cleared only visibility hints, daemon
PTYs outlive the page, and the new page never acks the dead page's bytes,
so any pty with >=512KB unacked at reload time was delivery-gated forever
(proven by dev repro: counter pinned at exactly 524288; one manual ackData
of the leaked bytes instantly unfroze the pane).
Fix: (1) zero in-flight counters + pending backlog on every main-frame
renderer lifecycle reset (panes rebuild losslessly from main's
authoritative snapshot on reattach); (2) hold PTY sends until the new
page's pty:data listener signals ready (pty:rendererDispatcherReady), so
boot-window output accrues in the capped backlog instead of being
counted-but-dropped; (3) 10s self-heal watchdog + reconcile-on-handshake
backstop so a missed reset (subframe-overlapped reload emits no
did-start-loading) or lost handshake can never itself freeze delivery;
(4) derive the active-renderer-pty report reactively so in-place rebinds
keep the interactive reserve; (5) expose reset/gate state in the delivery
debug snapshot for future diagnosis.
Adds an experimental reliability gate
(pty-delivery.renderer-lifecycle-accounting-reset) with red/green-proven
regression tests for each branch of the fix.
* chore(reliability-gates): link PR #8034 as the gate's motivating fix
* fix(terminal): sender-guard the dispatcher-ready handshake; fix WSL gate wording
CodeRabbit: the reconcile backstop destructively clears delivery
accounting, so a straggler pty:rendererDispatcherReady from a dying
window's webContents must not reset the new window — reuse the pty:write
main-window sender check (+ regression test). Also resolve the
contradictory WSL coverage claim in the reliability gate notes (WSL rides
the same local/daemon pipeline; mobile/relay and SSH are separate paths).
* docs: full design + context for agent status over WSL (STA-1515)
Why hooks don't work on Windows+WSL (loopback transport gap + WSL-side
installation gap), per-client transport map, the OMP-only fixes that
shipped (7642/7641) and why they don't generalize, the recommended
guest-resident relay over wsl.exe stdio mirroring the SSH relay plus
WSL-side hook installers, alternatives considered, validation facts and
gotchas from the 2026-07-08 Windows rig run, and acceptance criteria.
Co-authored-by: Orca <help@stably.ai>
* feat(agent-hooks): agent status over WSL — guest relay + WSL-side hook installers (STA-1515)
Agent hooks have never worked from inside WSL: under default NAT
networking, WSL's 127.0.0.1 is its own loopback, so every hook POST to
the Windows listener dies silently, and hook configs were only ever
written to the Windows home where WSL agents never see them.
Transport: a hooks-only guest relay (src/relay/wsl-agent-hook-relay.ts)
runs inside the distro, binds WSL loopback on the very port the clients
were already given (host-issued token; EADDRINUSE falls back to :0 with
endpoint-file re-coordination, which also covers mirrored networking),
and forwards parsed envelopes over its own wsl.exe stdio into
agentHookServer.ingestRemote — the same shape as the SSH relay. It exits
when stdin closes so a freed Windows port can never be forwarded into a
dead guest listener.
Installation: the unchanged SSH remote hook installers run against an
SFTP-shaped adapter whose primitives are home-scoped fs RPCs served by
the relay, so all 14 managed agents' hooks land in the WSL home over the
already-open channel with zero per-file wsl.exe spawns.
Lifecycle: per-distro manager ensured from buildPtyHostEnv on every WSL
PTY spawn (covers post-restart daemon reattach re-spawns), stale-bundle
reinstall via exit 42, no-node-43 cooldown, bounded retry for wsl.exe
'Catastrophic failure (E_UNEXPECTED)', breadcrumbed failures.
Zero per-client transport changes; listener stays Windows-loopback-only.
Co-authored-by: Orca <help@stably.ai>
* fix(agent-hooks): WSL relay link-death recovery + Codex runtime-home hook install (STA-1515)
Follow-ups from the first Windows-rig validation of PR #7903:
Link death: a mux protocol error or keepalive timeout could kill the
host<->guest link while the guest relay stayed alive returning 204s —
the manager stayed 'running' and every later envelope blackholed
silently (the exact observed signature: Claude hooks POST 204, store
never populates). wsl-hook-relay-link.ts now guarantees exactly-once
death handling from either signal (mux dispose OR child exit); the
manager breadcrumbs it, kills the child, and self-restarts after a
short cooldown since a live agent session produces no new PTY spawns
to re-trigger ensure. ORCA_WSL_HOOK_RELAY_DEBUG=1 traces each received
envelope pre-ingest. A live integration test pins the full host chain:
the real esbuild bundle over real child stdio through the real manager
into a real AgentHookServer.ingestRemote, exact Claude POST shape.
Codex: Orca launches WSL Codex with CODEX_HOME redirected to the
managed runtime home (~/.local/share/orca/codex-runtime-home/home), so
hooks installed to ~/.codex were never read. installRemote now accepts
an explicit codex home (flat layout), threaded from the relay manager;
the config.toml trust write is deferred while the file doesn't exist
(the launch path seeds it only-if-absent — creating it first would
cancel the seed), and the manager re-runs the byte-equality-idempotent
installers on later ensures (30s throttle) to upsert trust once the
seed lands.
Also: WSL test suites now run on Windows dev hosts (fs-backed suites
skip with rig coverage noted; manager suite uses a fixed POSIX home).
Co-authored-by: Orca <help@stably.ai>
* fix(agent-hooks): renderer ownership gate treats wsl:* connection ids as local (STA-1515)
Round-2 rig finding: with the link fixed, WSL hook envelopes reached
ingestRemote and the durable cache, but useIpcEvents.applyAgentStatus
drops any status whose stamped connectionId differs from the owning
repo's — 'wsl:<distro>' !== null for a local repo, so every WSL-relayed
status died before setAgentStatus and notifications.
wsl:* ids are transport provenance, not ownership: the gate now
normalizes them to local via isWslHookRelayConnectionId (shared
contract, also used by the relay link when stamping), while still
rejecting WSL-stamped events against SSH-owned repos. Provenance stays
stamped — it is what made this drop diagnosable.
Co-authored-by: Orca <help@stably.ai>
* fix(agent-hooks): adversarial-review hardening for the WSL hook relay (STA-1515)
Four independent review lenses over the branch; all confirmed findings
fixed before the next rig round:
Endpoint identity (4/4 reviewers): the guest endpoint dir was keyed by
the EPHEMERAL Windows hook port, so a daemon-surviving agent kept
sourcing the dead port-P1 file after an Orca restart — breaking the
restart-resume acceptance criterion and regressing shipped OMP
recovery. Now keyed by a restart-stable instance key (hash of the
Windows endpoint file path, crossed via ORCA_WSL_HOOK_INSTANCE): the
restarted instance's relay rewrites the SAME file, which is exactly
what re-coordinates survivors.
Restart policy: every failure arms the restart timer (one failed
relaunch no longer ends self-recovery), and the timer probes
wsl --list --running first — wsl -d BOOTS a stopped distro, so
recovery must never resurrect a VM the user shut down; stopped-distro
state is dropped instead. Failure counters reset only after 2min of
stable uptime, so connect-then-die loops escalate to the 10-min cap
instead of cycling every 10s. Timer policy extracted to
wsl-hook-relay-recovery.ts with direct tests.
Also: version-namespaced guest install dir (dev+prod instances no
longer reinstall over each other; PID-suffixed tmp files), 30s install
timeout (a wedged wsl.exe could pin the state machine at 'starting'
forever), per-candidate node version probing (apt node 12 on PATH no
longer masks nvm node 20 into a false no-node cooldown), WSL_UTF8=1 +
NUL-stripped stderr (catastrophic-failure matcher survives UTF-16LE),
ordered post-sentinel chunk handoff, port-fallback breadcrumb via the
home handshake, bad home reply now fails the connect, missing-bundle
warn-once, case-normalized distro keys, disposeAll wired to will-quit,
one-shot 60s reinstall timer for single-spawn Codex trust catch-up,
escaped + contract-derived spawn command.
Co-authored-by: Orca <help@stably.ai>
* docs: record round-3 rig validation status for agent status over WSL (STA-1515)
Co-authored-by: Orca <help@stably.ai>
* fix(agent-hooks): round-4 adversarial-review fixes for the WSL hook relay (STA-1515)
- dropState identity race: recovery re-checks state identity after the
distro-running probe await, and the manager's dropState only deletes the
exact state it was armed for — an ensure() landing mid-probe can no longer
have its fresh relay orphaned outside the map.
- Distro-running probe fails CLOSED: a probe error no longer reports
'running', so recovery can never wsl-d-boot a distro the user shut down.
- Relay spawns use --exec: bypasses the distro's default login shell
(fish/nushell chsh) and passes argv verbatim, dropping the $-escape shim;
same form as the Codex WSL login spawn.
- Post-sentinel chunk handoff rides a microtask so an envelope in the
trailing bytes can no longer dispatch before the link's notification
handler is registered.
- Guest relay mirrors the SSH relay's uncaughtException/unhandledRejection
posture.
- Replay cache capped at 256 panes with recency eviction (the WSL relay has
no per-pane teardown signal); meta map kept in lockstep.
- Launch script derives the stale-exit code from the shared contract
constant; one-shot reinstall timer refuses to arm after dispose.
- New oracles: sentinel unit suite, fs-bridge scoping suite, fixed-token
403/204, EADDRINUSE endpoint-file rewrite, cache-cap eviction, and the
recovery/manager race regressions (verified to fail with fixes reverted).
- Doc: round-4 review section + revised curl.exe stance (kept as the
no-node fallback — Codex is a native binary; fresh distros ship no node).
* docs: record round-4 pinned rig validation for agent status over WSL (STA-1515)
---------
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
* feat(rate-limits): Grok CLI OAuth weekly credit usage in status bar
Read ~/.grok/auth.json (read-only), fetch billing credits via cli-chat-proxy, and surface Grok in Settings, status bar toggles, and rate-limit polling alongside other usage providers.
* fix(grok): clarify comments and address CodeRabbit review
- Shorten Why comments per AGENTS.md; fix billing period end fallback.
- Share GrokAccountStatus type; hash-based locale keys; reload why in Settings.
* docs(grok): plain-language comments and Settings copy
* feat(stats): subscription usage section with Grok in Stats & Usage
Surface rate-limit weekly credits in Settings > Stats & Usage and link to Accounts for setup.
* feat(stats): Grok tab in Usage Analytics dropdown
* refactor(stats): drop Subscription usage block; align Grok pane with Codex
* fix(grok): align settings copy and visibility tests
* fix(grok): add localization catalog entries
* fix(grok): avoid eager usage refresh fanout
* fix(grok): harden usage refresh visibility
* test(grok): cover account status privacy boundary
* fix(grok): target refreshes and redact auth errors
* fix(grok): hide usage UI for signed-out users and align empty states
- Treat a token-less auth.json (e.g. after grok logout) as signed out
instead of surfacing a permanent status-bar error.
- Map billing responses without credit usage to 'unavailable' so plans
with no weekly credits hide the bar like Claude API-key billing.
- Gate the grok status-bar item and toggle on CLI PATH detection,
matching claude/codex/gemini/kimi.
- Guard an empty GROK_CLI_CHAT_PROXY_BASE_URL from producing a
relative billing URL.
- Drop dead minimax/kimi Stats & Usage search keywords left from the
removed subscription section.
- Add missing grok search keyword catalog entries and translate the
English-stubbed stats keywords in es/ja/ko/zh.
* test(ipc): mock grok account registrar in register-core-handlers test
---------
Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
* Add updater diagnostics and refine daemon teardown policy on quit
Introduce detailed logging, crash breadcrumbs, and telemetry tracing
events for the update quit-and-install lifecycle. This improves
visibility into macOS Squirrel.Mac state transitions, install guard
timeouts, and pre-quit cleanup failures.
Also, formalize and unit test the daemon teardown policy on quit to
explicitly keep the daemon alive (via disconnect) during updater and
normal quits so that daemon-backed PTYs are preserved for warm reattach.
* Remove updater lifecycle logging
Remove calls to `recordUpdaterLifecycle` across the auto-updater modules. This cleans up unused or redundant telemetry events during macOS update download, ready, and pre-quit installation phases.
* Remove redundant quit daemon teardown policy helper
Inline the daemon teardown check directly into the `will-quit` event
handler in the main process. The deleted `shouldShutdownDaemonForQuit`
helper simply returned `isDevParentShutdownRequested`, so using that
check directly simplifies the codebase and allows removing the redundant
policy and test files.
* Clarify PTY cleanup targets only local in-process PTYs
Update doc comments, test descriptions, and telemetry events to make
it explicit that only local, in-process PTYs are killed on quit.
Daemon-backed PTYs are preserved via daemon disconnect.
* Recover quit-for-update state on autoUpdater error events
- Reset update flags when the installer fails asynchronously via the
autoUpdater 'error' event (such as "no staged update" errors).
- Prevents the app from getting stuck in a half-transitioned state
where PTYs are killed and windows refuse to reopen.
- Migrate legacy updater warning logs and crash breadcrumbs to use
the structured recordUpdaterLifecycle diagnostic system.
* Simplify duplicate isMacInstallerReady() call in update-downloaded handl
Cache the result in a local variable so the deferred-status check reuses
the value from the log call instead of recomputing it.
* fix: tighten quit-and-install failure recovery
Only recover stuck quit-for-update flags after native quitAndInstall is
invoked and before install is committed. Defer PTY kill and close-listener
removal until after a successful native handoff so sync install failures keep
the session intact, and ignore post-commit updater errors for UI/mac state.
* fix: typecheck login-shell args check in shell-ready test
Avoid args.includes('-l') on a const tuple union that only admits '-i'
on one branch; compare args[0] instead so merge-base typecheck passes.
Add attribute filters (status, priority, assignee, labels) to the Linear
issues list, applied in GraphQL before pagination so hasMore matches the
filtered set. Thread filters through IPC/RPC/store cache with invalidation
on issue mutations, and mirror GitHub-style filter chrome on TaskPage.
* fix(preflight): resolve WSL/SSH agent paths past shell aliases
LeanCTX and similar tools wrap claude/codex as interactive shell aliases.
command -v then returns alias text, which fails absolute-path detection and
hides installed agents (#7816).
Prefer bash type -P, then zsh type -p, then command -v for dash/sh fallback
in WSL agent discovery, WSL isCommandOnPath, and remote relay probes.
* fix(preflight): harden alias-safe PATH lookup chain
Require non-empty results between type -P, type -p, and command -v so bash
type -p empty success cannot skip later lookups.
* fix(preflight): resolve agent executables directly from PATH
---------
Co-authored-by: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com>
* Improve workspace cleanup list
Co-authored-by: Orca <help@stably.ai>
* Address workspace cleanup review feedback
Co-authored-by: Orca <help@stably.ai>
* Fix workspace cleanup perf findings
Co-authored-by: Orca <help@stably.ai>
* Avoid stale cleanup progress cache
Co-authored-by: Orca <help@stably.ai>
* Complete workspace cleanup perf fixes
Co-authored-by: Orca <help@stably.ai>
* Fix worktree list option forwarding
Co-authored-by: Orca <help@stably.ai>
* Address workspace cleanup review nits
Co-authored-by: Orca <help@stably.ai>
* Fix workspace cleanup removal review findings
- Fail a queued removal that now needs a force the user never approved
(confirm-time approvedCandidates snapshot compared in preflight)
- Reword the 120s removal timeout to say removal continues in background
- Wire suppressPreservedBranchToast into cleanup removals
- Stop statting a repo after the first activity metadata timeout
- Document the WSL 9P best-effort stat gap; drop unused locale key
Co-authored-by: Orca <help@stably.ai>
* Split workspace-cleanup slice test to satisfy max-lines
Rebasing onto latest main pushed the combined store-slice test over the
800-line cap. Extract shared fixtures into a test harness and split the
suite into scan-progress and removal-preflight files instead of adding a
forbidden max-lines suppression.
---------
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
* Harden layout validation, watcher lifecycle, and connection robustness
- Throw instead of silently skipping when the packaged daemon-entry is
missing, preventing layout regressions from passing build checks.
- Terminate idle parcel-watcher processes to reclaim native handles and
avoid crash-prone native node module teardowns on shutdown.
- Bind the persisted WS fallback port first to prevent orphaning active
mobile pairings when the preferred port becomes free again.
- Cap concurrent disk reads for restored dirty tab verification at three
to prevent startup connection bottlenecks on remote SSH workspaces.
* Queue file IDs instead of snapshots in restored conflict scans
This avoids using stale file snapshots (e.g., outdated disk signatures)
if a tab is saved, closed, or re-baselined while waiting in the queue
behind the concurrency limit. The live state is now fetched from the
store and validated immediately before initiating the disk read.
On macOS 26, UNUserNotificationCenter aborts when executables are run
from Contents/Resources because bundleProxyForCurrentProcess returns
nil.
Moving the orca-notification-status helper to Contents/MacOS next to
the main Electron executable ensures proper bundle resolution and
avoids immediate crashes.
* auth v1
* fable review
* lint
* Account menu with org membership management
Default UX is a compact account menu (sign in, organization selection, sign
out) that renders only when cloud auth is configured; adds an organization
members dialog (invite, role, remove) gated on server-side role checks. The
multi-profile switcher UI is preserved behind ORCA_MULTI_PROFILE_UI=1.
Co-authored-by: Orca <help@stably.ai>
* Gate the optional account sign-in UI to dev builds
The account switcher stays hidden in packaged builds while the feature is
in progress. Dev builds still show it when the client env vars are set, and
a dev-only Settings > Dev Tools > Orca Cloud section mirrors it.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* feat(onboarding): state-aware macOS notification permission step
The Set up notifications step showed a one-size-fits-all 'Open Mac
Settings' button that simultaneously fired the macOS permission prompt
and opened System Settings — two competing system UIs, with System
Settings unnecessary for the common fresh-install case.
Electron exposes no API to read macOS notification authorization, but
scheduling outcomes do reveal it: a silent probe notification's 'show'
event means permission is granted, 'failed' means delivery is blocked.
A new notifications:probeDelivery IPC runs that probe (cached via
passive delivery evidence and a persisted confirmation flag), and the
onboarding card now renders the real state:
- fresh install: the probe itself pops the native Allow dialog the
moment the step opens; the card flips to 'Notifications are enabled'
automatically when the user clicks Allow (silent 2.5s re-probes)
- blocked: amber card with an Open System Settings deep-link, which
also self-heals once the user flips the toggle
- granted: green confirmation card
The test-notification button now feeds the same card instead of the
ambiguous 'if no banner appeared…' toast during onboarding.
Co-authored-by: Orca <help@stably.ai>
* fix: don't log expected probe rejections while polling for permission
Co-authored-by: Orca <help@stably.ai>
* fix: amber warning styling + single stable dev bundle id for notifications
- Blocked card now uses the app's shipped amber idiom (tinted surface with
amber title/body) instead of white-on-amber-wash, which read muddy in
dark mode; macOS permission card split into its own module to stay under
the max-lines budget.
- Dev instances previously minted a unique macOS bundle id per
branch x Electron version, registering a new Notification Settings entry
every time ('Orca: <branch>' rows piling up forever) and pointing the
settings deep-link at ids System Settings can't resolve. All dev
instances now share com.stablyai.orca.dev: one Notification Center
entry, one permission grant covering every dev build.
Co-authored-by: Orca <help@stably.ai>
* fix: tighten macOS permission card copy
Body copy was one long sentence; now a single short instruction with
'Updates automatically.' as a separate dimmer line. Also repairs locale
catalog parity for keys introduced by commits rebased into this branch.
Co-authored-by: Orca <help@stably.ai>
* fix: drop 'Updates automatically.' line; ad-hoc sign dev app copies
The extra line read as confusing filler — the cards now carry one short
instruction each.
Dev Electron copies had broken code signatures (the Info.plist identity
edits invalidate the ad-hoc seal), which macOS punishes by refusing
Notification Center registration outright: every dev notification failed
with UNErrorDomain error 1, the app never appeared in System Settings >
Notifications, and the settings deep-link had nothing to land on. The dev
runner now ad-hoc re-signs the copied bundle after the plist edits
(bundleLayoutVersion bumped so stale unsigned copies are recreated).
Verified end-to-end: runner-built copy passes codesign --verify --deep,
probe delivery returns delivered, the onboarding card flips green in dev,
and the deep link opens the dev app's own notifications pane.
Co-authored-by: Orca <help@stably.ai>
* fix: drop confusing copy line; session-only permission evidence
Removes the 'Updates automatically.' line from both permission cards.
Also drops the persisted notificationDeliveryConfirmed flag: OS-level
permission changes between sessions, and a stale positive rendered a
false green card. Delivery evidence is now session-scoped only.
Documented detection ceiling (verified empirically on macOS 26): while
the permission dialog is unanswered — and when notifications are toggled
off in System Settings after being authorized — macOS accepts requests
and silently swallows them, with no public API (Notification Center
delivered-history and legacy ncprefs both included) able to distinguish
that from real delivery. 'failed' remains definitive for unsigned builds
and dialog-level denials.
Co-authored-by: Orca <help@stably.ai>
* feat: real macOS notification permission readout via native helper
Electron has no API for UNUserNotificationCenter authorization, and every
observable fallback lies: scheduling succeeds (and getHistory lists the
notification) even while macOS silently swallows display because the
permission dialog is unanswered or notifications were toggled off in
System Settings. The onboarding card therefore showed 'enabled' after the
user disabled notifications.
Adds native/notification-status-macos: a tiny Swift binary that prints
the app's real authorization status. It runs from inside the app bundle
(NSBundle resolves the bundle by walking up from the executable) and
embeds the app's CFBundleIdentifier in a __TEXT,__info_plist section so
every codesign --force pass — electron-builder's signing or the dev
runner's ad-hoc deep sign — derives the identifier macOS keys
notification records to. Spawning it from the app returns authorized /
denied / not-determined exactly matching System Settings.
notifications:probeDelivery now prefers this readout (authoritative,
silent), firing at most one dialog-trigger probe per session while the
decision is pending, and falls back to the previous delivery-probe
heuristics when the helper is unavailable. The card polls the readout
silently in every state, so toggling Allow notifications in System
Settings flips the card within a poll — both directions, verified live.
Test notifications also consult the readout so 'delivered' is no longer
claimed for swallowed notifications.
Packaged builds ship the helper via extraResources and sign it in
afterPack like the computer-use helper; dev copies compile it on demand
(swiftc, non-fatal when missing) with the shared dev bundle id.
Co-authored-by: Orca <help@stably.ai>
* feat: in-app fallback for swallowed notifications + permission card in Settings
- Dispatch now consults the authorization readout before creating a
native notification: when macOS would silently swallow it (denied or
prompt unanswered) it returns reason 'blocked-by-system' instead of
piling invisible notifications into Notification Center. The terminal
notification path surfaces that as a once-per-session in-app toast
with an Open System Settings action. Mobile fan-out is unaffected.
- Settings > Notifications now shows the same live permission card as
onboarding (moved to components/notifications/), polling the readout
so System Settings changes reflect within seconds, and the test
button updates it inline.
- Test sends that are blocked at the OS level now show the
settings-pointing failure toast instead of a generic error.
Co-authored-by: Orca <help@stably.ai>
* fix: hide macOS permission card while Orca notifications are disabled
A green 'Notifications are enabled' card next to a disabled Enable
Notifications toggle read as a contradiction — the card now renders (and
the readout polls) only while Orca's own notifications setting is on.
Also single-flights the authorization helper so simultaneous agent
completions share one readout process.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Identify Claude sessions with no saved conversation turns but possessing
recoverable signals such as queued prompts or subagent transcripts.
This displays them in the sidebar with a "Not saved" badge instead of
filtering them out as empty, and provides detailed notices to recover
them via logs while disabling standard resume actions.
Additionally, extract Gemini session parsing logic into a separate
module and support counting sibling subagent transcripts across local
and remote SSH session scans.
Show a small amber dot on the floating-workspace launcher (both the
floating-button and status-bar triggers) whenever any floating-workspace tab
still has an unacknowledged terminal bell or agent completion, and a
composited amber dot on the Windows tray icon when the window is
minimized/hidden. Both clear through the existing show-until-interact paths —
engaging with or closing the offending tab drops the dot with no stale unread
state left behind.
The launcher dot derives from the existing per-tab/per-pane unread maps via a
new selectFloatingWorkspaceHasUnread selector (primitive boolean, empty-
workspace early return, no bespoke state). The tray dot rides the notification
dispatch and clears on window show/restore.
After #7750 removed the containment guard, the residual #7239 failure mode
is a persisted/inherited startupCwd whose directory no longer exists: every
spawn dies with the provider's missing-directory error. Fresh local renderer
spawns now opt in (cwdFallback: 'worktree') to recover at the workspace root
with a generic in-terminal notice; reattach, SSH, remote-runtime, runtime/API
and mobile callers keep exact cwd semantics, and existing directories —
including outside the worktree (#7685) — spawn as requested.
Co-authored-by: Orca <help@stably.ai>
* fix(ssh): guard mux dead-link detector against sleep/App Nap timer pauses
After system sleep or App Nap timer throttling, the first post-wake
timeout-check tick saw pre-pause keepalives as >20s stale and killed a
healthy link (false 'Connection timed out (no ack received)' ->
dispose('connection_lost') -> reconnect overlay churn). Track the last
tick time; when a tick gap far exceeds the interval, reset staleness
tracking, probe with a fresh keepalive, and let the next full window
make an honest liveness determination. A genuinely dead link is still
detected within ~25s after wake.
Also adds probeLiveness(timeoutMs): a keepalive round-trip primitive
that resolves true on the first frame of any kind, used by the resume
path to distinguish surviving links from dead ones.
Refs #7773
Co-authored-by: Orca <help@stably.ai>
* fix(ssh): probe relay liveness on system resume instead of unconditional reconnect
powerMonitor 'resume' previously called connectionManager.reconnect()
for every active target, guaranteeing a teardown + reconnect overlay on
every wake even when the connection survived sleep. Now each session's
relay link is probed (keepalive round-trip, 5s timeout, one retry for
slow post-wake network); only targets whose probe fails are reconnected.
Dead-after-sleep connections still reconnect promptly. The 'suspend'
grace-time handling is unchanged.
Refs #7773
Co-authored-by: Orca <help@stably.ai>
* feat(relay): prefix relay.log diagnostic lines with ISO timestamps
The remote relay.log had no timestamps, which blocked correlating
reconnect flaps with user activity and sleep/wake windows while
diagnosing #7773. Daemon-mode diagnostic lines now carry an ISO
timestamp prefix ('<ISO> [relay] ...', grep-stable). Connect-mode and
orca-cli passthrough stderr is untouched since it goes back to the
app/user terminal and is parsed (handshake-mismatch detection).
The relay bundle is content-hashed at build time, so the versioned
install picks up the new relay automatically on next deploy.
Refs #7773
Co-authored-by: Orca <help@stably.ai>
* fix(ssh): re-check session identity before post-probe resume reconnect
The resume probe can take ~10s; if the user disconnected the target or the
session/connection was replaced during that window, reconnecting would
resurrect an intentionally torn-down connection (CodeRabbit).
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* Sweep all persisted carriers of a removed SSH target id on re-adoption
reassignSshTargetId re-pointed repos and worktree metas but left the old
target id embedded in persisted session pty ids (ssh:<id>@@pty-N in tabs,
layouts, remoteSessionIdsByTabId), the startup reconnect list
(activeConnectionIdsAtShutdown, replayed via ssh.connect at boot — the
exact 'SSH target not found' in STA-1468), sleeping-agent resume records,
provisioned project host setups, sidebar host-scope arrays, and relay pty
leases. Any survivor resurfaces later as a failing connect or reattach.
New ssh-target-id-migration module re-points every carrier in one pass,
wired into reassignSshTargetId with per-carrier unit and store-level
round-trip tests.
Co-authored-by: Orca <help@stably.ai>
* Bridge SSH connection state to paired remote clients
The SSH surface was desktop-only: ssh:state-changed went to the host's
own BrowserWindow and the web client's ssh API was a no-op stub, so a
paired client's reconnect overlay never learned the host connected and
its target labels stayed empty (STA-1468 — overlay stuck on 'please
connect' over a live terminal).
- New sshStateChanged runtime client event, emitted from broadcastSshState
through OrcaRuntimeService onto the existing clientEvents stream.
- New ssh.listTargets / ssh.listRemovedTargetLabels RPC methods next to
the previously unused ssh.getState / ssh.connect.
- Web preload now routes listTargets / listRemovedTargetLabels / getState
/ connect to the paired host's runtime RPC instead of stubbing them.
- useIpcEvents applies sshStateChanged on paired web clients through the
same guarded path as desktop ssh.onStateChanged; desktop clients ignore
the event since a foreign runtime's targets would pollute their local
SSH store.
Co-authored-by: Orca <help@stably.ai>
* Harden the SSH reconnect overlay against stale or unknown target state
- Only present the destructive 'SSH host removed' state on positive
evidence (a removal tombstone label, or a hydrated non-empty target
list lacking the id). A client whose SSH state never hydrated has an
empty labels map for every id and must not offer workspace removal.
- After a failed Connect, resync target metadata so a stale overlay
converges to the ghost/re-adopted state instead of offering the same
failing Connect forever (the repeated 'SSH target not found' toast
loop in STA-1468).
Co-authored-by: Orca <help@stably.ai>
* Address CodeRabbit review on #7767
- Re-key workspaceSessionsByHostId partitions stored under a removed SSH
host id during re-adoption (no writer keys partitions by ssh host today,
but the schema tolerates it — re-key instead of stranding; live partition
wins when both keys exist).
- Track SSH target-list hydration explicitly (sshTargetsHydrated) instead
of inferring it from a non-empty label map, so a legitimately empty
target list still counts as removal evidence and a never-hydrated client
still never offers destructive removal.
- Apply the refreshed target list before the best-effort removed-labels
fetch in the overlay resync, so a labels failure can't discard it.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(main): detect CJK input source via cfprefsd on macOS 15
macOS 15's `plutil -extract <key> json` aborts with "invalid object in
plist for destination format" on the AppleSelectedInputSources array even
though it is all strings, so the selected-input-source probe threw and fell
back to the keyboard layout id (com.apple.keylayout.US). That disabled
forwardAsciiPunctuation, so third-party IMEs (Sogou, Doubao) sent half-width
,.? to the PTY instead of full-width ,。? in terminal panes and agent chat.
Apple's built-in IME happened not to trip the plutil bug.
Read the live prefs via `defaults export` (cfprefsd) and extract as xml1
before converting the clean subtree to JSON, dodging both the plutil json
bug and the stale on-disk plist. The parser and CJK term list are unchanged.
* fix(main): reap CJK input-source probe process group on timeout
Run the macOS input-source probe via detached spawn and SIGKILL the whole
process group on timeout so a wedged cfprefsd can't orphan the defaults/plutil
pipeline stages (the probe re-runs on every window focus-in). Pin absolute
/usr/bin paths, guard the stdout stream, and cover the non-zero-exit, spawn-
failure, and timeout fallbacks in tests.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: su'qiang <nslogname@MacBook-Pro.local>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* perf(pty): cap the unsent main→renderer PTY backlog (Win/Linux GB leak)
The per-pty `pendingData` string in ipc/pty.ts (main's unsent
main→renderer output queue) had no size cap. The 512 KB/pty + 8 MB
in-flight caps bound only SENT-but-unacked bytes; `onData` appends to
`pendingData` unconditionally and `flushPendingData` merely `continue`s
(no drain) while ack-gated. On Windows/Linux the main renderer is
background-throttled — `setBackgroundThrottling(false)` is set only for
`process.platform === 'darwin'` — and Chromium freezes hidden pages
after ~5 min, so a backgrounded Orca with an active/verbose agent stops
ACKing: in-flight pins at 8 MB, the flush gates, and `pendingData` grows
at raw PTY throughput → MB→GB in the main process. macOS is unaffected.
Fix: cap the backlog to the most-recent PENDING_DATA_MAX_CHARS (2 MB,
matching the daemon pendingOutput and renderer scheduler caps),
advancing startSeq by the dropped-char count (the same arithmetic
flushPendingData uses when slicing), and set a `droppedBacklog` flag on
the next payload. The renderer's dataCallback sees the flag and calls
the existing markHiddenOutputRestoreNeeded(), so on hidden→visible it
rebuilds the dropped span from the main headless snapshot — no output is
lost within scrollback depth. Background agents keep writing full-speed
into the runtime buffer; only the redundant delivery copy is trimmed.
The flag threads through the same layers as the existing `background`
field: main payload → preload types → dispatcher PtyDataMeta → renderer.
Tests (red→green): a daemon pty emits 5 MB while the renderer never
ACKs. Without the cap the flag is never set (fail); with it the first
emitted chunk carries droppedBacklog exactly once and delivery stays
bounded. Ordinary small output never sets the flag.
Note: leak + fix are Win/Linux-only (macOS is throttling-exempt), so not
reproducible at runtime on macOS; evidence is the code trace + tests.
* test(pty): clarify that the sent-total assertion is a sanity bound, not the cap proof
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* Allow Windows SSH directory browsing to recover
The remote project picker runs before the relay filesystem ACLs exist, so it uses a raw SSH exec channel with POSIX shell commands. Windows OpenSSH targets whose default shell is cmd.exe reject Orca's POSIX exec wrapper, which prevented browsing any remote directories. Keep the existing POSIX path as the first attempt, then fall back to a narrowly scoped PowerShell listing that emits the same line-based format.
Constraint: Add Remote Project needs raw SSH browsing before relay roots are registered
Constraint: Windows OpenSSH may use cmd.exe as the remote command shell
Rejected: Replace the POSIX command for all hosts | riskier for existing Linux/macOS SSH targets
Confidence: medium
Scope-risk: narrow
Directive: Keep POSIX browsing as the primary path; Windows fallback exists only for shell-wrapper rejection errors
Tested: npx --yes pnpm@10.24.0 exec vitest run --config config/vitest.config.ts src/main/ipc/ssh-browse.test.ts
Tested: npx --yes pnpm@10.24.0 exec oxlint src/main/ipc/ssh-browse.ts src/main/ipc/ssh-browse.test.ts
Tested: npx --yes pnpm@10.24.0 exec oxfmt --check src/main/ipc/ssh-browse.ts src/main/ipc/ssh-browse.test.ts
Tested: npx --yes pnpm@10.24.0 run typecheck:node
Not-tested: Live Windows OpenSSH host with cmd.exe default shell
* fix(ssh-browse): strip CRLF in Windows PowerShell browse output
Windows OpenSSH exec emits CRLF, but the browse parser split on \n only
and never stripped the trailing \r. Every directory line then failed the
endsWith('/') check and was misclassified as a file with a stray CR in
its name (and resolvedPath kept a trailing \r) — breaking the exact
Windows path this PR adds. Split on /\r?\n/ to match the existing
ssh-relay-versioned-install convention; POSIX (LF-only) output and
filenames with legitimate leading/trailing spaces are unaffected.
The added fallback test used \n-only fixtures that no real Windows host
produces, masking the bug; switch it to realistic CRLF output so it now
guards the regression.
Co-authored-by: Orca <help@stably.ai>
* fix(ssh-browse): locale-independent Windows fallback + UTF-8 output
- Trigger the PowerShell fallback on cmd.exe's locale-independent 9009 exit
code, not just English/Spanish stderr text, so non-English Windows hosts
actually recover.
- Pin [Console]::OutputEncoding to UTF-8 in the PowerShell script so non-ASCII
names (e.g. C:\Users\José, CJK, Cyrillic) aren't mojibake'd when decoded.
- Rethrow the original POSIX error when the PowerShell retry also fails, so a
false-positive predicate match doesn't mask the real failure.
- Tests: decode the -EncodedCommand payload to guard injection-safe escaping
and the UTF-8 pin; add a 9009-locale fallback case and a negative-predicate
(permission denied must not retry) case.
- Drop an incorrect sentence from the CRLF-split comment.
Co-authored-by: Orca <help@stably.ai>
* fix(ssh-browse): surface PowerShell error on proven-Windows fallback
When the fallback was triggered by cmd.exe's 9009 exit, the host is provably
Windows and PowerShell genuinely ran, so its error ('Cannot find path' /
'Access is denied') is the real cause — surface it instead of the misleading
cmd.exe "exec is not recognized" prose. Only the string-heuristic path (a
possible POSIX false positive) still rethrows the original error.
Also: note the system-ssh transport's 8-bit exit-code truncation caveat in the
9009 comment, and add tests for both double-failure paths (9009 -> surface
PowerShell error; heuristic -> surface original).
Co-authored-by: Orca <help@stably.ai>
* test(ssh-browse): guard the ~ -> $HOME PowerShell fallback branch
The tilde expansion in powerShellPathExpression (~ is the default browse path)
was unguarded — collapsing it to a literal would pass the whole suite. Add a
fallback test asserting the decoded script contains $dir = $HOME.
Also tighten the 9009 comment: the locale-independent trigger only holds for a
cmd.exe DefaultShell on the ssh2 transport; a powershell.exe DefaultShell exits
1 (not 9009) and, like the system-ssh transport, relies on the stderr heuristics.
Co-authored-by: Orca <help@stably.ai>
* fix(ssh-browse): emit forward-slash resolvedPath from Windows fallback
Get-Location.ProviderPath returns a native backslash path (C:\Users\alice),
but the renderer's parentPath/joinPath only split on '/': backslash paths make
the browser's 'Up' button a no-op and produce mixed separators when descending.
Normalize the emitted resolvedPath to forward slashes (matching the POSIX
branch) while keeping the native $resolved for Get-ChildItem -LiteralPath.
Update the fallback-test fixtures to the real forward-slash output and assert
the -replace normalization is present in the generated script.
Co-authored-by: Orca <help@stably.ai>
* fix(ssh-browse): root Windows drive paths in the PowerShell fallback
The forward-slash resolvedPath means the renderer rebuilds Windows paths with
POSIX helpers, so it hands back drive paths Set-Location mishandles: the
breadcrumb prepends a spurious leading '/' (/C:/Users -> current drive's root),
and 'Up' from a first-level dir yields a bare drive letter ('C:' is
drive-relative, not C:\). Normalize both to a rooted drive path in
powerShellPathExpression so navigation lands where the user clicked. POSIX,
UNC, and relative paths are left untouched.
Add parametrized tests for /C:/Users and C: -> rooted $dir literals.
Co-authored-by: Orca <help@stably.ai>
* test(ssh-browse): cover combined /C: drive-path normalization
Guards the strip-then-root ordering in normalizeWindowsDrivePath so a future
refactor can't regress the combined leading-slash + bare-drive case.
Co-authored-by: Orca <help@stably.ai>
* fix(ssh-browse): trigger Windows fallback on non-zero exit, not exit 9009
Verified on real Windows OpenSSH + cmd.exe that a rejected POSIX exec
wrapper arrives over SSH as process exit code 1, not 9009 (cmd.exe's 9009
ERRORLEVEL never crosses its process boundary; sshd forwards the process
exit code). Confirmed on both the ssh2 and system-ssh transports.
The old trigger keyed off exit 9009 (dead code) with an English/Spanish
stderr string fallback, so non-English cmd.exe hosts (German/French/
Japanese/etc.) never fell back and directory browsing failed.
Fix: retry via PowerShell whenever the POSIX attempt fails with a
RemoteBrowseError (command ran, exited non-zero) - locale-independent and
covers every cmd.exe locale. Transport errors/timeouts aren't
RemoteBrowseErrors, so dropped connections aren't mis-retried. Pick the
error to surface via the POSIX "command not found" exit 127 (no
powershell.exe means the host isn't Windows, so surface the original POSIX
error). Removes the fragile 9009/stderr-string heuristics.
Tests: correct injected exit codes to the real value (1), add a
Japanese-locale regression test, and lock the retry/no-mask contract.
---------
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com>
Deleting a config-sourced SSH target had no lasting effect: the Manage-SSH pane
re-imports ~/.ssh/config on open, and the import was a pure upsert with no record
of deletions, so the just-deleted host was re-inserted verbatim from the config
that still exists on disk.
Persist a `deletedSshConfigAliases` tombstone set:
- Deleting a config-managed target (source 'ssh-config', or an adopted legacy
import) records its alias; manual targets are never tombstoned.
- The passive on-open sync skips tombstoned aliases, so a deleted host stays
deleted.
- Re-adding or editing a target reclaims its alias, and the explicit Import
action (`reAdopt`) clears all tombstones to deliberately re-adopt config.
This also fixes the edit-then-reappear case: editing a config host to `manual`
already reserved its current alias, and reclaim covers alias changes.
110 files carried an eslint/oxlint-disable max-lines directive but are
already under the default max-lines budget (300 .ts / 400 .tsx / 600 .mjs
/ 800 test), so the suppression is dead. Removing it restores real
max-lines coverage on these files with zero behavior change.
Each removed directive had max-lines as its only rule; verified via a
full oxlint run (0 max-lines violations, 0 new errors). Diff is pure
deletions (200 lines, 0 additions) — no code touched.
Co-authored-by: Orca <help@stably.ai>
* fix(emulator): remove destroyed listener on stream stop to stop webContents leak
Both emulator stream IPC handlers register owner.once('destroyed', ...) per
start but never remove it on stop. .once only self-removes when the event
fires (window close), so every emulator tab show/hide cycle leaked a closure
on the long-lived main-window webContents — ~11 cycles trips Node's
MaxListenersExceededWarning and the closures grow unbounded until the window
dies. Store the handler on the session/subscription and removeListener on stop.
Co-authored-by: Orca <help@stably.ai>
* chore(emulator): trim why-comment to 2 lines, drop no-op afterEach
Review polish: honor AGENTS.md 1-2 line comment guidance and remove a
vi.clearAllTimers() that is a no-op without fake timers.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Enable three unicorn rules — one correctness, two performance — and fix every
existing violation repo-wide so the rules pass as errors.
prefer-number-properties (76 sites)
- parseInt/parseFloat/NaN -> Number.* : safe aliases (autofixed).
- isNaN -> Number.isNaN (12 sites, hand-converted): global isNaN coerces its
argument, Number.isNaN does not. Verified every call site already passes a
number (Number.parseInt results, number-typed fields, Date.getTime()), so the
conversion is behavior-preserving today and guards against a future non-numeric
argument silently coercing.
prefer-array-find (26 sites)
- .filter(pred)[0] -> .find(pred); .filter(pred).at(-1) / .pop() -> .findLast(pred).
Drops the intermediate array and short-circuits.
prefer-array-index-of (5 sites)
- .findIndex(x => x === v) -> .indexOf(v).
Verified: typecheck (node/cli/web) clean, 53 affected suites pass (1679 tests),
oxlint clean repo-wide. mobile/ uses findLast safely (already ships ES2023
.toReversed()); config scripts and e2e helpers run on Node 24.
Reject SSH relay PTY reattach when the persisted lease identity does not match the current relay PTY, and carry pane/tab identity through metadata so remote hook env stripping does not bypass the guard.
Also preserves live reused PTY ownership on identity mismatch so a stale lease cannot tombstone a fresh owner.