Skip to content

[upstream #10183] fix(win): tree-kill local PTYs so ports free on terminal stop - #39

Closed
innocarpe wants to merge 371 commits into
mainfrom
fix/windows-terminal-port-tree-kill
Closed

[upstream #10183] fix(win): tree-kill local PTYs so ports free on terminal stop#39
innocarpe wants to merge 371 commits into
mainfrom
fix/windows-terminal-port-tree-kill

Conversation

@innocarpe

Copy link
Copy Markdown
Owner

Portfolio mirror of my contribution to upstream stablyai/orca.
Exhibition only — the real review/merge target is upstream.

Upstream

Summary

Summary - On Windows, every local PTY stop runs taskkill /T /F on the ConPTY root before shell kill, so npm run dev / Vite children release listening ports. - Ports panel kill also tree-kills on Windows instead of single-PID process.kill. - POSIX keeps agent-only descen

Note

  • Do not merge this into innocarpe/orca main until the upstream PR is merged.
  • After upstream merges: sync fork from upstream, then close this mirror PR.
  • This open PR exists so visitors see in-flight work on this fork's Pull requests tab.

@innocarpe

Copy link
Copy Markdown
Owner Author

Sync update (be844668a)

Address CodeRabbit: verify process is gone after Windows tree-kill before Ports kill reports success.

@innocarpe

Copy link
Copy Markdown
Owner Author

Sync update (f8985b553)

Address CodeRabbit: only ESRCH counts as process-gone after Windows tree-kill; EPERM is failure.

nwparker and others added 27 commits July 23, 2026 18:35
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
…dy (stablyai#10157)

An SSH connect whose relay deploy fails permanently (e.g. a dev build
missing the platform relay package) drove an unbounded reconnect loop.
doConnect creates the relay session ('idle'), then connectionManager.
connect() drives the raw transport to 'connected' before the relay
establishes. That premature 'connected' was forwarded to the renderer,
which treats it as "host fully up": applySshConnectionStateChange bumps
tab.generation for stranded remote panes -> remounts TerminalPane -> the
pane connect gate re-invokes window.api.ssh.connect -> restarts the whole
cycle forever (and spams connected-gated reads that fail with "Remote
connection dropped").

Hold the premature 'connected' at 'deploying-relay' in onStateChange until
the relay session is 'ready'. doConnect still broadcasts the authoritative
'connected' directly after establish() succeeds. Gate on connectInFlight so
the hold is scoped to a live connect and never wedges a stray transport-blip
'connected' on a session left 'idle' by a relay version mismatch.
…yai#10246)

* fix(terminal): hide SSH error toast under the reconnect banner

The z-50 TerminalErrorToast was stacking over the non-blocking SSH reconnect
banner with the raw ssh:connect failure. Suppress the toast while the banner
owns recovery and clear matching toast text so it cannot flash after reconnect.

* fix(terminal): strip only SSH-owned lines from aggregated terminal error

onPtyError newline-joins multiple PTY errors into one string, so the prior
startsWith() classification misfired on aggregated errors: an unrelated error
before the SSH failure left the stale ssh:connect text to flash after reconnect,
and an SSH-first error discarded any unrelated error. Classify per line and drop
only reconnect-owned lines, keeping the rest. Closes CodeRabbit's aggregation edge.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
…yai#8991)

Preserve OMP session identity and exact resume paths across cold restoration, AI Vault, mobile, WSL/SSH, and host-authority routes. Add mixed-version fallback and completed-session recovery coverage.
…blyai#9973)

* fix(terminal): re-verify cached macOS login-preflight rejections

A conclusive PAM rejection was cached for the process lifetime, so one false
verdict (the probe runs over pipes, not a PTY) disabled the login(1) TCC
attribution wrapper for a daemon that survives app quits and updates for
weeks — reintroducing the every-invocation AppData prompts stablyai#7003 fixed.
Rejections now re-verify after 30 minutes; accepted verdicts still cache
for the process lifetime.

Refs stablyai#9756

* fix(daemon): replace hosts with stale login preflight cache

Protocol 26 shipped the process-lifetime PAM rejection cache. Preserve its live sessions as a legacy generation, but route fresh terminals through protocol 27 so updating actually loads the expiring-cache fix.

Refs stablyai#9756

* fix(terminal): validate rejected login probes under a PTY
…10266)

* fix(worktree): match created worktrees through symlink roots

On immutable Linux, /home is often a symlink to /var/home. git worktree
list reports the realpath while Orca still holds the /home request path,
so creation failed with "Worktree created but not found in listing".

After local worktree add, fall back to realpath when string comparison
misses. Keep WSL listings on string comparison only (host realpath is not
authoritative there).

Closes stablyai#10170

* test(worktree): harden symlink reconciliation authority

* fix(worktree): reconcile creation by Git branch identity

* test(worktree): reproduce symlink-root listing with real Git

* test(worktree): cover cross-platform reconciliation

* fix(worktree): keep reconciliation main-only

---------

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
…blyai#10176)

* fix(workspace-board): sync Linear on context-menu Move to Status

The board's right-click "Move to Status" only wrote the local
workspaceStatus and silently dropped the Linear sync that drag-and-drop
performs. Thread an onAssignWorkspaceStatus callback from the drawer
through the kanban card chain into WorktreeContextMenu so the menu
funnels through the same local-first + Linear-sync path
(moveWorktreesToStatus) as drag-and-drop. Outside the board (sidebar
list) the menu keeps its local-only behavior.

* test(workspace-board): guard context-menu Move to Status routing

Extract the context-menu status-assign routing into a pure
planWorkspaceStatusAssignment helper (behavior-preserving) and unit-test
it, so the board Linear-sync vs sidebar local-only branch — the exact
path stablyai#10175 regressed on — cannot silently flip back unnoticed. Covers
board-sync-forwards-all-ids, local-only-writes-only-changed, and the
same-status no-op case.

Addresses code-review finding: the added drawer tests exercised the
sync wiring via a mocked LaneGrid but never the menu's routing branch.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: ElNelyo <ElNelyo@users.noreply.github.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
…e (crashes d2c1da69, bb74236c) (stablyai#10271)

Co-authored-by: Orca <help@stably.ai>
…n minimize→restore (stablyai#10158)

* fix(terminal): stop the reveal fit from reflow-garbling inline TUIs

grok (and other inline-viewport TUIs like Codex) render garbled after the
floating terminal is minimized and brought back up. On reveal the resume path
fit xterm synchronously right after re-attaching WebGL, whose cell metrics
differ from the DOM renderer's — so it could propose a one-column-off grid,
reflow xterm, then snap back a frame later (a net-zero resize "wiggle"). xterm's
main-buffer wrap→unwrap is not a perfect inverse, and an inline TUI that
diff-paints its pinned region redraws over the corrupted buffer.

Replace the unconditional synchronous reveal fit (fitAllPanes) with a gated fit
(PaneManager.fitAllPanesStable → fitRevealedPane):
- fit synchronously only when the fit element's pixels actually changed while
  hidden (a real resize the app must reflow for anyway, kept ahead of the async
  PTY size reassert so it can't forward a stale grid);
- if the pixels are unchanged but the grid diverged while hidden (snapshot /
  SSH-reattach direct terminal.resize, or an appearance/DPI change), repair it on
  a steady grid (requestStablePaneFit) so a sustained mismatch refits while a
  transient cell-metric wobble does not reflow;
- otherwise leave the pane alone.

The common minimize→restore is now a hard no-op with zero reflow. Also applied to
the window-wake reveal path.

* refactor(terminal): tighten reveal-fit comments + rename to fitAllRevealedPanes

Quality pass: make fitRevealedPane the single canonical explanation of the
reveal wobble and reduce the duplicated comments at the call sites to short
pointers; rename PaneManager.fitAllPanesStable -> fitAllRevealedPanes ("stable"
only described one of its three branches); symmetric early-returns in
fitRevealedPane. No behavior change.
…er workspace (stablyai#10252) (stablyai#10268)

* fix(worktree): don't path-sweep sibling sessions when deleting a folder workspace (stablyai#10252)

Deleting one folder-workspace instance could kill terminal/agent sessions in
OTHER workspaces sharing the same checkout path — sibling instances, and even
worktrees of a different repo rooted under that directory. Both pi and Claude
Code agent sessions died at once with no recovery.

The `cwdOwned` path fallback in killAllProcessesForWorktree() derives its match
path via splitWorktreeIdForFilesystem(), which strips the `::workspace:<uuid>`
suffix and collapses a folder instance's path to the shared checkout dir. Every
untagged session under that dir then path-matched and got swept (worst case: a
home directory registered as a folder repo).

Disable the path fallback for folder-workspace instances — their filesystem
path can't identify a single instance. The exact `${worktreeId}@@` prefix and
authoritative `session.worktreeId` matches (both carrying the instance uuid)
still tear down the deleted instance's own sessions; normal git worktrees
(unique paths) keep the fallback. The runtime and registry sweeps already
matched by exact worktreeId.

Adds isFolderWorkspaceInstanceId() and regression tests. See
docs/delete-workspace-cwd-owned-sibling-kill.md.

* rm design doc
…i#10193)

* fix(remote): create paired agent sessions without host focus

* test(remote): assert structured resume request

* test(remote): preserve provider-separated resume coverage

* test(remote): assert paired agent focus authority

* fix(remote): separate agent host creation from viewer focus

* test(remote): harden agent-session authority validation

* test(remote): validate retired pane identity

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix: route folder workspaces in worktree operations (stablyai#10251)

Folder workspaces are not Git worktrees and never appear in the repo/worktree
catalogs, so they were falling through to unresolved cross-host routing and
failing closed on all owner-dependent operations. Extract folder workspace
ownership logic to a dedicated module and add dedicated routing for folder
workspace identifiers before checking Git worktree catalogs.

* persist folder workspace metadata on the FolderWorkspace record

Folder workspaces lack worktreeMeta rows; metadata updates (activity bumps, unread status, terminal focus) must call updateFolderWorkspace. Fixes routing so local folder workspaces resolve to 'local' even when unrelated runtimes exist (stablyai#10251).

* Fix folder workspace mutations routing through owners

Folder workspace updates and deletions were routing through the currently
focused runtime instead of the owning runtime. Add coordinators for
concurrent-update race prevention and activity-persistence coalescing.
Handle runtime-owned folders in editor file operations and terminal activity
tracking.
Folder workspaces are a first-class workspace type that all changes must consider
alongside git worktrees. Document this requirement for developers.
Point localized READMEs and in-app Android download CTAs at
mobile-android-v0.0.32 (English README and orca-site were already updated).
…7650)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… branches (stablyai#7654)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
brennanb2025 and others added 27 commits July 26, 2026 20:51
…tings order (stablyai#10801)

The legacy Codex session-id rescan (used when a persisted record has no
transcriptPath) returned the FIRST trusted home holding a rollout with that id.
That home becomes the pane's CODEX_HOME — i.e. it picks the ACCOUNT — and the
list ended in per-account homes ordered by settings INSERTION order, so the
account was decided by whichever one the user happened to add first.

Ranks instead: selected account -> real system home -> shared runtime mirror ->
everything else by normalized path. Both ranking inputs are required (an
optional one would silently degrade to pure path order), and the selection
arrives as a thunk so the common provenance-present resume never stats the
ownership marker for a ranking it never runs.

The mirror needs its own tier: prepareLegacySharedCodexSessionResume's guard
only fires when the resolved home IS the mirror, and that guard is what
migrates the rollout into ~/.codex. Without it a system-default selection
silently resumed under an arbitrary account and stayed pinned there permanently
once the hook stamped a transcript path.

Reviewed over two independent rounds; every tier individually mutation-proved.
Live-validated in a real Orca dev build by reading the spawned PTY's actual
CODEX_HOME across three builds (head, base, and head-minus-the-mirror-tier).

Note: fixes none of stablyai#10757's user-visible symptoms on its own — it is a
correctness precondition. Verified on macOS only; Windows coverage is
fixture-only.
…lyai#10814)

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
…lyai#10872)

Closing Ctrl+F left one match highlighted until the window was minimized
and restored.

xterm's DecorationService keys its SortedList on `decoration.marker.line`,
but `SortedList.delete()` only records an index and defers compaction,
while `Marker.dispose()` sets `line = -1` — mutating that same sort key.
After the first disposal the array is no longer sorted, so the binary
search inside `delete()` can miss a decoration that is present. It returns
false, `onDecorationRemoved` never fires, and the decoration stays live and
keeps painting. Repaints don't help; they faithfully re-paint a live
decoration, which is why only a window cycle appeared to fix it.

`clearDecorations()` disposes the active match before the match
highlights, which is exactly the order that trips this.

Patch `delete()` to retry once after compacting pending deletions, on the
miss path only, so the common bulk delete keeps its O(log n) search and
deferred batching. A 3000-trial randomized differential against upstream
semantics shows no behavior change for well-ordered lists.
…rkers, marketplace v0 (experimental) (stablyai#8549)

* feat(plugins): Orca plugin system — kernel, content packs, panels, workers, marketplace v0 (experimental)

Adds Orca's experimental plugin system behind a settings flag: a
supervised kernel, declarative content packs (VM recipes, commands and
keybindings, language packs), sandboxed iframe panels, forked worker
hosts, and a Git-backed marketplace v0 with consent, provenance and
kill-list enforcement.

Theme, icon-theme and terminal-theme contributions are deferred to a
follow-up pass.

* fix(plugins): make unsupported marketplace listings unreachable by key

findPlugin() backs preview/install/previewInstalledUpdate via
requireListing(), so filtering only listPlugins() hid the catalog card
while leaving the dead install path reachable one click later.

* fix(plugins): fan Pi session-only status out to plugin subscribers

The providerSessionOnly early-return in applyNormalizedStatus emitted to
onAgentStatus (main-window fanout) but skipped enrichedStatusListeners, so
plugins subscribed to agent.status.changed silently missed every Pi
session_start event. Route both emit sites through one helper so a future
early return cannot drop the plugin tap again.

Co-authored-by: Orca <help@stably.ai>

* plugins: drop dead code and hoist duplicated trust-boundary patterns

Cleanup pass over the P1 diff, no behavior change:

- Delete `readPluginTreeSnapshot`/`readSnapshotFile` and their types, plus
  the now-vestigial `directories`/`signal` plumbing in `collectFiles`.
- Delete `resolveContainedPluginDirectory` (no callers).
- Delete `plugin-content-load-pool.ts`; it reimplemented the existing
  `mapWithConcurrency`, whose index arg also removes the pairing wrapper
  in `buildPluginList`.
- Hoist `PLUGIN_CONTENT_HASH_PATTERN` and `PLUGIN_COMMIT_PATTERN` into
  the install-lockfile module; 11 sites hand-rolled these identically.
- Point the new reliability gate at the PR instead of gitignored docs
  paths, matching every other gate's link form.

* fix(plugins): retry plugin state renames on Windows AV/EPERM locks

Six plugin write paths (lockfile, provenance, current pointer, kill
list, marketplace cache, staged install dir) did a plain rename, so an
antivirus or indexer holding the target open surfaced as a failed
install. The repo already retries this hazard for issue stablyai#1507, but only
through a sync helper; these paths are all async.

Adds one bounded async retry + atomic write used by all six, and trims a
consent-provenance header that restated its own JSX.

* test(plugins): cover the Windows rename retry path

The retry loop shipped untested: both existing cases hit the non-retry path,
and the temp-cleanup test passed identically with the `finally` removed.
Mock `rename` to queue errno codes so CI can exercise locks it cannot provoke.

Co-authored-by: Orca <help@stably.ai>

* fix(plugins): pin bundled plugin resources to LF

Windows CI checks out with autocrlf, so the byte-hashed launch tree arrived
as CRLF and verify-packaged-plugin-resources rejected it — the packaged build
could never pass on Windows. Reproduced locally: CRLF yields the exact CI
error, LF verifies clean. Files are already LF, so nothing renormalizes.

Co-authored-by: Orca <help@stably.ai>

* test: guard the bundled-plugin LF pin against a CRLF checkout

The byte-hash mismatch only surfaced in Windows packaging CI. Assert the
.gitattributes pin and that a CRLF tree is rejected, so a regression fails
on any platform instead of waiting for a packaged Windows build.

Co-authored-by: Orca <help@stably.ai>

* ci: trigger packaged-build check on bundled plugin resource changes

The launch tree is byte-hashed during packaging, but no trigger path covered
it — so the CRLF fix for that check would not have re-run the check. Add the
resources, verifier and .gitattributes paths that can break packaging.

Co-authored-by: Orca <help@stably.ai>

* perf(plugins): rebuild the panel frame only when its baked theme values change

The revision keys the panel iframe, so every bump destroys the sandboxed
frame and its in-panel state. It counted root attribute mutations, but
--workspace-sidebar-live-width is written every rAF of a sidebar drag, so
dragging with a panel open blanked it ~60x/sec. Compare the two values the
shell actually bakes in instead.

Co-authored-by: Orca <help@stably.ai>

* test: stop pinning a plugin name in the CRLF guard

The CRLF case rewrites every launch file, so the reported mismatch is
whichever plugin sorts first. P2 adds theme plugins that sort ahead of
orca-navigation-shortcuts, which broke the assertion there.

Co-authored-by: Orca <help@stably.ai>

* style: drop stray blank lines left by the rebase resolutions

Both sides of the agent-hooks and orca-runtime conflicts contributed a
trailing blank, which oxfmt rejects. Whitespace only.

Co-authored-by: Orca <help@stably.ai>

* test(plugins): stop the startup budget failing on machine load

P95 runs 16-34ms idle but exceeds the 50ms bound under full-suite
parallelism, so the gate flaked. Widen it to catch an order-of-magnitude
regression instead; the no-worker/no-plugin-code assertions are the real
guarantee. Verified a 400ms regression still fails.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
…8255)

* fix(remote): accelerate shared-control and pane recovery on resume/online

Narrow stablyai#8255 onto current main after stablyai#9774: fire pending shared-control
reconnect timers and pane recovery backoffs on system resume and browser
online, without replacing the per-pane recovery state machine or reconnect
banner UX.

* test(remote): cover online and occluded-resume recovery triggers

* fix(remote): centralize recovery acceleration

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(mobile): block iOS uploads below the last shipped App Store version

The closed-train guard looked up each candidate version's own App Store
record, but a version only gets one once it is submitted for review.
0.0.34 reached TestFlight and was never submitted, so it had no record,
nothing looked closed, and the patch-bump walk stopped there — while
0.0.35 had already shipped. Apple rejected the upload after a 24-minute
build (90186 closed train, 90062 needs a higher CFBundleShortVersionString).

Fetch the highest closed version once and treat everything at or below it
as closed, comparing semver numerically so 0.0.10 outranks 0.0.9.

Also read appVersionState alongside appStoreState: the latter is
deprecated in App Store Connect API 3.3 and renames the shipped state to
READY_FOR_DISTRIBUTION, so reading only the old field would silently find
zero closed versions once Apple stops populating it.

* chore(mobile): prepare 0.0.36

app.json sat at 0.0.32 while 0.0.35 shipped on the App Store, because
release versions are resolved on the runner and never committed back.
Close the four-version drift so the checked-in version matches reality
and the iOS release no longer depends on the closed-train walk to find
an open version.

Bump Android versionCode 8 -> 9 in the same commit: the version is shared
between platforms, and shipping 0.0.36 with the code that already shipped
for 0.0.32 produces an APK that cannot install over the released build.
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
…acter (stablyai#10866)

* perf(agent-status): strip terminal control bytes by run, not per character

stripTerminalControl built its result with a per-character `+=`, allocating a
fresh string for every retained character. The Command Code status detector
calls it four times per PTY chunk — the scan text, the chunk-boundary variant,
and both previous-text lengths — so an agent pane paid that on every write.

Control bytes are sparse in real output, so copy the spans between them instead:
2.3x-2.6x from 5 KiB to 106 KiB chunks. Output is byte-identical, checked
exhaustively over every string up to length 4 across a 13-symbol control/unicode
alphabet plus 200k random strings (224,831 inputs, 0 mismatches).

* docs(agent-status): condense the run-copy rationale comments

Review feedback: both comments walked through the implementation. Keep one line
of non-obvious rationale each, per the repo's comment guidelines.

Co-authored-by: Orca <help@stably.ai>

* test(agent-status): correct terminal strip benchmark

* test(agent-status): bound terminal strip benchmark

---------

Co-authored-by: Orca <help@stably.ai>
* fix(terminal): restore link hover after mouseleave

* test(terminal): verify mouseleave listener cleanup

* test(terminal): assert link hover listener wiring
On Windows, ConPTY shell-only kill left npm/dev-server children listening,
so switching projects could still serve the previous app on the same port.

- taskkill /T /F via killWithDescendantSweep for every local PTY stop on Windows
- Ports UI kill path also tree-kills on Windows
- Keep POSIX agent-only descendant sweep; plain POSIX terminals unchanged

Closes stablyai#10150
Related: overlaps Windows agent path of stablyai#10004 / stablyai#10100
terminateWindowsProcessTree is best-effort and always resolves so PTY
teardown is never blocked. Ports UI needs a real outcome: after taskkill,
probe liveness with process.kill(pid, 0) and return failure if the PID
is still alive.

Addresses CodeRabbit review on stablyai#10183.
EPERM from process.kill(pid, 0) means the process is still alive without
signal rights; do not report Ports kill success in that case.

Addresses CodeRabbit follow-up on stablyai#10183.
… rebase cleanup

No code change. Re-opens the contribution for review after branch recovery.
… tests

CodeRabbit: platform overrides leaked when expects threw before the
end-of-test restore; wrap bodies so cleanup always runs.
Address CodeRabbit: TerminateProcess can leave a PID queryable until
handles drain; probe a few times before reporting failure. Platform
restore already runs in finally via withPlatform.
@innocarpe
innocarpe force-pushed the fix/windows-terminal-port-tree-kill branch from 274473f to 1ad4b84 Compare July 27, 2026 15:24
@innocarpe

Copy link
Copy Markdown
Owner Author

Upstream stablyai#10183 closed without merge — closing portfolio mirror.

@innocarpe innocarpe closed this Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.