ci: validate rebased upstream PR 8480 - #3
Closed
bbingz wants to merge 1 commit into
Closed
Conversation
Owner
Author
|
All executable code gates passed for base 371d71a and head 0eabed9. Closing this temporary CI mirror; results are linked from upstream PR stablyai#8480. |
5 tasks
bbingz
pushed a commit
that referenced
this pull request
Jul 22, 2026
…ablyai#9506) * fix(editor): don't flag editor-initiated moves as changed-on-disk An in-app move/rename (explorer drag-drop, inline rename, tab rename) re-homes the open tab to the new path and carries its unsaved draft forward. The move also physically relocates the file, which the worktree watcher reports as delete(old)+create(new) a few ms later. Because the tab already lives at the new path by then, that create echo was treated as an external write landing on a dirty tab and raised a spurious "changed on disk" banner. Add a short-lived self-move registry (the move analog of the existing self-write registry) stamped at the single remap choke point, and have the external-watch handler recognize the move's own watcher echo: suppress the changed-on-disk mark on the re-homed dirty tab and the tombstone on the source path. Genuine external edits are unaffected. Covered by unit tests for the registry, the remap recorder, and the watch-hook suppression (plus a no-over-suppression guard). Co-authored-by: Orca <help@stably.ai> * refactor(editor): harden move-echo suppression per adversarial review Addresses review findings on the self-move suppression: - Stamp the self-move at the call sites BEFORE the on-disk rename (recordSelfMoveForOpenTabs), not after the tab re-home, so the watcher echo can't win the race. This makes the source-side delete guard actually effective and removes a possible one-frame flash. - Suppress only the move's own create echo, not update events, so a genuine external write to the moved path within the TTL still raises the changed-on-disk banner (closes an over-suppression gap). - Track source/target roles independently per path so an immediate undo can't clobber the original move's still-in-flight stamp. - Raise the registry cap above realistic bulk-move sizes so a large directory move never self-evicts its own not-yet-echoed stamps. Updated + added tests: registry undo/role + cap, the new call-site helper (incl. directory move), and a real-update-still-marks case. Co-authored-by: Orca <help@stably.ai> * fix(editor): make move-echo suppression watcher- and TTL-robust Round-3 hardening after adversarial review: - Suppress the move's own watcher echo regardless of event kind. The main-process watcher coalesces a create+attr-change burst into a lone update, so a create-only gate let the echo through on some hosts and re-exposed the false banner. Suppression is now bounded by the self-move TTL; a genuine write to the exact path within that short window is the documented trade-off (draft is preserved regardless). - Bracket the on-disk rename with the self-move stamp via a single renameOpenTabsPathOnDisk wrapper: stamp before (to beat the watcher), re-stamp on success (a slow SSH/runtime rename can outlive the TTL, so the fresh window must start when the file actually moved), and clear on failure (a rename that never happened must not suppress real events). All move entry points (explorer move, inline/tab rename incl. undo/redo, untitled rename) route through it. - Treat a tab as remote for TTL purposes when it has a runtime owner OR an SSH worktree connection (an SSH tab can carry a null runtime owner). - Registry tracks source/target roles independently per path so an immediate undo can't clobber the original move's in-flight stamp; cap raised above realistic bulk-move sizes. Tests: registry role/clear/cap, the rename wrapper (success re-stamp + failure clear), the call-site helper (dir move + clear), and watch-hook coalescing-robust suppression + post-TTL surfacing. Co-authored-by: Orca <help@stably.ai> * fix(editor): refcount self-move roles so a failed move can't clear a live one Two concurrent moves onto the same destination both stamp that path as a target; if the second rename fails and clears, it must not erase the first (successful) move's still-live target stamp. Reference count each role's registrations and clear only the failed move's own contribution. Adds a regression test for the shared-destination case. Co-authored-by: Orca <help@stably.ai> * fix(editor): give self-move stamps per-registration expiries + retract tokens The refcount model used a single shared expiry scalar per role that only grew via max() and reset at refs=0, so releasing the max-contributing registration left survivors inheriting an over-extended window (and an expired registration could be resurrected by a later stamp on a key the opposite role kept resident). Both over-suppress genuine changes. Model each stamp as an independent registration carrying its own expiry (a list per role). recordSelfMove returns a ticket; clearSelfMove retracts exactly that registration. A role is live while any of its registrations is unexpired, so concurrent stamps, failed-move clears, and expiry are all precise. Wrapper/helper thread the tickets through. Adds regressions for the over-extension and resurrection cases. Co-authored-by: Orca <help@stably.ai> * test(editor): cover source-side self-move guard in the pre-remap ordering Adds the case where the watcher's delete(old) arrives while the tab is still at the old path (before remap re-homes it): with a live self-move source stamp the tombstone must be suppressed. Pairs with the existing naked-delete control (no stamp → deleted) to pin the guard's behavior. Co-authored-by: Orca <help@stably.ai> * docs(editor): document the failed-move suppression window as a bounded trade-off A self-move stamp is placed before the rename and retracted if it fails, so an event consumed during the pre-failure window is swallowed. Note that this only matters for the rare unrelated-dirty-tab-at-destination case and the recoverable missed-source-tombstone case, and that a move has no bytes to echo-verify the way self-writes do. Co-authored-by: Orca <help@stably.ai> * feat(editor): decide move echo vs external write by content identity Replaces the time-bounded self-move suppression heuristic with a correct-by-construction identity check, so a genuine external write to a just-moved path is never swallowed and the move's own echo is never a false conflict — regardless of watcher event-kind coalescing or timing. - Remap now carries the edit-session identity (lastKnownDiskSignature, externalMutation, pendingDiskBaselineVerification) onto the re-homed tab. Previously the close+reopen dropped it, so a moved tab lost its disk baseline (and any pre-existing changed-on-disk conflict silently vanished on move). - On a live self-move-target dirty event the watch hook now reads the destination and compares getDiskBaselineSignature(disk) to the tab's carried baseline: equal => move echo (suppress), differ/binary => genuine write (banner). Autosave is suspended synchronously before the read so a write landing mid-read can't be overwritten; a generation token makes overlapping reads safe. Fails CLOSED (marks changed) on a missing baseline or read error — never blind-suppresses. - The self-move registry now only scopes WHEN to verify. The source-side delete still can't be content-verified (nothing to read), so it stays a bounded, documented suppression. - Trim the verbose Why-comments across these files to 1-2 lines. Adds content-identity verification tests (echo/differ/no-baseline/read- error/binary/autosave-gate) and a remap test for the carried identity; splits the watch-hook suite to stay under the max-lines limit. Co-authored-by: Orca <help@stably.ai> * fix(editor): give live move-verification its own autosave gate The live self-move echo verification reused pendingDiskBaselineVerification as its autosave gate, but that field is also the always-mounted restored-tab conflict scanner's work queue: the scanner scans any pending dirty tab, launches its own read, and clears the flag without checking the live generation — so it could lift the gate mid-read and let autosave overwrite a genuine external write. Give live verification a dedicated pendingLiveDiskVerification field (both suspend autosave; each cleared only by its owner). Transient, not persisted, not carried across a re-home. Co-authored-by: Orca <help@stably.ai> * feat(editor): atomic rekeyOpenFilesForPathChange store action (move restructure stage 1) Foundation for treating an Orca-owned move as an in-place retarget of the open edit session (not close+reopen), per the locked design. One commit-only store update migrates every path-derived id + all id-keyed state (openFiles full-spread, the 6 file-id maps, activeFileId(+byWorktree), tabBarOrder, unified tabs/groups via the now editor-family-widened migrateHydratedEditorTabsAndGroups, pendingEditorReveal, untitled consume). Preflight fails closed on collision (never merges two live sessions) or stale with zero mutation. Not yet wired to a coordinator (stage 4). Stage 1 of 5; behind the shipped content-identity fix. Co-authored-by: Orca <help@stably.ai> * feat(editor): op-scoped in-flight move registry + source integration (stage 2) editor-path-move-inflight.ts tracks Orca-owned moves for the exact duration of the rename+rekey (no TTL): source paths suppress the delete tombstone, target paths latch a destination event seen before the rekey (never suppress). Wired into the watcher's delete filter alongside the old TTL registry (OR fallback) so suppression keeps working until the stage-4 coordinator drives every move through beginEditorPathMove. Stage 2 of 5. Co-authored-by: Orca <help@stably.ai> * feat(editor): move-echo provenance + autosave gate on OpenFile (stage 3 store) Adds pendingSelfMoveEcho {operationId,targetPath} to OpenFile and has the rekey action install it + pendingLiveDiskVerification on dirty autosave-capable destinations (moveOperationId arg), atomically in the same commit that re-homes the tab — so the verify gate survives the rekey and its op-id token supersedes a stale in-flight verification. Replaces the module-scoped generation map (which broke under rekey). Verification-reader wiring + coordinator follow. Co-authored-by: Orca <help@stably.ai> * refactor(editor): remap moves via atomic in-place rekey, not close+reopen (stage 4a) remapOpenEditorTabsForPathChange now builds an owner-aware rekey plan (plain-path id to the first owner, owner-qualified to the rest; previews resolve their source to the moved edit's new id) and applies it via rekeyOpenFilesForPathChange in one commit — preserving the full OpenFile + cursor/view/group/MRU state and closing the close/reopen watcher-race window. Passes moveOperationId through so dirty destinations get the content-verify gate. 4545 tests green. Co-authored-by: Orca <help@stably.ai> * feat(editor): move coordinator drives rename/drag/undo/redo/untitled (stage 4b) executeOpenEditorPathMove is the single transaction for every in-app move: quiesce affected saves -> op-scoped begin (per runtime owner) -> on-disk rename -> atomic in-place rekey (installs the content-verify gate/provenance) -> settle -> re-verify any destination echo latched before the rekey. On failure the store is untouched. Verification now triggers off the on-OpenFile provenance (consumed on resolve) and the watcher latches pre-rekey destination events. Wired into all five call sites; old renameOpenTabsPathOnDisk + separate remap removed from them. 2751 tests green. (TTL self-move registry now dead; removed next.) Co-authored-by: Orca <help@stably.ai> * refactor(editor): remove the dead TTL self-move registry (stage 4c) The coordinator + op-scoped in-flight suppression + on-OpenFile provenance fully replace the time-bounded self-move registry, so delete it and its two helper modules (record-self-move-for-open-tabs, rename-open-editor-tabs-path). The watcher source filter now uses only isActiveMoveSourcePath and the verification trigger only the tab's pendingSelfMoveEcho. Rewrote the self-move test suite onto the new primitives. 7225 tests green. Co-authored-by: Orca <help@stably.ai> * test(editor): end-to-end coordinator move test (stage 4 done) executeOpenEditorPathMove renames on disk, retargets the session in place with draft/dirty/baseline preserved + gate/provenance installed, settles the in-flight transaction; on rename failure the store is byte-identical and the transaction is released. Co-authored-by: Orca <help@stably.ai> * feat(editor): mirror-safe move — detach moved mirrored tab + close-notify host (stage 5) The atomic rekey changes a tab's id, so a moved mirrored tab would be culled by the host snapshot (losing its draft) or resurrect the old path. Ship the safe minimum: the rekey detaches a moved tab from the host mirror (mirroredFromRuntimeSession cleared) and the coordinator close-notifies the host's old-path tab (close intent suppresses re-mirroring). Prevents the data-loss/resurrection; the moved tab becomes companion-local. The full host-rekey path-change protocol (preserving mirror ownership) is a documented follow-up. Co-authored-by: Orca <help@stably.ai> * fix(editor): address review round 1 (4 majors) - Coordinator now propagates the rekey result: a collision/stale AFTER a successful on-disk rename triggers an inverse rename + throws, instead of reporting success with the source tab stranded at a vanished path (#1). - Cross-worktree: affected set spans all worktrees at the source path, sub-ops scoped per (worktree, owner), and the rekey partitions tab/group/tab-bar migration by each file's own worktree (was applied under one scope) (#2). - Diff tabs: single-file unstaged diff tabs are now retargeted on a directory move (rebuild the diff id + relative path) instead of stranding (#3). - Mirror close-notify moved to AFTER a successful rename so a failed rename can't desync the host by closing its authoritative tab (#4). Adds tests: collision->inverse-rename, diff-tab retarget. 6698 tests green. Co-authored-by: Orca <help@stably.ai> * fix(editor): review round 2 (mirror ordering, rollback error, diff sources) - Close the host mirror tab only AFTER the rekey commits (capture pre-rekey resolution first): a rekey collision after a successful rename no longer desyncs the host by closing its authoritative tab (high). - Surface a failed inverse rename instead of swallowing it: the thrown error now states the on-disk move may remain at the new path (high). - Restrict diff-tab retargeting to staged/unstaged (purely path-derived ids); branch/commit diffs carry compare metadata and combined 'Changes' is worktree-rooted, so rebuilding them from path would produce a wrong id (med). Co-authored-by: Orca <help@stably.ai> * fix(editor): resolve the move verify gate proactively (review round 3) The rekey gates every dirty moved tab pending a destination content check, but verification only ran when a watcher event arrived for that path. If the watcher was down, throttled, or the event coalesced away, the gate never cleared and autosave stayed suspended for the tab. The coordinator now drives verification for every tab it gated once the rename has committed, so the gate resolves on its own. That makes the destination-side event latch redundant, so the in-flight tracker is source-only again. Co-authored-by: Orca <help@stably.ai> * fix(editor): review round 4 (gate strand, cross-worktree verify path, leak) - Don't install the move-echo verify gate on a tab already showing the changed-on-disk banner: it's autosave-suspended via externalMutation and verification skips a 'changed' tab, so the gate would strand forever. - Content-verify reads each moved tab's own filePath. A cross-worktree/ floating tab's relativePath is relative to its own root ('../…') and must not be joined onto the initiating worktree path (would read the wrong file and raise a false conflict banner on unsaved work). - Coordinator settles the in-flight source suppression in a finally so a throw between rename and commit can't leak it, and only after the rollback rename so a late forward-rename delete stays suppressed. - Migrate pendingEditorReveal.fileId across the rekey (matcher prefers it). Co-authored-by: Orca <help@stably.ai> * fix(editor): don't consume move-echo provenance in the safety-net verify (round 5) The round-3 proactive post-commit verify ran resolveLiveMoveVerification, which consumed pendingSelfMoveEcho. On FSEvents/SSH the real destination watcher event reliably lands AFTER the fast local read, so it then found no provenance, took the immediate changed-on-disk mark (no baseline check when there is no recent self-write), and raised a false conflict banner on the just-moved dirty tab — the exact data-loss this change removes. The proactive verify is a safety net: it now releases the autosave gate but leaves the provenance, so a later destination event is still recognized as the move's own echo and content-verified. Only a real watcher event consumes the provenance. Keeping it is safe — every watcher consumer verifies by content, which is strictly safer than the immediate mark. Co-authored-by: Orca <help@stably.ai> * fix(editor): scope move rekey to the initiating execution host (round 6) Co-authored-by: Orca <help@stably.ai> * fix(editor): prefix-suppress the move root so late tabs under a dir move aren't flagged (round 8) Co-authored-by: Orca <help@stably.ai> * chore(editor): trim move-fix comments to the why; drop redundant rename quiesce Co-authored-by: Orca <help@stably.ai> * fix(editor): record mirrored-close intent synchronously to close the ghost-tab window Co-authored-by: Orca <help@stably.ai> * fix(editor): use flavor-aware path containment for move selection (Windows/UNC case) Co-authored-by: Orca <help@stably.ai> * fix(editor): reconstruct moved path by segment count (WSL alias / duplicate-separator safe) Co-authored-by: Orca <help@stably.ai> * fix(editor): infer moved-path flavor by syntax, preserving legal POSIX backslashes Co-authored-by: Orca <help@stably.ai> * test(editor): lock POSIX ancestor-backslash destination flavor Co-authored-by: Orca <help@stably.ai> * fix(editor): flavor-aware trailing-separator strip; preserve POSIX literal backslashes Co-authored-by: Orca <help@stably.ai> * fix(editor): flavor-aware separator folding in relative-path recompute (POSIX backslash) Co-authored-by: Orca <help@stably.ai> * perf(editor): keep the fs-watcher delete path O(deletes) when no move is in flight Co-authored-by: Orca <help@stably.ai> * docs(editor): tighten move-fix comments to one-line why-only Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
brennanb2025
added a commit
that referenced
this pull request
Jul 28, 2026
* fix(runtime): surface desktop RPC startup failures * fix(runtime): isolate RPC failure telemetry * fix(runtime): satisfy the changed-code quality gate and kill vacuous dialog tests The `no-floating-promises` label span covers the whole `app.whenReady().then()` callback, so adding lines inside it made a long-standing finding overlap changed code. `void` is the linter's own suppression; no `.catch()` on purpose. The startup-failure tests were vacuous: mutation runs showed the wait-for-show deferral, the destroyed-window guard, the `closed` companion event, listener cleanup, the cause walk, the cycle guard, and the truncation bound could all be deleted with every test still green. The "not called yet" assertion ran before any microtask, so it passed either way. * test(runtime): de-brittle the desktop RPC-failure source assertions Anchoring the slice on the full destructure and matching the whole dialog call expression made an innocuous rename break the test with a cryptic 'expected -1'. Match the shape that is actually the contract instead. * test(runtime): repair the silently-unbounded desktop startup slice The desktopEnd anchor comment lost a word in 98b00d3, so indexOf returned -1 and slice(start, -1) covered index.ts to EOF. Moving the dialog call to a path that never runs at startup still passed. Anchor on code instead, and assert both bounds so a future reword fails loudly. * test(runtime): bound the attach anchors in the startup ordering slice Round 3 bounded the desktop pair but left attachStart/attachEnd unguarded in the same test: deleting the PTY startup barrier from attachMainWindowServices() and breaking the rateLimits.attach(window) end anchor still left the case green. * test(startup): bound the last two unguarded slice anchors in this file Rounds 3 and 4 fixed the desktop and attach pairs; two instances of the same class survived in the same file, both proven vacuous by mutation: - it #3 never bounded readyEnd. Renaming the `pairing:` payload key makes it -1, widening readyPayload from 372B to ~52KB. Moving the reconciliation status out of the serve-ready payload (its whole point) but leaving it later in index.ts then kept all 6 cases green. - it #2 bounded desktopWindowStart against reconciliationStart rather than serveEnd. An earlier `Promise.resolve(openMainWindow())` steals the anchor, collapsing desktopStartup to '' while every existing guard still passes, so its only assertion — a negative — succeeds against an empty string. Both mutants now fail. `src/main/ipc/pty-startup-barrier-ordering.test.ts:11` has the same latent shape; left alone as out of scope for this PR. * fix(runtime): keep walking the cause chain past an unmapped code getErrorCode returned the first code it found, so an outer wrapper carrying an unrecognised code masked a nested EACCES/ENOSPC and classified it unknown. Only a mapped code ends the walk now; every other input classifies as before. Unreachable today (writeSecureFile rethrows raw fs errors with .code intact), but the classifier's job is surviving whatever error shape reaches it. * fix(runtime): tell the user what to fix, not just to restart The dialog's only advice was "Restart Orca to try again", which is true for address_in_use and wrong for the rest: permissions, a full or read-only disk, and a missing data folder all survive a relaunch, so the user restarted, hit the same failure and had no next step. Route the error class we already compute into the copy so each cause names the thing the user has to change. Guidance and telemetry now derive from the same classifier, so they cannot drift apart. * fix(runtime): guide users through long RPC paths * fix(runtime): avoid false window listener warning * fix(runtime): guard destroyed window before web contents
bbingz
pushed a commit
that referenced
this pull request
Jul 29, 2026
…(C1) (stablyai#10625) * fix(terminal): park SSH worktrees like local ones (C1 retention, slice A) SSH ptys were blanket-excluded from hidden-view parking, so a hidden SSH worktree retained every pane forever (C1: renderer heap climbs to the V8 ceiling). SSH bytes transit local main — fact-mode watchers already cover them, and main keeps a headless model served over pty:getMainBufferSnapshot that the SSH reattach path never consulted. - isParkRestorableTerminalPty: snapshot-backed OR (SSH + policy); threaded through both park verdicts, both selectors, watcher coverage, and the watcher start guard. Remote-runtime/fail-open/foreign/null unchanged. - Parked-SSH reveal paints from main's headless model (dimension-matched, ~5k rows) and degrades to the relay 100KiB replay unless the snapshot is a non-empty source==='headless' payload — never a blank/stale paint. - Kill switch: settings.terminalSshViewParking (default on). DESIGN.md records the approved plan and the H1 magnitude non-claim. Co-authored-by: Orca <help@stably.ai> * fix(terminal): bound hidden-worktree retention with a force-park budget (C1, slice B) Un-parkable worktrees (remote-runtime ptys, uncoverable tabs, SSH with the slice-A switch off) had unlimited retention: the parking cap/TTL only ever saw eligibility-passing worktrees, so one bad tab pinned a whole worktree's panes forever. Retention is now memory-bounded, not eligibility-bounded. - terminal-hidden-worktree-retention.ts: retention budget (12 hidden / 45min TTL, sized from the measured 2.5-19MB per-pane V8 cost, DESIGN.md §2) over hidden worktrees ordinary parking can never evict; reuses the hot-retain ranking so last-active exemption, deterministic ties, and deadline-driven rechecks hold. Fail-open/foreign-pty tabs are eviction-exempt (a remount would fresh-spawn and orphan the live shell). - Terminal.tsx: force-parked ids join the parked set AFTER the coverage veto (darkness for uncoverable tabs is the accepted cost); buffers captured via the sleep-flow registry before the unmount render; retention TTL added to the recheck deadlines for budget candidates only. - Verdict stays out of its own effect deps; policy test asserts idempotence and time-monotone membership (flip-loop dwell regression). - Kill switch: settings.terminalHiddenWorktreeRetentionBudget (default on). Co-authored-by: Orca <help@stably.ai> * fix(terminal): demote hidden scrollback for eviction-exempt worktrees (C1, slice C) The retention budget (slice B) must exempt worktrees holding fail-open or foreign-worktree ptys — a remount would fresh-spawn and orphan the live shell — which would leave that class unbounded again. Instead, past the same 45min retention TTL their hidden panes drop to the minimum scrollback tier (measured: ~19MB -> ~1.3MB V8 heap per 50k-row pane; trimmed history is gone by design, reveal restores the configured cap for future output). - terminal-hidden-scrollback-demotion.ts: module-state verdict registry (parked-watcher pattern) with content-equality notify damping; applied in the existing scrollback-rows effect in use-terminal-pane-lifecycle. - selectScrollbackDemotedTerminalWorktrees: pure, TTL-gated, time-monotone. - Retention TTL wakeups now also cover exempt worktrees so demotion fires. - Kill switch: settings.terminalHiddenScrollbackDemotion (default on). Co-authored-by: Orca <help@stably.ai> * fix(terminal): paint the SSH model snapshot inline, not via nested coordinator (C1 slice A fix) applyMainBufferSnapshot runs its own structuralReplayCoordinator.run; calling it from applyReattachPayload (already inside the coordinator when a relay replay exists) deadlocks on the coordinator's tail chain. The model paint now mirrors the daemon-snapshot branch inline (folded scrollback + rehydrate + screen, dimension-matched, escape tail last) and arms the restored-snapshot seq baseline so deferred/live chunks the snapshot covers dedupe instead of double-painting. Also falls through (no early return) so reattachPayloadApplied still latches. Adds the folder-workspace id parity unit case. Co-authored-by: Orca <help@stably.ai> * test(terminal): SSH park+reveal e2e round-trip + as-built design notes (C1) Docker-gated (ORCA_E2E_SSH_DOCKER=1) spec: SSH tab parks behind a decoy and reveal restores marker content at multi-viewport scrollback depth. DESIGN.md records the as-built deltas (inline paint, force-park shape, last-active floor) and the residuals so follow-ups aren't lost. Co-authored-by: Orca <help@stably.ai> * fix(terminal): paint SSH reveal from main's model even when the relay replay is empty (C1 review #1) A relay restart empties the replay buffer; the reveal previously painted nothing even when main's headless model held the session. The reattach now prefetches the model snapshot when no structural replay exists (SSH-shaped ptys only) and paints it inside the coordinator; emptiness is judged on the composed payload (scrollbackAnsi + data + pendingEscapeTailAnsi) so an alt-screen snapshot with an empty screen frame still paints. Co-authored-by: Orca <help@stably.ai> * fix(terminal): decouple scrollback demotion (slice C) from the retention-budget switch (C1 review #2) Per the approved contract each slice reverts behind its own switch: slice C now requires only the master terminalHiddenViewParking plus its own terminalHiddenScrollbackDemotion flag. The TTL wakeup timer fires for demotion candidates even with the budget switch off. No DEFAULT_SETTINGS entries exist for sibling flags (defaults are the '!== false' optional pattern), so no explicit defaults are added. Co-authored-by: Orca <help@stably.ai> * fix(terminal): scope eviction exemption to the tab, not the worktree (C1 review #3) One eviction-exempt tab (fail-open/foreign pty) previously vetoed force-park for its whole worktree, pinning co-located remote-runtime tabs forever. The worktree now force-parks while exempt tabs keep their mounted panes via a per-tab exclusion mirroring the Activity-portal pattern (legacy watcher sync, legacy render, and the overlay cold-parking hook). Ordinary parking is untouched — a worktree with an exempt tab still cannot ordinary-park. Slice C now also demotes exempt tabs' panes as soon as their worktree force-parks under the count budget (they are the only panes left mounted). Co-authored-by: Orca <help@stably.ai> * fix(terminal): demote un-parkable worktrees the force-park lever spared (C1 review #4) The last-active exemption means a single hidden un-parkable worktree never force-parks — and slice C previously only targeted exempt-tab worktrees, so its panes held full scrollback forever. Demotion now also covers un-parkable non-exempt worktrees past the retention TTL that are absent from the force-parked set (last-active spared, or slice B switched off). Membership stays time-monotone for fixed inputs; covered by new idempotence/monotone selector tests. Co-authored-by: Orca <help@stably.ai> * fix(terminal): keep the hidden clock running through transient background-measure windows (C1 review #5) Whole-worktree background mounts (browser-automation bootstrap lease, mobile mounts, agent wakes) open a ~3s self-clearing measure window that previously deleted hiddenSince — every remount restarted the 30s hysteresis and the 45min retention TTL, so a periodically re-mounted force-parked worktree never re-parked. The measure window still pauses parking/eviction verdicts (all selectors skip measuring candidates); only the clock survives, so the prior verdict resumes as soon as the window closes. Visible and portal-holding worktrees still reset the clock. Co-authored-by: Orca <help@stably.ai> * test(terminal): make the SSH park+reveal depth assertion prove the model paint (C1 review #6a) Pad the session with ~180KB of output after the numbered markers so the earliest marker falls outside the relay's 100KiB rolling replay buffer while staying inside main's ~5k-row headless model; asserting marker_1 after reveal now proves the headless-model paint rather than passing under the relay fallback. Co-authored-by: Orca <help@stably.ai> * docs(terminal): rewrite DESIGN.md as the single as-built C1 contract (review #7) One contract matching the code: status IMPLEMENTED around force-park (not the unmount proposal), real kill-switch names with coupling + revert matrices, the true retention-floor formula with measured per-pane and demotion numbers, an explicit when-OOM-is-still-possible paragraph naming the H2 pendingSideEffects residual, the applyMainBufferSnapshot deadlock constraint inside the slice-A section, stable-signal phrasing instead of a capability latch, fail-open AND foreign-worktree exemption class, verified cites, and a planned/landed/follow-up test matrix. Co-authored-by: Orca <help@stably.ai> * fix(terminal): resolve the eviction exemption per pane, not per tab (C1 review #8) isEvictionExemptTerminalTab read only tab.ptyId — the FIRST leaf's pty — while the coverage veto that makes a worktree a retention candidate walks every pane. A split tab whose second leaf held an unrestorable pty therefore failed coverage (→ force-park target) yet looked exempt-free, so force-park unmounted it and orphaned the live shell. The exemption now resolves panes through the same resolveParkedTerminalPaneCandidates, keeping tab.ptyId in the union for the no-layout/no-capture case. Also from the same review round: - force-park's capture passes includeLocalBuffers:false like every other shutdownBufferCaptures caller; it was serializing up to 512KB/pane of scrollback into the store inside a fix meant to bound renderer heap. - Terminal.tsx unmount resets the scrollback-demotion registry — module state with no reset path, read by a pane effect that runs before the host effect that would clear it, so a stale verdict trimmed restore replays. - memoize watcher coverage per tab within the parking pass; the retention candidates re-asked it for every mounted worktree, not just the parked few. * docs(terminal): drop DESIGN.md — the as-built C1 contract moves to the PR body Co-authored-by: Orca <help@stably.ai> * fix(terminal): cap the deferred PTY side-effect queue (C1 residual H2) pendingSideEffects grew without bound under background timer throttling (~64 drained/s vs hundreds queued/s overnight). Cap at 512 entries with oldest-first eviction: titles drop (last-wins), a pending bell latches onto the next survivor, agent-status payloads collapse onto the survivor keeping the newest 16 (last-wins store state, KB-scale strings). Co-authored-by: Orca <help@stably.ai> * fix(terminal): carry command-lifecycle facts through parked watchers (C1 follow-up) Parked fact-mode watchers omitted onCommandFinished/onCommandCode*, so OSC 133;D and Command Code scrape signals went dark while parked. New parked-terminal-command-status.ts ports the store-level subset: git-UI nudge on every command finish, same-turn status-row drop for SSH PTYs (exact mounted-path parity — the foreground tracker refuses SSH ids), and the Command Code working seed / 1500ms done settle. Byte mode scans the same shared parsers for authority-off parity. Local-PTY status drops stay with the mounted pane: they need pty-connection's process-confirm ladder to tell a leaked nested-shell 133;D from a real agent exit. Co-authored-by: Orca <help@stably.ai> * test(terminal): retention-budget force-park e2e with a retentionLimit override (C1 6b) ORCA_E2E_TERMINAL_RETENTION_LIMIT flows preload → e2e-config → getTerminalParkingPolicyOverrides (exposeStore-gated, positive-integer only) so a spec can shrink the force-park budget to 1. The Docker-gated spec opens two remote worktrees on one relay target (second pre-seeded remote repo), disables terminalSshViewParking to make both un-parkable, hides both behind the local context, and proves the older one force-parks while the last-active exemption spares the newest; re-activating the evicted worktree restores the marker tail via relay replay. Co-authored-by: Orca <help@stably.ai> * test(terminal): retention-budget e2e via same-repo remote worktrees (passes docker lane) The first draft added a second remote repo mid-session, whose pane pty spawn misroutes to the local daemon with the remote cwd (pre-existing multi-repo issue, reproducible without any retention override — a seeded local repo plus one remote repo shows the same misroute). The spec now budgets across three worktrees of the ONE connected repo, created through the product createWorktree path (an external git-worktree-add only lands as a detected worktree needing adoption) and polled through the relay's transient post-connect reconnect window. Verified green on the local Docker lane in 20.8s. Co-authored-by: Orca <help@stably.ai> * fix(terminal): prevent remount thrashing during post-measure cool-down ( Implements the C1 retention contract: preserve worktree `hiddenSinceMs` through a background-measure window (so TTL/ranking stay honest), but re-park waits for a full `coldParkDelayMs` cool-down after the measure ends. Without the cool-down, every ~3s measure lease on a past-deadline worktree thrashes remount/reattach. Core changes: - Terminal.tsx: add measure clock (measuringTerminalWorktreeIdsRef) and post-measure cool-down tracking (terminalWorktreeParkCooldownUntilRef); gate parking candidates until cool-down expires. - Extract snapshot replay choreography to shared terminal-snapshot-replay-paint.ts (used by SSH reattach + daemon restore paths). - Add SSH model snapshot timeout (750ms) with fallback to relay replay. - Move cold-park recheck deadline logic to terminal-cold-park-recheck-deadlines.ts; add cool-down deadline to scheduling. - useTerminalTabColdParking: implement matching measure-clock contract with per-tab cool-down gate to keep tab deadlines synced with worktree retention clock. - Add resolveTerminalMountScrollbackRows() to demote new xterms under demoted worktrees (pane births during demotion must take the demoted tier at create). - Add kill switches: terminalSshViewParking, terminalHiddenWorktreeRetentionBudget, terminalHiddenScrollbackDemotion. * fix(terminal): detect Command Code completion in parked mid-turn panes Seed the byte watcher with in-flight turn state from agent status: the watcher is recreated per park cycle with no startup command to arm it, and the banner scrolled away before parking. Also memoize eviction-exempt checks and use SSH PTY ID builder in tests. * fix(terminal): flush pending command-code settles on reveal remount When a parked pane reveals mid-Command Code turn, the new detector cannot re-observe the already-passed idle composer. Cancelling the settle leaves the row stranded at 'working', so dispose now flushes the pending settle instead. Extract readInFlightCommandCodeTurn to shared space and seed detectors with in-flight turns so remounts complete mid-flight commands. Also memoize SSH model probes to prevent double timeouts on reattach. * fix(terminal): remove scrollback demotion (C1 slice C) The scrollback demotion feature for eviction-exempt hidden worktrees is no longer needed. Retention budget limits are now sufficient without this additional bound. Remove the terminal-hidden-scrollback-demotion module, the selectScrollbackDemotedTerminalWorktrees function, and related per-pane demotion logic. * test(terminal): assert bounded probe during stalled reveal Add assertion to verify that a stalled reveal operation makes exactly one `getMainBufferSnapshot` call, ensuring retry logic doesn't introduce redundant probes that would extend the timeout window before relay fallback. * fix(terminal): implement C1 retention budget for hidden parked worktrees Addresses OOM regressions in hidden parked terminals by force-evicting worktrees past a retention budget: at most 12 mounted while hidden, none past 45 minutes (absolute, not exempted by last-active). Eviction is least-recently-hidden-first. Exempt tabs (unrestorable local PTYs) keep their panes to avoid orphaning shells; worktrees are force-parked even if they contain exempts, and their buffers released elsewhere. SSH/remote worktrees serialize buffers pre-eviction for reveal; local worktrees keep daemon snapshots. Command Code's done-settle window is transferred across park/reveal boundaries so the row cannot strand at 'working'. Model probe on SSH reattach is scoped to park-reveal only, not ordinary reconnects. Includes new E2E suite proving the budget actually releases memory. * memoize eviction-exempt terminal tabs to avoid redundant store reads Each tab's exemption check re-reads the store and walks the layout tree. Introduce selectEvictionExemptTerminalTabIds() to resolve all exempt tabs for a worktree in a single pass, then memoize the result in Terminal.tsx and useTerminalTabColdParking. This prevents O(n) store reads when checking exemptions across multiple tabs and ensures the set remains stable across unrelated re-renders. * refactor: reformat hidden-worktree retention comments Reflow to 80-character lines and remove internal ticket references (C1, C1 slice C). * fix(lint): split overlay slot and eviction-exempt tabs under max-lines Static analysis failed because TerminalPaneOverlayLayer (401) and terminal-parked-tab-watchers (304) exceeded oxlint max-lines. Extract the slot component and eviction-exempt helpers into dedicated modules. * test(terminal): stabilize retention budget e2e control arm Stage un-parkable remote pty ids only after both worktrees are hidden, and keep re-staging during the control-arm poll so a late updateTabPtyId cannot flip the decoy back to park-restorable and ordinary-park it before budget engages. * test(terminal): pin retention e2e decoy to a mounted pane snapshot Use the active pane-identity snapshot for the decoy tab instead of all worktree tabs, and re-assert un-parkable ids after the control-arm hold so a deferred/empty tab id cannot fail the budget-off mounted-count check. * fix: memoize terminal eviction exemptions on layout leaf PTYs Splits add leaf panes to the layout store without changing the tabs array. A memo keyed only on tabs misses this change, leaving new panes unexempted for unmount. Include layout leaf PTYs in the exemption memo key so it recalculates when splits occur or PTYs are re-minted. --------- Co-authored-by: Orca <help@stably.ai>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CI mirror for upstream PR stablyai#8480 after rebasing onto the latest main.
Base is pinned to 371d71a. Head is 0eabed9. This PR exists only to run fork-authorized repository checks and will be closed after results are recorded.