mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
8a07bbd8cf58dcaca62868101627eddc6feeb0ac
9254
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8a07bbd8cf |
fix(orchestration): enforce nested worker depth instead of an accidental fence (#16668)
* fix(orchestration): enforce nested worker depth instead of an accidental fence Orca documented that "dispatched workers cannot spawn their own sub-workers (worker-start is coordinator-fenced)". No such check existed. What existed was a single Run-binding check in the workerStart RPC: a worker's terminal is not bound to a Run, so worker-start happened to fail. The rule was emergent, asserted by no test, and written in no doc — and it leaked. A worker could run-create its own Run, task-create, and worker-start: now bound, the check passed. Replace it with a real, configurable depth cap. Depth is derived from the caller's own active Dispatch rather than from Run binding, which is what dissolves the run-create bypass: creating a Run does not stop you being a worker. Enforcement lives in a single dispatch-row writer that owns all three INSERTs that mint a live worker — the generic claim, the supervised worker-start path (including every retry), and the remote attachment. Two of those were missed by earlier drafts of this change, so `creator` and `maxDepth` are required parameters: a new spawn path cannot compile without deciding, and a boundary test refuses the SQL anywhere else. Schema v30 adds depth to dispatch_contexts and remote_dispatch_attachments, NOT NULL DEFAULT 1 and backfilled to 1 so an unstamped or pre-upgrade row fails closed rather than reading as a root coordinator. The attachment pane indexes widen to the five states in which a remote worker may still be running: loss of contact is not evidence of process death, so an unverifiable worker still counts as a nesting parent. Also adds the caller-evidence assertion that workerStart was the only Run-scoped verb to skip, so a declared --from cannot name another terminal's pane and inherit its depth. Default is 1, so behaviour is unchanged unless the new setting is raised. Two limitations are deliberate and documented rather than papered over: this is a guardrail and not a security boundary, since a caller whose launch evidence is unverifiable (any ordinary restored terminal) can declare another handle; and it is enforced at supervised dispatch creation, so a settled worker whose process is still alive counts as a root again. * fix(orchestration): share caller resolution and pin worker gaps * refactor(orchestration): make the caller resolver's pane contract explicit Overloads so requireStablePane callers get a non-null string instead of casting, and rename the attestation opt-out to say what it means: the caller asserts it itself. A flag called assertEvidence:false reads as "attestation optional", which is the hole this helper exists to close. * fix(orchestration): propagate dispatch depth to federated workers * chore(cli): refresh bundled orchestration guide |
||
|
|
256f23c7a0 | fix(automations): validate create destination projects | ||
|
|
5a59bc5bc4 |
fix(grok): stop Orca's Grok hooks from costing anything outside Orca (#16666)
* fix(grok): stop Orca's Grok hooks from costing anything outside Orca Orca registers Grok agent-status hooks in the global $GROK_HOME/hooks. Grok loads that directory on every session, so a Grok run that Orca did not launch still paid for the hook on every event, and Orca rewrote the file even after a user had emptied it to opt out (#15518). The registered POSIX command now guards on ORCA_PANE_KEY before doing anything. That variable is part of the pane identity Orca injects into terminals it launches, and unlike the port and token it never comes from the endpoint file, so it is present exactly when the session belongs to Orca. A standalone session short-circuits without spawning a shell for the managed script at all. The same guard is applied to the remote install, because a remote host runs standalone Grok sessions too. PreToolUse is no longer registered. It is a blocking hook, so Orca sat on the critical path of every tool call and doubled the per-tool spawns, for a transition PostToolUse already reports. Windows cannot use the guard: the command there must be a single spawnable token, so it is a bare script path with no shell to evaluate a test. For that case the hooks are removed when Orca quits -- locally, on WSL guests, and on connected SSH hosts -- and reinstalled on the next launch. A config the user has emptied is left alone on startup; turning the setting back on in Settings is an explicit and later choice, so that path reinstalls. Removal is careful about what it is deleting. It strips only Orca's own entries, keeps user-authored ones, and deletes the file only when no hook entries remain -- keying that off the whole object would leave a stray non-hook key behind, and the emptied-config check would then read that remnant as a deliberate opt-out and never reinstall. A config the user has symlinked into a dotfiles repo is written through rather than unlinked, and is exempt from the emptied-config check for the same reason: after a quit it is a file Orca emptied, not one the user did. Writes go through temp+rename. Grok refuses to build a sandbox profile for a hook JSON with more than one hard link, so publishing by hard link would fail any session that started during the write. Install and removal on remote hosts now read the platform from the same field. They did not, so a Windows remote whose bridge env was incomplete had hooks installed and never removed. Co-authored-by: Siddiqui Qamar <137684575+siddqamar@users.noreply.github.com> * fix(grok): preserve hook state outside Orca --------- Co-authored-by: Siddiqui Qamar <137684575+siddqamar@users.noreply.github.com> |
||
|
|
588eec68b4 |
fix(native-chat): stop rendering a tool result whose call is outside the window (#15653)
* fix(native-chat): stop rendering a tool result whose call is outside the window A tool result carries no call id, so it can only be attributed to a tool call loaded alongside it. Both chat views read a windowed transcript tail (mobile 40 messages, desktop 300), and the window regularly opens between an assistant's `tool_use` record and the user-role record that answers it. Claude also re-emits already-answered `tool_result` records at a `/compact` boundary, long after their call scrolled out of the window. `foldToolMessages` had no rule for those: with no assistant predecessor in the output they were pushed through as standalone messages and rendered as a bare, unowned block of raw tool output with no tool name — reading as a message from nowhere mid-conversation. Sampling real Claude transcripts, 176 of 400 sessions (44%) produced one in a mobile-sized first page. Drop a result no loaded call can own, before folding. It is not lost: it comes back attached to its call as soon as the owning turn pages in. * fix(native-chat): scope tool result attribution to folded turns * fix(native-chat): preserve harness-attributed tool results * fix(native-chat): keep interruption boundaries |
||
|
|
d9c77c5830 | fix(automations): restore main checks | ||
|
|
b755629f37 |
feat(i18n): add Korean translations for CLI-created workspace labels (#16212)
- Localize filter toggle labels in SidebarFilter and SidebarWorkspaceFilterSection. - Localize card detail descriptions in WorktreeCardCliDetailSection. - Localize meta badge accessibility label in WorktreeCardMetaBadges. - Resolves English fallback for CLI-created workspace UI under Korean locale. |
||
|
|
3fbed6612e | docs: remove tracked design plans (#16663) | ||
|
|
cda2280d63 |
Show all automations (#16532)
* Add all-host automations with scoped ownership and multi-authority suppo
Enable automations to run on multiple hosts (SSH targets and local) with
owner-fenced mutations, scoped list queries per host, and conflict
resolution. Introduces desktop and runtime authorities as distinct
automation storage owners, with per-host caching, invalidation, and
retry scheduling on the renderer. Captures registration generations for
SSH hosts to survive re-adoption. Adds CLI support for destination
selection and conflict recovery.
* Filter automation create projects by destination host
Only offer projects available on the selected destination, preventing
the mismatches that would fail at submit time. Auto-adjust the project
selection if it becomes unavailable when the destination changes.
* Add runtime storage authority support for automations
- Support both runtime and desktop as automation storage authorities
- Make owner preconditions optional for legacy-client compatibility
- Cache automation list projections to improve performance
- Add per-row repo/worktree resolution for cross-authority collisions
- Extend automation.list RPC to always include owner metadata
* Replace child_process.execFile with runProcess for external automations
- Migrate external-manager to use cross-platform runProcess wrapper per child-process safety policy
- Abstract electron app/ipcMain APIs in orca-runtime via environment accessors
- Install fake app environment in automation tests for consistent setup
- Reorganize imports to use specific module paths (ssh-target-registry, agent-detection, browser-error)
- Remove external-manager from child-process import allowlists (no longer violates direct import)
* Unify desktop automation CRUD onto the local runtime RPC surface
The desktop authority now speaks the same automation.* RPC contract as
remote runtimes, via callRuntimeRpc({kind:'local'}) -> runtime:call ->
the shared RpcDispatcher. The automations:list/listRuns/create/update/
delete/runNow IPC arms, their preload members, and every renderer
desktop-vs-runtime transport fork are retired; the runtime methods are
the single implementation of scoped lists, owner fencing, and change
publication for both transports (mobile clients already exercised them).
The desktop probe scheduler's priority lease survives the move as an
AutomationService hook the IPC registration installs and the runtime
methods take, so Orca's own automation traffic still parks queued
external-manager probes.
External-manager scope arms and dispatch-loop plumbing stay on IPC by
design; automation change events keep their existing channels (renderer
ingestion already converges them by authority).
* Remove automation ghost SSH tombstone scanning
This functionality for synthesizing tombstones for automation-referenced SSH
targets is no longer needed as part of the automation system refactoring.
* Refuse orphan automations at dispatch time, not migration time
Remove migration-time disabling of orphan automations and the `enabledDecidedBy` field. Dispatch now refuses orphans at runtime instead, simplifying state management and UI. Orphans are left unstamped and enabled; dispatch refuses to run them via `resolveAutomationRunTarget`.
* Show all automations in flat table with unified filter menu
- Replace host picker component with comprehensive Filters menu supporting status, last run, agent, and host filters
- Flatten automation list layout to single table instead of host-grouped sections
- Add Host column to display execution host for each automation
- Display active filters as removable pills below toolbar
- Delete unused AutomationHostPicker* components
* Add automation owner fencing and destination validation
- New AUTOMATION_OWNER_FENCING_RUNTIME_CAPABILITY for owner preconditions; legacy clients get owner metadata snapshotted at RPC boundary for compatibility
- Editor captures and revalidates automation destination before save, preventing silent retargeting if SSH infrastructure changes mid-edit
- SSH target types now isolate renderer-authored fields; generation is server-owned and stripped by IPC handlers
* Route automation recovery actions to the origin host
When an automation action fails due to owner fencing, recovery verbs
("Update server", "Reconnect") must run on the host where the refusal
originated: the row's captured owner for row operations, or the
destination the create dialog captured, not the list's filtered host.
* Remove external manager scope limitation notices
Consolidate create destination eligibility checks with a unified predicate
and fix the bug where desktop repo IDs could be sent to runtime hosts where
they cannot resolve.
* Persist only store-derived automation contexts, not client-perspective o
Store contexts must never be based on client-provided runContext or sourceContext
values—clients speak a different perspective (e.g., 'runtime:<id>' for host IDs
they assign), and persisting those makes the store projection orphan automations
it actually owns. Derived contexts now take precedence in create and update paths,
with explicit null still honored to clear a value. Tests verify this by simulating
drift after storage and confirming that moves re-derive while toggles preserve.
|
||
|
|
885106baee |
fix(terminal): stop routing unowned workspaces to the focused runtime (#16584)
`createWebRuntimeSessionTerminalResult` collapsed an explicit
`environmentId: null` ("I resolved ownership and nobody remote owns this")
into "caller said nothing", then fell back to
`settings.activeRuntimeEnvironmentId`. The tab-strip "+" shell rows and the
guest-focus Ctrl+T relay both pass that explicit null, so a local workspace's
new terminal was created against whatever remote runtime happened to be
focused, which answered `selector_not_found` for a worktree id it had never
seen.
The same call selects the runtime as the workspace's execution host before
the create, and the error path never handed that selection back — leaving the
workspace latched to the runtime that just refused it, so every later
owner-routed action (the next Ctrl+T included) silently followed the latch
until a workspace switch reset it.
Fixes #16444
|
||
|
|
44a3ba59e6 |
test(git): isolate worktree-shared-directories git config portably (#16582)
`os.devNull` is `\\.\nul` on win32. Git normalizes it to `//./nul` and rejects it as a config path, so every `git` call in this suite threw in `beforeEach` and 15 of 19 tests failed on Windows. POSIX resolves the same constant to /dev/null, which Git accepts, so CI never saw it. Point GIT_CONFIG_GLOBAL at a real empty file in a private mkdtemp directory, matching how skill-git-tree-identity and skill-windows-workspace already isolate, and use GIT_CONFIG_NOSYSTEM instead of GIT_CONFIG_SYSTEM. Set both on `process.env` rather than only on the suite's `git()` helper. `resolveWorktreeSharedDirectories` runs its own `git check-ignore` through the production runner, and `GitRuntimeOptions` carries no env, so the runner inherits `process.env`. The per-call override never reached the code under test: a host `core.excludesFile` could make a fixture that is not gitignored come back as ignored. Fixes #15409 |
||
|
|
19e9ec695b |
perf(windows): ship the native process table to Windows relay hosts (#16598)
* feat(windows): let a relay host bind the native process table directly The CIM fallback from #16550 answers on relay hosts, but it costs a powershell.exe and ~1.4s per scan where the native reader costs ~57ms. It is a parachute, not the destination. Teach the loader a second source: the desktop app keeps resolving the npm package, and a relay host -- which has none of our node_modules -- binds a bare `windows-process-tree.node` staged beside the bundle. The CIM scan stays as the last resort, so a host with neither is unchanged. Bind the addon directly rather than its package wrapper. lib/index.js adds only a queue over getProcessList, and that queue is the wedge this module already defends against: it latches a module-global requestInProgress with no try/catch. We hold our own single-flight and deadline, so going straight to the addon drops the duplicate. Measured on a Windows 11 SSH host with ~1490 processes, running the relay-externals bundle from the deployed relay directory: no addon staged nativeAvailable=false 1247ms (CIM) addon staged nativeAvailable=true 57ms memory restored Degradation was exercised on that host, not just in fakes: a truncated upload, a text file, and a foreign-arch ELF each fall through to the scan rather than throwing, and restoring a good addon recovers. A file that loads but lacks getProcessList is rejected by shape, because binding to it would reject every read forever where falling through still answers. No artifact is staged yet, so this is inert until the packaging change lands: today every relay takes the same CIM path it does now. * build(relay): ship the Windows process-table addon to relay hosts The CIM scan restored correctness on Windows SSH hosts, but it costs a powershell.exe and ~1.4s per read where the native addon costs ~57ms. It was always the floor, not the destination. The addon cannot be npm-installed on a relay host: it carries a binding.gyp, so npm rebuilds from source and the build wants Spectre-mitigated libraries even where MSVC is already present. The binary inside the published tarball loads, but predates our patch and still caps enumeration at 1024 processes -- on a 1486-process host it returned exactly 1024 rows with the querying process among the missing, which reads as unavailable only under load. No published alternative clears the bar either; the one fork with a working prebuild story still carries the same cap. So build it where a compiler exists and ship the result. The build script refuses unpatched source -- checking the source rather than trusting the install, because the Spectre hunk fails loudly while the 1024 hunk fails silently -- and verifies the PE machine field so a cross-build cannot emit host arch for another target. The artifact is optional: hashed when present so a relay carrying it never shares an immutable directory with one that does not, and never probed, since requiring a file only a Windows build machine can produce would make a correct relay read as MISSING and redeploy forever. Builds on any other OS keep using the scan, unchanged. arm64 cross-compiles from the x64 runner but needs the optional MSVC ARM64 toolset, so it stays best-effort: a runner image without that component should cost arm64 relays the fast path, not fail the release the x64 relay is riding on. ORCA_REQUIRE_RELAY_NATIVE_ADDONS is a per-arch list rather than a flag for exactly that reason. * build(relay): require the arm64 process-table addon too The arm64 cross-compile is no longer unproven. On a Windows x64 machine with the MSVC v143 ARM64 build tools component installed, node-gyp --arch=arm64 produces a genuine ARM64 image: x64 machine=0x8664 152064 bytes arm64 machine=0xaa64 139776 bytes So arm64 stops being best-effort and joins x64 in the required list. It was only best-effort because the component is optional and I had not seen it succeed; a runner image without it now fails the build with MSB8020 naming the missing component, and that step runs before the long packaging step so the failure costs seconds rather than twenty minutes. The env var stays a per-arch list rather than reverting to a flag, so a future arch can land best-effort before being promoted the same way. |
||
|
|
691759540a |
fix(terminal): blur suspended panes on the dispose branch too (#16592)
suspendPaneRendering blurred panes only on the WebGL-retention branch; the dispose branch — taken by every pane past MAX_RETAINED_HIDDEN_WEBGL_CONTEXTS=6 — did not. Make it unconditional so both branches leave a suspended pane in the same state. No measured cost is being fixed, and the earlier cursor-blink-timer rationale was wrong. Measured on Windows 11 against the shipped @xterm/xterm 6.1.0-beta.287 + @xterm/addon-webgl 0.20.0-beta.286, N=12 panes: display:none and inert each make Chromium fire a real blur on the pane's helper textarea, which pauses the WebGL blink interval on its own, and disposeWebgl() disposes the blink manager regardless. Hidden panes measured 0 interval fires and 0 rAF fires over 8s with and without the explicit blur. Focus is also a document-wide singleton, so "one timer per hidden pane" was never possible. Kept as defence in depth for opacity:0 without inert — TerminalOverlaySlot's startup probe inside an active worktree — the one hide mode that keeps focus. |
||
|
|
e2cb797506 |
perf(sleep): park idle agents in the worktree you are working in (#16591)
The planner skipped the entire activeWorktreeId, so the tree a user actually works in never parked anything — exactly where a 16 GB Windows host accumulates its idle Codex/Grok panes and starts hard-paging (#16211). The two guards that remain are the correct granularity and already existed: foregroundTerminalTabIds covers the tab on screen, and the foregroundTerminalLastSeenAtByTabId floor in getEligiblePane holds any tab left inside the idle window. Test lever taken from @sanshengai's #16214, which found this first: pinning the existing sibling-tab regression to activeWorktreeId means it fails against the pre-fix planner. A standalone background-worktree case does not, because the fixture's active worktree is a different one — that is why the first cut of this change shipped a vacuous test. #16214 changed only the planner suite; the same one-line change also breaks agent-hibernation-coordinator's two revalidation tests, which used activeWorktreeId as their eligibility lever. Those now flip setForegroundTerminalTabIds instead, which is the property they were written to prove. Co-authored-by: sanshengai <sanshengai@users.noreply.github.com> |
||
|
|
87f5c6cd03 |
perf(terminal): stop rebuilding parked-watcher keys on every overlay render (#16596)
* perf(terminal): stop rebuilding parked-watcher keys on every overlay render Every mounted worktree's TerminalPaneOverlayLayer rebuilt its parked-watcher synchronization key from scratch on every render: JSON.stringify of the whole split-tree root per tab, then a second JSON.stringify pass that re-escaped that already-serialized string. Two app-global subscriptions in the cold-parking hook (pendingStartupByTabId, sleepingAgentSessionsByPaneKey) made any write for any tab in any worktree trigger that render everywhere at once, so the cost scaled with mounted worktree count. - Memoize the store-derived half of the reconciliation key on the already shallow-stable selector output. The captured-pane half still recomputes per render because that registry mutates outside React. - Replace the outer JSON.stringify of already-serialized fragments with a length-prefixed join, which is injective for arbitrary fragments and does no escaping pass. - Narrow both global subscriptions to worktree-scoped, value-comparable keys. Measured on a 12-worktree x 4-tab x 4-leaf-split model: 9.5 us -> 1.3 us of key work per worktree render (7.3x), before counting the renders the narrowed subscriptions now avoid entirely. Key semantics are unchanged: no hash is introduced, only memoization of an identical serialization and an injective replacement for the outer pass. * refactor(terminal): narrow the park subscriptions with useShallow, not string keys Review follow-up. zustand's `shallow` already compares Sets and plain objects structurally and order-insensitively, so the encode-to-string / parse-back pair each subscription carried was doing by hand what `useShallow` does for free. - Restore the Set-returning `selectSleepingRecordParkExemptTabIds` and subscribe through `useShallow`. Drops the NUL separator, the `.sort()` that existed only to keep insertion order out of the key, the O(k^2) `includes` dedup, the parse helper and the caller's `useMemo` — and removes the ordering invariant that was enforced by a comment alone. - Same for the pending-startup presence hook: `useShallow` over the presence record, keeping the frozen empty singleton for the zero-allocation steady state. - Drop the `useMemo` around the reconciliation selector. `useShallow` returns a fresh closure every render regardless, so the memo bought nothing and its WHY comment described behaviour zustand 5 does not have. The memo that is the real fix here, `reconciliationStoreInputsKey`, is untouched. Adds a narrowing case for a sleeping record this worktree can never resume, which pins both the blocked-record exemption and the narrowing itself; it fails against the pre-narrowing code (2 renders, expected 0). Net -25 lines of production code. |
||
|
|
7f034a182f |
docs(windows): correct why the process-tree addon is not installed on relay hosts (#16565)
The note said the package "ships no prebuilds". It does: the published 0.8.0 tarball carries build/Release/windows_process_tree.node, apparently an accidentally published MSVC build directory (.obj and .tlog files ship with it). The conclusion was right and the reason was wrong, so record what was actually measured on a Windows SSH host with 1486 processes. Installing it normally rebuilds from source, because the tarball carries a binding.gyp and npm runs node-gyp regardless of what is already compiled inside. That build fails with MSB8040 (Spectre-mitigated libraries) even on a host that already has MSVC Build Tools 2022 -- the requirement our binding.gyp patch deletes, and patches do not cross SSH. Skipping the build keeps the tarball binary, which loads (it is N-API) but predates the src/process.cc patch and still caps enumeration at 1024. On that host it returned exactly 1024 rows with the querying process among the missing, which the self-presence guard rejects -- so it would work on a quiet machine and fail only under load, the shape of bug that survives testing. Also records the measured cost of the fallback, since the table's 706ms figure is from a 1050-process host and reads as more headroom than there is, and names the fix for the tracked gap: ship our own patched .node as a relay asset, as config/relay-assets already does for node-pty. |
||
|
|
1fafccb26b |
fix(settings): use Workspace Directory for the Create-project default path (#14767) (#16583)
* fix(settings): use Workspace Directory for the Create-project default path `repos:getDefaultCreateProjectParent` hardcoded `join(homedir(), 'orca', 'projects')` and never consulted the settings store, so Settings -> General -> Workspace Directory had no effect on the Location field of "Create new project". Users had to retype the path every time, or fake it with an NTFS junction. Resolve the parent from the store instead, through the same rule the rest of the app uses for a host preference: `host override ?? client default`, i.e. `getEffectiveHostSetting(settings, LOCAL_EXECUTION_HOST_ID, 'defaultWorktreeLocation', settings.workspaceDir)`. This handler only ever answers for the local host, and a local-host override previously could not win either. A seeded value is not a user choice. `workspaceDir` is never blank -- new installs seed it with `~/orca/workspaces` -- so treating any non-blank value as configured would silently relocate every existing user's new projects into the worktree root. Worktrees nest at `<workspaceDir>/<repoName>/<branch>`, so such a project would then host its own worktrees inside its own working tree. Compare against `getDefaultWorkspaceDir(homedir())` (now exported) via `normalizeRuntimePathForComparison`, and keep `~/orca/projects` for blank, whitespace-only, and untouched-default values. Also scope the `~/orca/projects` shorthand in `formatCreateProjectParentSummary` to the fallback path itself. Otherwise a user with Workspace Directory set to `J:\PROJECTS` saw the summary line claim `~/orca/projects` while the field held `J:\PROJECTS`. Fixes #14767 * fix(settings): keep configured orca/projects paths verbatim in the create summary The collapsed Location summary used a tail match on orca/projects, so a configured directory like /data/orca/projects rendered as ~/orca/projects. Scope the shorthand to usual home layouts and pin the lookalike cases. |
||
|
|
a27c691fdd | fix(terminal): stop detached exit observers pinning evicted panes' xterm buffers (#16551) | ||
|
|
5e900b10b3 |
fix(windows): let a build with no job exports still retire a dead agent (#16563)
* fix(windows): let a build with no job exports still retire a dead agent
#16419 (
|
||
|
|
4d2dc0fae5 |
test: pin cross-version browser placement test to explicit baseline (#16554)
* test: use explicit baseline for cross-version browser placement test Pin to v1.4.184 to ensure consistent testing against the release predating client placement. This avoids coupling the legacy-baseline bump to unrelated schema refactors in newer versions. * fix(windows): treat inaccessible processes as alive in tests When checking process state on Windows, EPERM (permission denied) indicates an inaccessible but live process. Only ESRCH (process not found) proves exit. Correct isAlive() to distinguish these cases. Also add windowsHide:true to child process spawns and use explicit SIGKILL when force-killing the host process. |
||
|
|
1c9fb84b77 |
fix(worktree): stop push-target rollback deleting a sibling's remote (#16569)
A push target that reuses an existing Orca-created fork remote inherits ownership of it (`remoteCreated = isRemoteCreatedByKnownWorktree(...)`), so the final worktree to be deleted can remove it. Rollback then reused that same flag to decide whether to undo its own work -- but a reused remote was not added by this call, and a live sibling worktree is still pushing to it. A failed fetch during create therefore deleted a remote another worktree depends on. Track `remoteAddedHere` separately: ownership stays inherited for cleanup, while rollback only removes a remote this call actually added. Both the local and the SSH path had the same bug and are fixed together. Original work by Jinjing (AmethystLiang) in 616d2a4ec8c; split out of that branch so the release fix in #16550 stayed a clean cherry-pick. |
||
|
|
2f5f5ce23c | fix(ui): restore the light-mode dropdown shadow (#16570) | ||
|
|
3c5c908451 | fix(automations): scope project refs to destination host (#16552) | ||
|
|
868fc39d32 | fix(worktrees): refresh paired clients after external discovery (#16557) | ||
|
|
e4d95e032d |
fix(windows): restore a CIM fallback for relay hosts with no native binding (#16550)
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
f72dcb908e | fix(contextual-tours): stop measuring 60 times a second while nothing moves (#16453) | ||
|
|
4ff428f763 | fix(mobile): retain relay during brief backgrounding (#16543) | ||
|
|
933345d347 |
Clarify upstream divergence stats for rebased branches (#16358)
* Clarify upstream divergence stats for rebased branches When a branch is rebased, it still tracks the pre-rebase upstream while comparing against the new base. Move upstream arrows to the head line to prevent them being confused with compare-base counts. * Show upstream divergence stats independent of compare base Measure HEAD against upstream regardless of compare-base state, so divergence indicators stay visible even when comparison is missing, loading, or failed. Also use cross-platform temp paths in tests. * Show commit counts against compare base, not upstream Upstream divergence (↑/↓ against tracking branch) was confusing for rebased branches — the counts appeared beside the base ref but measured against the upstream branch. Show only the compare base count instead, on the line that names it. * Report branch divergence in both directions Rebased branches are typically ahead AND behind their base; a single count hides this case. Use symmetric range with --left-right --count to capture both directions efficiently, then expose commitsBehind in the UI alongside commitsAhead. * Use semantic names for i18n keys and template variables Rename hash-based translation keys to descriptive identifiers and replace generic value0/value1 placeholders with semantic variable names like `count` and `ref`. Improves code maintainability and makes translation strings self-documenting. |
||
|
|
07b82340f3 |
Route terminal file links to sibling workspace tabs (#16544)
* fix: route terminal file links to sibling workspace tabs Detect when a clicked file is already open in a sibling workspace and route to that existing tab instead of creating a duplicate. Reorganizes workspace activation to dispatch by both worktree id and execution host, allowing the same worktree name across different remotes to be disambiguated and routed correctly. * test: validate terminal file link opens in correct sibling worktree Enhance test to check both file path and active worktree ID, ensuring the linked file opens in the intended sibling workspace. |
||
|
|
6a3bd2a1b8 |
fix: keep tab-cycle shortcuts in sync with rendered group order (#16549)
Tab-cycle shortcuts (Ctrl+Tab) were getting out of sync with what the TabBar actually renders. When a tab hydrated into the strip before group.tabOrder was updated, it fell out of the cycle until a click. Align keyboard cycling to use the same reconcileTabOrder pass the TabBar uses, so the cycle always walks what the user sees. Fixes STA-3475, particularly in remote servers where hydration timing diverges from local. |
||
|
|
b57b812e72 |
fix(ssh): recover initial state hydration
Hydrate SSH connection states independently of best-effort tombstone labels, with bounded fanout and regression coverage. |
||
|
|
5479bd9159 |
refactor(task-page): split task page into focused modules (#15163)
* rm unused files * rm unused files * fix(task-page): clean readiness lint findings * Add GitLab IPC timeout wrapper and improve error handling - Extract GitLab timeout logic into reusable `withGitLabIpcTimeout` wrapper to protect all GitLab API calls from hanging indefinitely - Apply timeout protection to all GitLab list and fetch operations - Add error handling for GitHub and Linear issue creation operations - Fix event bubbling in GitHub work item row to prevent nested button clicks from opening detail page - Remove unused `usePRReviewCellState` hook - Consolidate redundant imports * refactor(task-page): extract components and improve provider handling - Add glab timeout handling (30s) to prevent IPC thread blocking - Extract GitHub assignee/review components to dedicated files - Improve GitLab work item row keying (repoId:id) and keyboard event handling - Add context-aware error handling for Jira creation failures - Refactor GitHubAssigneeAvatar to use shared GitHubUserAvatar component * Add timeout support and error handling for GitLab operations - Admission control times out queued work after 30s to prevent indefinite queueing behind saturated operations - Mutation errors now display to users via toast instead of failing silently * Consolidate workspace attachment labeling into unified utility Extract common label-generation logic from GitHub and Linear work-item components into a single getWorktreeAttachmentLabel function, removing duplication across attachment types. * Improve TaskPage accessibility, i18n coverage, and error handling - Add missing aria-labels, roles, and semantic attributes for improved screen reader support - Extract hardcoded UI strings into i18n system with translate() calls - Add error handling and proper abort signal support for async operations - Use locale-aware date formatting throughout - Fix pagination disabled state and reviewer suggestion merging logic - Improve async state management with proper refs and effects - Add Textarea component import for Jira dialog * Improve TaskPage accessibility and i18n key naming - Add DialogTitle/Description with i18n to Linear issue dialog - Use useId to improve aria-labelledby in GitHub selectors - Replace hash-based i18n keys with semantic names - Use Object.hasOwn instead of `in` for safer filter checks - Fix PR review cell to clear input only on success * Add missing dependencies to TaskPage hooks and useCallback/useEffect arr Fixes exhaustive-deps warnings by adding missing setters, refs, and computed values to dependency arrays. Refactors GitHub and Linear issue state handling to compute values from pageData where available, with fallback to local state. Moves imperative ref updates into useEffect to properly track dependencies. * Fix TaskPage ref timing and null repo selection state Treat null newIssueRepoId as a valid selection, and use useLayoutEffect to synchronize the provider context ref before paint rather than after. * Extract Linear issue dialog components and fix popover scroll styling - Consolidate scroll styling: apply popover-scroll-content and scrollbar-sleek classes to PopoverContent wrappers - Remove redundant max-h-60 overflow-y-auto styles from inner picker divs - Fix GitHub new issue repo selection to explicitly target first selected repo on fresh mount - Correct CacheEntry import paths from store/slices/github to store/github/cache-model - Update tests to reference extracted dialog components instead of TaskPage.tsx * Improve GitHub task page i18n and fix issue creation edge cases - Add i18n support to GitHub work item aria-labels (draft PR, PR, issue) - Optimize work item row by extracting repeated source context call - Add safety check to prevent opening detail page when issue URL is missing - Fix dependency reference in detail opener hook - Extend GitLab job trace timeouts (60s backend, 65s frontend) for slow logs * Increase GitLab job trace fetch timeouts Job traces can outlive the runner's 30-second default timeout. Extend fetch operations to allow 60–65 seconds to complete. * Verify sourceContext variable extraction in github row test Update expectations to check that sourceContext is assigned to a variable rather than called inline, matching the refactored component implementation. |
||
|
|
290f192d84 |
fix(updater): surface and degrade renderer shutdown checkpoint failures (STA-5505) (#16497)
* fix(updater): surface and degrade renderer shutdown checkpoint failures The in-app updater could refuse to install with 'Renderer shutdown checkpoint was not completed.' while the actual persist() error was swallowed unlogged, leaving users stranded on old builds (STA-5505). - report the swallowed persist error: console, crash breadcrumb, and a cross-world DOM attribute so the thrown error (and the Update Error dialog) names the underlying cause - stop failing the checkpoint on sleeping-agent quit-capture errors; the periodic capture bounds the loss to one minute - extend the existing durable-session degradation to full-session staging failures during an intentional restart, preserving the dirty-draft guard * fix(quit): degrade and surface checkpoint-vetoed app quits (#15352) Cmd+Q walked the same shutdown checkpoint as the updater: a persist() throw preventDefault()ed the synthetic beforeunload and confirmNativeWindowClose returned silently — quit accepted, nothing logged, SIGKILL the only exit. - run the quit checkpoint inside a window-close scope so full-session staging failures degrade to the durable tier for app-level closes too (dirty editor drafts still hard-block) - when the checkpoint still vetoes the quit, toast the published failure reason instead of dying silently * fix(updater): retry-then-degrade staging and honest capture-loss accounting Review findings on the first pass: - a first full-session staging failure now stays a visible, retryable error; only a repeat failure degrades to durable-only staging, so a transient IPC failure keeps its retry instead of silently dropping just-captured scrollback - the sleeping-capture comment no longer overstates periodic coverage (periodic mode skips done panes and never stamps quit origin); the swallowed failure records a crash breadcrumb - pin the exact degradable-shutdown gate expression in the source-shape test so rewiring it cannot pass silently * fix(updater): arm the staging-retry flag only for degradable shutdowns An unrelated unload's staging failure must not burn the visible first retry of a later restart or quit. * fix(updater): isolate shutdown checkpoint retries Reset full-session staging retry state when a shutdown attempt is abandoned, and route Terminal-less closes through the same scoped synthetic checkpoint as mounted workspaces. Keep arbitrary thrown-value diagnostics non-throwing and localize the quit failure toast. * fix(updater): preserve checkpoint retry lifecycle * fix(updater): preserve empty checkpoint failure reason |
||
|
|
7c2b5a2334 | ci: rename cross-platform adhoc workflow (#16536) | ||
|
|
a1ec0479e2 |
fix(windows): revalidate PTY liveness from the job object, not a forked helper (#16419)
* fix(windows): answer console membership from the job object, not a forked helper
node-pty answers "which processes are attached to this pane's console?" by
FORKING a helper, because GetConsoleProcessList must run from a process
attached to that console. Orca asked on a foreground poll, per pane, so each
read spawned a conpty_console_list_agent -- hundreds of hidden processes
exhausting RAM within minutes, respawning as fast as they were killed (#10857).
QueryInformationJobObject has no console-attachment constraint: any process
holding the job handle can ask. Orca already creates that job per PTY, and
listPtyJobProcessIds has exposed it since the W1/W2 work with zero callers.
One syscall, no children.
Semantics the three call sites rely on are preserved: a root-only set still
proves the shell is alone (so a stale agent can be retired), and size > 1 still
proves something is running under it. The single difference is that a
descendant detached from the console stays in the job -- which widens the set,
the conservative direction for every caller.
Also fixes the third call site, which returned { available: false } whenever
membership was unavailable AND a recognized agent existed -- i.e. exactly while
an agent was running. Membership only ever narrowed the candidate list, so an
unavailable answer now leaves it unfiltered instead of failing the whole
resolution.
The no-fork test is asserted through a module-level vi.mock of
node:child_process. A vi.spyOn of a require()'d child_process does not
intercept the module's own import binding: the first version of that test
passed with a fork() deliberately reintroduced.
* fix(windows): keep console attachment for the candidate filter
Readiness review caught that this PR changed two different questions as if they
were one, and the repo's own plan doc had already said so:
"The job is the wrong set here -- it would re-admit precisely the detached
process the filter exists to drop." (windows-wsl-root-cause-plan.html, Use B)
The two uses:
- Use A, `size > 1` at local-pty-provider and the daemon tracker -- "is anything
in this pane besides the shell?". The job answers this, in-process and with no
fork. Unchanged from the previous commit.
- Use B, the candidate filter -- "which of these are ATTACHED TO THIS CONSOLE?".
Its whole job is dropping a descendant that detached, and the job object keeps
those, so answering it from the job makes the filter a no-op in its motivating
case: a detached `Start-Process droid` would be granted byte authority, and a
detached sibling would make an attached agent look ambiguous.
Use B goes back to GetConsoleProcessList, in its own module named for what it
answers, with its fail-closed null restored. That path is not the #10857 storm:
it runs only when a recognized agent candidate already exists, not on every
foreground poll. Bounding it to one pooled supervised helper is the remaining
half, and per the plan doc either half alone takes #10857 from unbounded to one.
My earlier claim that widening membership is "the conservative direction for
every caller" was wrong -- true for Use A, backwards for Use B. The hardware run
did not catch it because I measured a WSL pane, where the superset is harmless,
and never a detached GUI child, which is the divergence.
* fix: restore the coverage and ratchets the module split dropped
Round 2 of review. Two blockers, both from moving the forking code to a new
file without moving what guarded it.
- The child_process import ratchet was RED: windows-console-attached-processes.ts
imports node:child_process and was unlisted, and the old entry was stale. I
never ran that suite -- lint and the providers/daemon tests both pass without
it, which is exactly the gap the ratchet exists to close. Entry repointed;
count unchanged at 159.
- The forking module had ZERO tests. Its 11 assertions -- bounded timeout,
single kill, spawn error, malformed message, helper-pid removal -- were in the
file that now answers a different question, so the module that actually caused
#10857 was shipping untested. Moved with the code.
Also: nothing pinned the round-1 fix itself. No test drove console attachment to
null and asserted the fail-closed result, so re-deleting that branch would have
gone green. Now covered, and verified to fail when the branch is removed.
Cleanups the split left behind: `consoleMembershipUnavailable`/`consoleProcessIds`
renamed to `pane*` where they now hold job membership, the duplicated
`WindowsConptyMembershipDeps` type name, comments still describing the console
on the job path, and eight reliability-gate paths pointing at the moved tests.
* fix(windows): let a superset job answer expire instead of vetoing retirement
Round 3. The job read had reintroduced #9258's bug by a new mechanism.
`size > 1` returned unconditionally, so any pane holding a console-detached
descendant never retired its cached agent. A WSL pane always holds some: the
measurement in this PR's own test recorded job [40980,104068,4888,69908] against
console [69908,40980], i.e. console said "shell alone, retire" while the job said
"three others alive, keep". #9258's third commit describes the identical failure
from the other direction -- a bare shell reading as [helper, shell] "looked like
it still had a child ... the foreground refresh held the exited agent's identity
indefinitely" -- and that is what came back.
It bites because the read branch that serves the cached name across a Windows
shell fallback is deliberately untimed: #9258 made it so on the stated assumption
that "the background refresh authoritatively retires it". Removing the retire
authority left the identity with no bound at all. Second-order: a non-null cache
makes idleNoEvidenceShell false, which pins the refresh at the 1s TTL, so an idle
WSL pane also scanned the process table every second forever.
A TTL on the read would have been the wrong fix -- untimed is deliberate, because
on Windows the fallback name is structurally uninformative. Instead the job answer
is treated as what it is: a SUPERSET of the console, which cannot tell a working
agent from a leftover. Proof of absence retires immediately (size 1, unchanged);
an inconclusive answer ages out at 30s; unverifiable (null) still holds forever
per ssh-execution-boundary.md. Only successful scans that found no agent advance
the clock -- a degraded scan returns before this -- so the fix cannot expire an
agent it simply failed to see.
Also from review:
- Restore the root requirement the forked probe had. Without it a set of one
non-root pid -- shell gone, descendant alive -- read as "shell alone, retire",
inverting the truth.
- Rename to windows-pty-job-membership.ts / readWindowsPtyJobProcessIds. The old
name still said ConPTY console while reading the job, and conflating those two
sets is precisely the bug
|
||
|
|
5e5457983a |
Add clickable See more button to palette section overflow hints (#16533)
* feat: Add clickable See more button to palette section hints Allow incremental expansion of capped sections (worktrees, tabs, projects) by clicking "See more" to reveal 20 additional entries per section. Replaces static "X more" messages with interactive expansion that resets when the query changes. * Make soft preview See more non-clickable when no rows are hidden - The soft preview hint's expand button is only actionable when rows are hidden beyond the hard cap (leadingHardOverflowCount > 0) - When all rows already render, expanding would only reshuffle already-visible content without revealing anything new - Pass undefined as the handler to prevent the click behavior in this case - Add test case to verify the button doesn't appear when all rows fit |
||
|
|
5a8b4aff7b |
perf(git): only schedule the upstream-ref poll for the repo that has one (#16443)
* perf(git): only schedule the upstream-ref poll for the repo that has one A single global binding means at most one worktree holds a selected upstream ref at a time, so every other repo's 2s poll woke only to stat an empty set. Rebinding is synchronous and in-process, so reacting to it detects a newly selected ref exactly as fast as polling did — the wake-ups were pure waste. Measured at 100 repos with no selected ref: 0.811 -> 0.320 CPU-ms/s idle. * fix(git-watch): re-read ref selection after building the poller A rebind can flip back while the poller is being constructed. The concurrent unbind sees statusRefPolling still null and correctly does nothing, so without re-reading selection the in-flight build adopts a poller for a repo that no longer holds a ref — reinstating the idle wake-ups this change removes. * fix(git-watch): fence concurrent ref-poller starts with a generation token Startup and each rebind could be mid-build at once, and the startup path adopted its poller unconditionally. A rebind that won the slot first was then overwritten without being unsubscribed, stranding that poller's timer and visibility listener for the process lifetime. The generation names which attempt still owns the slot so every loser tears down what it built. Adds a regression test driving the real binding path; it fails if the poller is dropped without unsubscribing. Reported by CodeRabbit on #16443. |
||
|
|
91a500712c |
fix(crash-reporting): see the renderer memory the heap counters never report (#16449)
* fix(crash-reporting): see the renderer memory the heap counters never report Windows renderer crash 36048e26 arrived with 618MB of private renderer memory and a `renderer_memory` breadcrumb reporting a 150MB V8 heap. Both numbers were right: xterm scrollback lives in `Uint32Array` backing stores and glyph atlases live in GPU transfer buffers, and neither is counted by `usedHeapSize`, `mallocedMemory`, or Blink's allocator. That made the report unanalyzable. `renderer_memory_highwater` is the crumb carrying the subsystem census that names what grew, and it is armed on `usedHeapSize / heapSizeLimit`. At 150MB of a 4192MB limit that ratio is 3.6% — nowhere near the 60% mark — so the census never reached a single one of these reports. Measured on Windows (6 worktrees x 4 terminal tabs, 8000 lines each, this app at 4218d505): filling 24 mounted panes moved the renderer working set from 210MB to 656MB while `usedJSHeapSize` stayed at 43MB for the whole run. Sample the renderer's own OS footprint through `process.getProcessMemoryInfo()` (available in the sandboxed preload) and: - report `privateMB`, `residentMB`, and `outsideHeapMB` — the footprint minus everything V8 and Blink admit to holding — on every `renderer_memory` crumb; - arm the highwater census on private-footprint marks (600MB / 1000MB) as well as the heap ratio, so growth outside the JS heap now carries the pane and store census that names it. The footprint read is async, so a sample annotates with the previous read and refreshes in the background: one interval of staleness is irrelevant to a footprint trend, and awaiting it would make every sample reentrant. A shell without the bridge, or a runtime that withholds the read, keeps sampling exactly as before. Retained-breadcrumb keys now distinguish the two threshold ladders; keying only on `thresholdPct` collapsed every footprint crumb onto one slot. crash-diagnostics.ts split at the max-lines budget: memory sampling moves to renderer-memory-sampling.ts and the shared payload shaping to crash-breadcrumb-data.ts. * fix(crash-reporting): retain all renderer memory marks |
||
|
|
76f7e785fc | style(github): trim repo identity cache comments (#16517) | ||
|
|
630b71730b |
fix(sleep): restore agent auto-hibernation for non-Pi agents (#16430)
* fix(sleep): let non-Pi agents hibernate again, and stop repaints resetting the idle clock Auto-hibernation could never fire for claude, codex, gemini, opencode, grok, or any other resumable TUI agent — only pi/omp/prime-agent. #10238 broadened the `origin: 'live'` resume anchor so every resumable agent keeps its `--resume` handle when a turn ends. The planner rejects any pane that already has a sleeping record, and its exemption was still Pi-only. Since the planner's eligibility conditions are the same conditions that write the anchor, that rejection covered every otherwise-eligible non-Pi pane. - Split the conflated predicate. `isLiveResumeAnchorForCompletedAgent` answers "is this record just this pane's own live anchor?" with no vendor gate; the Pi-gated wrapper keeps today's exact semantics for the manual-sleep and quit capture call sites; `isAutomaticHibernationAllowed` carries the `automaticResumeBlockedBy` fence on its own. - Fence automatic hibernation. A fenced worker must not be auto-relaunched, and the capture does not copy the flag — so hibernating one would erase it. Checked in the planner and again inside the shutdown action against freshest state, re-evaluated after the synchronous capture callback that could itself fence it. - Anchor the idle clock on `stateStartedAt`, not `updatedAt`. Same-state repaints (OSC 9999, reconnect replays) advance `updatedAt`, restarting the 30-minute countdown and invalidating the two-tick confirmation. - Floor that anchor on PTY-binding age and a boundary-resolution stamp, so a wake or app restart still gets a full idle window instead of sleeping the whole backlog on the ancient timing main replays. The boundary stamp is written synchronously where the flag clears; sampling it per tick would miss a boundary written and cleared between two samples. - Signature drops `updatedAt` and gains agent kind plus full resume identity, which is the change detection `updatedAt` was providing by accident. Splits the planner into planner / pane-eligibility / snapshot to stay under the file length limit. * fix(sleep): drain pane teardowns sequentially `runAgentHibernationTick` launched every confirmed shutdown unawaited, so a backlog fanned all of them out at once. Each shutdown re-runs a full runtime-liveness sweep (one `terminal.list` per runtime-owned worktree, 10s timeout) and then a `terminal.stopExact` (15s timeout) — so ~100 overdue panes meant ~100 concurrent sweeps plus ~100 concurrent stops plus interleaved persistence writes. On an SSH runtime that is hundreds of near-simultaneous RPCs at the relay. The fanout predates this branch, but auto-hibernation could not fire for non-Pi agents, so it never ran at scale. Restoring eligibility is what exposes it. Awaiting each teardown also makes `tickInFlight` real: it was cleared in the `finally` as soon as the promises were launched, so it never covered the drains it was meant to guard. Each candidate still re-validates against a fresh plan at its own turn, so a slow drain cannot act on stale confirmation, and per-candidate failures are already caught so one stuck teardown cannot abort the rest. * perf(sleep): scope hibernation rechecks to pane owner |
||
|
|
67f2fc9e7c | perf(github): stop re-spawning the repo-identity probe every 30 seconds (#16450) | ||
|
|
cac5388545 | Update README downloads badge | ||
|
|
d2a35eebe3 | fix(mobile): avoid unsupported Hermes array sorting (#16506) | ||
|
|
c8567eb16e | fix(sidebar): preserve hidden rows in manual order (#16488) | ||
|
|
0e0a8c943b |
fix(mobile): recover Relay connections after resume (#16498)
* fix(mobile): recover relay sessions on resume * fix(mobile): expedite relay retry on app resume * fix(mobile): keep relay reconnect controller under lint limit * fix(mobile): rebuild relay client after pairing rejection * refactor(mobile): keep relay reconnect policy under lint limit |
||
|
|
efa3b972c2 | fix(native-chat): prevent duplicate mobile prompt echoes (#15656) | ||
|
|
29f1f4e545 |
test(perf): warm palette matcher before timing
Warm the palette matcher before measuring steady-state p95 performance. |
||
|
|
a9781a4118 |
STA-4150: client-hosted remote browser (consolidated) (#15448)
Co-authored-by: Jinwoo-H <jinwoo@stably.ai> |
||
|
|
98bdd653ab |
fix(native-chat): stop the spinner on a not-yet-flushed transcript (#16493)
* fix(native-chat): stop the spinner on a not-yet-flushed transcript A brand-new agent session can take minutes to write its first JSONL line, and one that is never prompted never writes it at all. The host emitted no stream frame until the file resolved, so every native-chat client sat on a bare spinner with the composer enabled but the transcript blank -- forever, in the never-prompted case. The resolve poll now reports the transcript as pending after a short grace, and both host handlers emit a `pending: true` snapshot. It is deliberately not a plain empty snapshot: an empty window sold as a settled read would capture over retained history and unblock consumers that require a trustworthy transcript (the launch-draft adoption would re-offer a prompt the agent may already have taken). Clients render it as the "start a chat" empty state while keeping the read unsettled -- `awaiting-transcript` on mobile, an `awaiting` read phase on desktop, which also stops the seed loop expiring into an error card for a session that is simply new. New optional field only, so older clients ignore it and still stop spinning. * fix(native-chat): negotiate pending transcript frames |
||
|
|
bc98655a39 |
fix(remote): stop an empty host inventory settling the mirror (#16414)
* fix(remote): stop an empty host inventory settling the mirror An inventory with zero published snapshots satisfied `settles.length === fullInventory.publishedSnapshotCount` as `0 === 0` and fired the environment-wide host-mirror verdict with no host evidence behind it. A live relay/SSH-paired host answers exactly that until its renderer's first publish, so the drained resume sweep forked a second agent onto a PTY the host was still running. Gate that one case behind a `terminal.list` readiness probe: it reads the PTY controller, not the session-tab mirror, so it sees live PTYs the mirror has not published. Only "no terminals" settles; live or unverifiable leaves waiters parked for the next inventory or per-worktree frame — a host with genuinely zero terminals still settles, so panes do not park forever. Fixes STA-5377 * fix(remote): fence host readiness probes by generation * fix(remote): preserve legacy terminal probe fallback |
||
|
|
61c7b51c8c |
docs(AGENTS): add code-reuse guidance and verification commands (#16451)
- Add "Reuse Before Reimplementing" section guiding developers to check for existing implementations before writing new code - Add "Verifying Changes" section with quick reference for typecheck, test, and lint commands - Fix typo: "Non-obviosu" → "Non-obvious" |