Skip to content

fix(orchestration): main owns every settled-worker fence decision; renderer fence copy becomes advisory - #19427

Closed
Jinwoo-H wants to merge 16 commits into
mainfrom
fence-unify
Closed

Jinwoo-H wants to merge 16 commits into
mainfrom
fence-unify

Conversation

@Jinwoo-H

@Jinwoo-H Jinwoo-H commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 67 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​8190 $\color{#cf222e}{\Huge{\mathbf{−}}}$​366 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​7824
Prod 93 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1439 $\color{#cf222e}{\Huge{\mathbf{−}}}$​736 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​703

Summary

Replaces #19344 and #19358 as a single PR. Both branches are merged in unchanged (their heads 2197d23bba and 34a70e8f25 are ancestors of this branch), then one commit moves the last two fence decisions into main and deletes the renderer's fence-consistency lane.

Invariant. A worker fenced by orchestration (legacy-orchestration-worker) is never relaunched and never killed by automation. Owner. Main, which persists the fence. After this PR the renderer's copy of the fence set is advisory: a stale copy can no longer launch or kill anything.

Why a unification pass

Five review rounds across #19344 and #19358 each found a race in the renderer's mirror of the fence (pending read vs hydration vs scoped snapshot vs reply supersession). The mirror had to be race-free because two destructive renderer decisions trusted it and main did not check them:

  1. The auto-resume sweep created a new tab (new pane key) with a resume claim; main's fence is keyed by the fenced pane key, so main never saw that spawn as fenced.
  2. Hibernation sent pty:kill with keepHistory; main's kill path never consulted the fence.

What changed (commit 2120f09)

  • Main admits the resume claim. isSleepingAgentResumeBlocked matches the spawn's resumeProviderSession against the host partition's sleeping records and refuses (structured reattachUnverifiable) when the source pane is fenced. Applied at IPC pty:spawn (begin + execute), the runtime controller (entry + claim path, rechecked after async owner reconciliation), and therefore paired terminal.create / createAgentSession. No new wire field: the existing resume metadata identifies the record.
  • Main admits the hibernation kill. assertPtyHibernationAllowed resolves the persisted pane for a keepHistory kill and throws the existing agent_hibernation_automatic_resume_blocked error for a fenced pane. Non-reversible (user close) kills are unchanged.
  • Renderer keeps the sleeping record until main admits the spawn; a refusal closes only the new tab (automatic-resume-spawn-settlement.ts).
  • Deleted: runtime-session-application.ts (pending-read lane, supersession, consumed-object tracking), refresh serialization, renderer-side legacy-fence normalization for authority, and three ordering-only test files. Hydration now sets the fence in the same store transition as the records and otherwise last-write-wins.
  • Shared predicate isPaneAutomaticResumeBlocked now reads the canonical set (readWorkspaceSessionResumeFences) so recordless canonical fences and explicit retirement are honoured in main.
  • Retained TERMINAL_FENCED_CREATE_RUNTIME_CAPABILITY's client-side status.get gate: it protects new client + old host, the direction host-side negotiation cannot cover.

Net this commit: +207 / -281 non-test source lines. Full report with the wire decision, capability reasoning, and probe census: ~/orca/review-archive/followups/fence-unify-report.md (local archive).

Compatibility

  • Old client + new host: refusal is a non-success reply to clients not advertising the capability (from fix(terminal): let main own a fenced worker's PTY session #19358 round 4). Clients that omit resume metadata cannot be mapped; documented cost.
  • New client + old host: old host cannot enforce the new admission; the renderer hint still prevents most requests but is not authoritative there.
  • No schema change, no stream opcode, no new persisted field.

Verification (lead)

  • Suites: 3187 files / 32127 tests across renderer lib/store/terminal-pane and main ipc/runtime/persistence/shared; node + web typecheck; lint; reliability manifest (111 gates); max-lines ratchet.
  • Mutations on the new admissions, each killed: resume admission disabled (5 fail), kill admission removed (4), refusal leaves the tab (2), admission ignores the fence (1).
  • Archived reviewer probes from fix(terminal): let main own a fenced worker's PTY session #19358 rounds 1–3 and fix: persist runtime-owned settled-worker resume fences #19344 rounds 3–4 pass on this tree (the round-3 old-client oracle's checked-in replacement passes 7/7).
  • E2E on a fresh build, hidden windows: all four fence specs, both orders, 5/5 twice.

Round-1 review follow-ups (dc6390b, 69ea26a)

Codex round 1 found five real defects: main decided on identity it did not own. Fixes, each at the owning layer:

  • F1 (P0) Renderer duplicate cleanup could delete the fenced record main uses to identify a resume source. The Store write boundary now retains a fenced pane's record for the fence's lifetime; runtime retirement drops the fence entry so the next renderer write may drop the record.
  • F2 (P0) Runtime fallback rechecked only the destination pane after awaiting attach. One admission (isFreshPaneResumeBlocked, source or destination) now runs at the provider.spawn site and in the claim callback; the four redundant entry checks are deleted.
  • F3 (P1) Hibernation admission consulted only persisted bindings. It now iterates fenced pane keys and refuses when either the current runtime owner or the persisted owner is the target PTY.
  • F4 (P1) The rollback claim survived admission. Admission clears it; a refusal after accepted or preconnect input keeps the tab.
  • F5 (P1) A stale positive fence reply could overwrite a newer hydration. A single request generation (import-free module) drops replies older than a later request or hydration.
  • F6 (P2) Six surviving mutations now fail permanent tests.

Verification: round-1 probes pass at this head; ten lead mutations plus the worker's 28 all killed; six-directory gate 24,776 tests; both typechecks; changed-code quality; reliability manifest 111 gates; e2e 5/5 both orders on a fresh build. Net non-test source delta this round: 16 files, +125/-117.

…ly home

The resume fence that stops a settled orchestration worker being relaunched had
two homes and a directional gap between them. `sleepingRecordFromEntry` derived
`automaticResumeBlockedBy` solely from `automaticResumeBlockedPaneKeys`, which
starts empty on every renderer boot and is never persisted, so the first
`setAgentStatus` after any renderer start rebuilt the record without the flag.
The hibernation planner then admitted the pane, killed a live worker PTY, and
worktree activation relaunched it with `--resume`.

The record is now the durable home. Every rebuild path resolves the flag through
one predicate that reads the record first and the pane-key map second, gated on
session identity, so `carryOverAutomaticResumeBlock` and the open-coded carry in
`recordAgentProviderSession` collapse into it. `recoveryRecordMatches` treats the
flag as significant, so a rebuild that would drop it is no longer accepted as an
equal record. The planner and the hibernation coordinator's re-check now read the
pane-key map too, which closes the window where a pane is fenced before any
record exists.

Main stops announcing each fence once per process. `prepare` re-announces every
blocked pane on every pass and the renderer-startup handshake returns the fenced
pane set, so a renderer that reloads, crashes or opens a second window re-derives
its whole map instead of losing the fence. The announce and the lift edge are now
committed only after the session write succeeds, so a staging failure retries
rather than dropping the fence forever.
A settled orchestration worker's pane (automaticResumeBlockedBy:
'legacy-orchestration-worker') attached renderer-only: transport.attach
registered IPC listeners without ever asking main to attach the daemon
session. Main is the daemon's only client, so after an app restart its
adapter never learned the id, pty:hasPty answered a fabricated false, and
the visibility reconciler tore the tab down while the process still ran.

The fence's intent is right, the mechanism was wrong. A fenced pane now
takes the SAME deferred reattach as every other restored pane and carries
the fence as attachOnly on the connect: main attaches the live session or
refuses to mint one. A session its owner proves absent comes back as the
existing exitedBeforeAttach terminal state, so the pane neither respawns
nor retires a binding it may never replace.

Deletes the renderer-only attach path (retained-legacy-pty-attach.ts, the
legacyAttachOnlyPtyId short-circuit, and the SSH gate that skipped the
deferred flow for these panes).

Pre-commit hook skipped: it runs `pnpm install`, which fails on this
worktree's symlinked node_modules. Its checks (oxlint, react-doctor
oxlint, oxfmt) were run manually on every staged file.
…ed state

Codex review of #19344 found the startup reply could re-fence a retired pane.
Two paths, one defect: the reply was built somewhere other than the object that
owns fence publication. The runner read `plan.blockedPanes` captured before it
awaited workspace resolution, inventory and persistence, so a release or takeover
landing inside that window produced a reply older than its own lift; and a pass
whose `setWorkspaceSession` threw published nothing through the push channel yet
still returned those keys, fencing a pane no later pass could retire.

The persistence object is now the only publisher. It exposes the set it has
actually committed, counted by a commit generation, and the runner reads that
after the pass finishes rather than deriving anything from the starting plan. A
pass that commits nothing reports the previous committed state, which also keeps
an unreadable plan from ever reading as "no pane needs its fence". The generation
travels with both channels, so the renderer applies main's whole committed set
and drops a reply that a newer lift has already overtaken. That makes the seed
authoritative instead of additive, which is what finally retires a fence on a
pane that has no sleeping record to sweep.

Manual sleep and hibernation rollback both committed a record captured before the
await, erasing a fence delivered during the stop. Both now resolve the flag from
state at commit time through the same predicate as every other rebuild.

Also: the degraded-startup routing gate searched for the call the seed helper
replaced, so its ordering assertion silently passed on -1; it now follows the
helper and asserts the indirection still reaches main. The shared fake-CLI e2e
fixture is re-materialized on demand, because the first spec file's teardown
deleted it out from under the next file in the same worker.
Both specs that import completed-worker-retirement-fixture registered a
per-file `test.afterAll(cleanupCompletedWorkerFixture)`. That helper does
`rmSync(fakeCliDir, { recursive: true })`, and fakeCliDir holds the fake
`codex` executable itself, not just the ledger. Playwright loads the
module once per worker, so with `--workers=1` whichever spec file
finished first deleted the binary out from under every later file in the
run: the next spec launched a deleted path, wrote no lifecycle events,
and timed out waiting for them. Proven with a probe at the failing poll:
fakeCliDirExists false, ledgerExists false, ledger [].

The temp dir's lifetime is the worker process's, so the module now owns
its own teardown via process.once('exit') and no spec can end it. Ledger
accumulation was NOT the cause; both specs already truncate per test.

Separately, `liveness before reveal` sampled pty:hasPty once right after
backgroundMountTab, which only waits for the pane manager to exist. Main
learns the session when the pane's deferred reattach lands, so the sample
raced it (2 failures in 10 runs, order-independent). It now polls, and
stays soft so a slow attach cannot short-circuit the tab-teardown
assertion that follows. A restarted main that never attaches stays false
for the whole 20s window, so both assertions still fail on unfixed code.

Pre-commit hook skipped (ERR_PNPM_CMD_SHIM_CHMOD on this worktree's
symlinked node_modules); oxlint and oxfmt run manually on all three files.
…one home

The fence was main-owned state stored inside a renderer-owned field.
`sleepingAgentSessionsByPaneKey` is co-authored: the renderer emits the whole map
and main applies it as a wholesale replace, so main's stamp survived only until
the renderer's next write. That inversion is what produced the original P0 — the
fence was erased on disk, not just in memory — and every guard added since was
compensation for it: a carry predicate at each record builder, two equality hunks
behind that predicate, a volatile pane-key map with a seeded startup reply, and
main-side bookkeeping to synthesise lift edges for panes with no record.

The fence now lives in `legacyWorkerResumeFencesByPaneKey`, a runtime-authored
session field classified like `clientHostedBrowserPagesByWorktree`. It is absent
from the renderer's patch builder, so the renderer cannot write it, and the
persistence layer carries it across any write that never mentions it. Main
rewrites the whole set every recovery pass, record or no record. Level-triggered:
no announcement to consume once, no edge to miss, no generation to compare.

The renderer reads it through ordinary session hydration and re-reads it when
main pings that it changed; the ping carries no state, and reads are coalesced so
the last read is the last write. Hibernation eligibility, the resume decision, the
reattach choice and the park exemption all consult that one field.

`automaticResumeBlockedBy` stays on the record as an outbound projection for
clients too old to read the new field, stamped in `buildSleepingAgentSessionData`
alone — the single function both persistence paths build records through.

Deleted: the pane-key map and its writer, the startup seed helper and the reply
shape it needed, `carriesAutomaticResumeBlock` and its call in every record
writer, both equivalence hunks, `lastPlanBlockedPaneKeys`, the fence generation,
and the record-sweeping half of `liftRetiredFences`. All five Codex findings and
all five Opus findings are now unreachable rather than handled.
Remove renderer-declared attachOnly policy. Main reads the persisted host
pane fence before early adoption, provider spawn and retirement, and sends
structured observed-exit or unverifiable results without deleting history.

Keep binding retirement coupled to replacement eligibility, preserve paired
host evidence and recovery error routing, and cover absent-daemon restart.
@greptile-apps

greptile-apps Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Too many files changed for review (160 files, 100 file limit).

Bypass the limit by tagging @greptile-apps to review.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — the unification pass that moves the last two settled-worker fence decisions into main and demotes the renderer's fence copy to advisory.

  • Resume admission in main — isSleepingAgentResumeBlocked matches a spawn's resumeProviderSession against the host partition's sleeping records and returns reattachUnverifiable/exitedBeforeAttach for a fenced source, enforced at pty:spawn (begin + execute), the runtime controller (entry + claim path, rechecked after owner reconciliation), and paired terminal.create/createAgentSession/ensureAgentSession.
  • Hibernation admission in main — assertPtyHibernationAllowed throws agent_hibernation_automatic_resume_blocked for a keepHistory kill of a fenced pane; non-reversible (user close) kills are unchanged.
  • Single persisted home — new runtime-authored legacyWorkerResumeFencesByPaneKey field, written level-triggered per host by RuntimeLegacyWorkerTerminalRecoveryPersistence.prepare() and preserved across renderer session writes.
  • Advisory renderer mirror — legacyWorkerResumeFencesByPaneKey store state refreshed via a pull IPC on an invalidation ping; hydration installs fence + records atomically; the sleeping record is kept until settleAutomaticResumeSpawn runs after main admits or refuses.
  • Old-client negotiation — negotiateTerminalCreateRefusal maps a refusal to remote_runtime_unavailable for clients without TERMINAL_FENCED_CREATE_RUNTIME_CAPABILITY; new optional wire fields exitedBeforeAttach/reattachUnverifiable.
  • Cleanup — renderer pending-read lane, refresh serialization, setSleepingAgentAutomaticResumeBlocked, attachRetainedLegacyPty, and three ordering-only test files deleted.

ℹ️ Nitpicks

  • src/main/ssh/ssh-pending-pty-kill-replay.ts:38 (and the mirror comment at ssh-pending-pty-kill-replay.test.ts:207) still document isPtyAlreadyGoneError as matching /Session not found/i. That predicate now delegates to isSessionNotFoundRefusal (case-sensitive 'Session not found: '), so the quoted regex is stale — the intent (matches on message text) still holds, but the literal is wrong.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@coderabbitai

coderabbitai Bot commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: d0bd5673-dbb1-45b8-8747-7984a1a84340

📥 Commits

Reviewing files that changed from the base of the PR and between 69ea26a and 2538a81.

📒 Files selected for processing (13)
  • src/main/ipc/pty-ssh-undelivered-kill.test.ts
  • src/main/ipc/pty/pane/hibernation-admission.ts
  • src/renderer/src/lib/automatic-resume-spawn-settlement.test.ts
  • src/renderer/src/lib/automatic-resume-spawn-settlement.ts
  • src/renderer/src/lib/legacy-worker-resume-fence-refresh.test.ts
  • src/renderer/src/lib/legacy-worker-resume-fence-refresh.ts
  • src/renderer/src/lib/resume-sleeping-agent-session-legacy-worker.test.ts
  • src/renderer/src/lib/resume-sleeping-agent-session.ts
  • src/renderer/src/store/terminals/workspace-terminal-hydration.ts
  • src/shared/terminal-tab-types.ts
  • src/shared/workspace-session-schema.test.ts
  • src/shared/workspace-session-schema.ts
  • src/shared/workspace-session-sleeping-agents.ts
💤 Files with no reviewable changes (2)
  • src/renderer/src/lib/legacy-worker-resume-fence-refresh.ts
  • src/renderer/src/store/terminals/workspace-terminal-hydration.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/renderer/src/lib/resume-sleeping-agent-session-legacy-worker.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.


📝 Walkthrough

Walkthrough

This change moves settled-worker resume fences into runtime-authored workspace-session state. It persists, hydrates, projects, and refreshes those fences across host partitions and renderer writes. PTY flows now attach existing sessions without replacement spawns and return structured refusal outcomes when absence cannot be verified. Renderer and RPC paths preserve pane bindings and negotiate compatibility through a new runtime capability. Tests cover local, SSH, remote-runtime, daemon-restart, persistence, and end-to-end restart scenarios.

Merge Risk: 🟡 Moderate · up to 2538a

This change centralizes resume-fence ownership and session recovery, but unresolved concerns remain around refusal cleanup, attach classification, retained bindings, and validation reliability. These should be addressed or explicitly accepted before merge.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description gives detailed change rationale, compatibility information, and verification results, but it omits several required template sections. It does not include the required Linked Issue ent… Add the missing template sections. Include a linked issue in the required format, provide before/after visual proof or write N/A with a reason, document testing and platforms, complete the Agent skill upstream boundary checkbox, add Notes…
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 56 functions across 64 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: main now owns settled-worker fence decisions, while the renderer copy is advisory.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description gives detailed change rationale, compatibility information, and verification results, but it omits several required template sections. It does not include the required Linked Issue entry, Visual Proof or an explicit N/A statement, Testing checklist, AI Disclosure, Review, Agent skill boundary, Notes, or Checklist.

Resolution

Add the missing template sections. Include a linked issue in the required format, provide before/after visual proof or write N/A with a reason, document testing and platforms, complete the Agent skill upstream boundary checkbox, add Notes and Review details, and complete the checklist. Include AI Disclosure if applicable.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (6)
src/preload/api/pty-api.ts (1)

68-69: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the two new outcome fields on the shared contract.

The surrounding reply fields carry doc comments that state their meaning and host-version semantics. These two fields drive the renderer's most consequential decision in this PR: reattachUnverifiable must not be read as proof that the process died, while exitedBeforeAttach is that proof. Record the distinction here, because this type is the contract every transport and renderer path reads.

♻️ Proposed documentation
+    /** Main proved the named session was gone before the attach; no replacement was created. */
     exitedBeforeAttach?: boolean
+    /** Main could not prove the session's state. Never a claim that the process died. */
     reattachUnverifiable?: boolean
src/shared/workspace-session-resume-fences.ts (1)

13-13: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Export the fence sentinel as a shared constant.

The string 'legacy-orchestration-worker' is the persisted sentinel that decides whether a pane is fenced. It is repeated as an inline literal in this predicate and across the cohort's fixtures, for example src/main/ipc/pty-ssh-undelivered-kill.test.ts line 132 and src/main/ipc/pty-attach-only-fenced-pane-spawn.test.ts line 87. A typo in the producer or in this comparison silently disables a fence, and no type check catches it.

Export a named constant from this module and compare against it.

♻️ Proposed refactor
+export const LEGACY_ORCHESTRATION_WORKER_RESUME_FENCE = 'legacy-orchestration-worker' as const
+
 export function readWorkspaceSessionResumeFences(
-        .filter(([, record]) => record.automaticResumeBlockedBy === 'legacy-orchestration-worker')
+        .filter(
+          ([, record]) =>
+            record.automaticResumeBlockedBy === LEGACY_ORCHESTRATION_WORKER_RESUME_FENCE
+        )
src/renderer/src/store/slices/store-sleep-pane-hibernation.test.ts (1)

220-220: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align the test name with what the test asserts.

The name claims an empty advisory fence, but the body asserts only suppression rollback, the sleeping record, and the retained status row. It never reads legacyWorkerResumeFencesByPaneKey. Either assert the fence set stays empty after the refusal, or rename the case to describe the rollback it verifies.

src/main/runtime/runtime-legacy-worker-terminal-recovery-persistence.ts (1)

92-92: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Move the invalidation ping out of the write try block.

The session write at Line 91 already succeeded when notifyFenceChanged runs. If the notifier throws, the catch at Line 94 logs a write failure and returns, so the remaining hosts are never written. The missed ping is also sticky: the next pass sees sameFenceSet true for the committed host and does not ping again, so a live renderer keeps a stale fence set until reload.

Isolate the ping so a notifier failure cannot be read as a write failure.

♻️ Proposed change
     try {
       for (const [hostId, fenced] of fencedByHost) {
         const current = store.getWorkspaceSession(hostId)
         if (!current || sameFenceSet(current, fenced)) {
           continue
         }
         store.setWorkspaceSession({ ...current, legacyWorkerResumeFencesByPaneKey: fenced }, hostId)
-        this.notifyFenceChanged?.()
+        try {
+          this.notifyFenceChanged?.()
+        } catch (error) {
+          console.warn('[orchestration] legacy worker resume fence ping failed', error)
+        }
       }
     } catch (error) {
src/renderer/src/components/terminal-pane/pr-base-pty-transport-test-loader.ts (1)

3-3: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Declare esbuild as a direct development dependency.

The loader imports esbuild directly, but the root package does not declare it. The current install resolves it through electron-vite and shamefullyHoist: true; this does not provide explicit dependency ownership.

tests/e2e/settled-worker-resume-fence-restart.spec.ts (1)

31-88: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Extract the duplicated settled-worker setup into the shared fixture module. Both new specs copy findSecondaryWorktree and backgroundMountTab verbatim, then repeat the same launch-to-worker_done sequence: settings update, terminal.resolvePane, orchestration.runCreate, orchestration.taskCreate, orchestration.workerStart, worker PTY discovery, transcript seeding, the setAgentStatus working/done pair, and the orchestration.send completion. Both files already import ./helpers/completed-worker-retirement-fixture, so that module is the natural owner. A future change to the orchestration RPC shape currently needs two edits that can drift apart.

  • tests/e2e/settled-worker-resume-fence-restart.spec.ts#L31-L88: delete the local findSecondaryWorktree and backgroundMountTab definitions and import them from the shared fixture module; call a shared setup helper for the launch-to-worker_done sequence and keep only the fence-persistence and reload assertions in this spec.
  • tests/e2e/settled-worker-tab-survives-restart.spec.ts#L35-L88: delete the identical local helper definitions and use the same shared imports and setup helper; keep only the daemon-session-gone branching, the reveal transition capture, and the persisted-layout assertions in this spec.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: 1eba99fe-6fdc-48fe-a59f-05f440b03638

📥 Commits

Reviewing files that changed from the base of the PR and between c1e15c4 and 2120f09.

📒 Files selected for processing (156)
  • config/reliability-gates.jsonc
  • src/main/daemon/daemon-errors.ts
  • src/main/daemon/daemon-pty-adapter-attach-only-ownership.test.ts
  • src/main/ipc/pty-attach-only-fenced-pane-spawn.test.ts
  • src/main/ipc/pty-resume-claim-ensure-fence.test.ts
  • src/main/ipc/pty-runtime-kill-and-exit.test.ts
  • src/main/ipc/pty-ssh-undelivered-kill.test.ts
  • src/main/ipc/pty/ipc/renderer-kill.ts
  • src/main/ipc/pty/ipc/spawn-begin.ts
  • src/main/ipc/pty/ipc/spawn-execute.ts
  • src/main/ipc/pty/ipc/spawn-run.ts
  • src/main/ipc/pty/pane/hibernation-admission.ts
  • src/main/ipc/pty/pane/stable-owner-retirement.ts
  • src/main/ipc/pty/pane/stable-owner.ts
  • src/main/ipc/pty/pane/stable-pane-fenced-evidence.test.ts
  • src/main/ipc/pty/pane/stable-pane-final-fence-recheck.test.ts
  • src/main/ipc/pty/pane/stable-pane-resume-fence-host.test.ts
  • src/main/ipc/pty/pane/stable-pane-resume-fence.ts
  • src/main/ipc/pty/provider/liveness.ts
  • src/main/ipc/pty/runtime/kill.ts
  • src/main/ipc/pty/runtime/spawn-early.ts
  • src/main/ipc/pty/runtime/spawn-execute.ts
  • src/main/ipc/pty/runtime/spawn.ts
  • src/main/orca-profiles/profile-project-session-field-disposition.ts
  • src/main/persistence/legacy-worker-session-projection.ts
  • src/main/persistence/loading-store/session-host-partitions.ts
  • src/main/persistence/loading-store/store-runtime-authored-session-fences.test.ts
  • src/main/persistence/restoring-sessions/session-worktree-ownership.ts
  • src/main/persistence/runtime-authored-workspace-session-fields.test.ts
  • src/main/persistence/runtime-authored-workspace-session-fields.ts
  • src/main/providers/pty-spawn-result.ts
  • src/main/runtime/orca-runtime-create-terminal.ts
  • src/main/runtime/orca-runtime-fence-automation-owner.ts
  • src/main/runtime/orca-runtime-get-agent-session-execution-namespace.ts
  • src/main/runtime/orca-runtime-preserved-branch-cleanup.ts
  • src/main/runtime/orca-runtime-terminal-attach-refusal.test.ts
  • src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-02.spec.ts
  • src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-03.spec.ts
  • src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-04.spec.ts
  • src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-05.spec.ts
  • src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-06.spec.ts
  • src/main/runtime/rpc/methods/agent-session.ts
  • src/main/runtime/rpc/methods/terminal/terminal-lifecycle-methods.ts
  • src/main/runtime/rpc/terminal-create-refusal-negotiation.test.ts
  • src/main/runtime/rpc/terminal-create-refusal-negotiation.ts
  • src/main/runtime/runtime-legacy-worker-host-write-publication.test.ts
  • src/main/runtime/runtime-legacy-worker-recovery-publication-order.test.ts
  • src/main/runtime/runtime-legacy-worker-session-publication.test.ts
  • src/main/runtime/runtime-legacy-worker-terminal-recovery-persistence.ts
  • src/main/runtime/runtime-legacy-worker-terminal-resume-fence.test.ts
  • src/main/runtime/runtime-notifier-contract.ts
  • src/main/runtime/runtime-pty-controller-contract.ts
  • src/main/runtime/terminal-attach-refusal.ts
  • src/main/runtime/terminal-create-pane-identity.ts
  • src/main/runtime/terminal-create-reveal.ts
  • src/main/startup/main-process-ipc-bootstrap.ts
  • src/main/window/runtime-window-lifecycle.ts
  • src/preload/api/agent-status-api.ts
  • src/preload/api/agent-status-bridge.ts
  • src/preload/api/app-api.ts
  • src/preload/api/app-bridge.ts
  • src/preload/api/pty-api.ts
  • src/renderer/src/components/terminal-pane/ipc-pty-attach-evidence.test.ts
  • src/renderer/src/components/terminal-pane/ipc-pty-connect.ts
  • src/renderer/src/components/terminal-pane/pr-base-pty-transport-test-loader.ts
  • src/renderer/src/components/terminal-pane/pty-connection-agent-session-resume.test.ts
  • src/renderer/src/components/terminal-pane/pty-connection-test-store-fixtures.ts
  • src/renderer/src/components/terminal-pane/pty-connection/deferred-session-attach.ts
  • src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-choice.test.ts
  • src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-choice.ts
  • src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-connect.ts
  • src/renderer/src/components/terminal-pane/pty-connection/fresh-spawn-eligibility.ts
  • src/renderer/src/components/terminal-pane/pty-connection/fresh-spawn-start.ts
  • src/renderer/src/components/terminal-pane/pty-connection/reattach-result-handler.ts
  • src/renderer/src/components/terminal-pane/pty-connection/reattach-unverifiable-result.test.ts
  • src/renderer/src/components/terminal-pane/pty-connection/retained-legacy-pty-attach.ts
  • src/renderer/src/components/terminal-pane/pty-connection/run-deferred-connect.ts
  • src/renderer/src/components/terminal-pane/pty-connection/sleeping-record-access-recordless-fence.test.ts
  • src/renderer/src/components/terminal-pane/pty-connection/sleeping-record-access.ts
  • src/renderer/src/components/terminal-pane/pty-connection/unverifiable-reattach-recovery.test.ts
  • src/renderer/src/components/terminal-pane/pty-connection/unverifiable-reattach-recovery.ts
  • src/renderer/src/components/terminal-pane/pty-transport-spawn-errors.test.ts
  • src/renderer/src/components/terminal-pane/pty-transport-types.ts
  • src/renderer/src/components/terminal-pane/remote-hidden-output-restore-outcomes.test.ts
  • src/renderer/src/components/terminal-pane/remote-hidden-output-restore-unavailable-banner.repro.test.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pr-base-refusal-compatibility.test.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-fenced-outcome.test.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts
  • src/renderer/src/components/terminal-pane/sleeping-record-park-exemption.test.ts
  • src/renderer/src/components/terminal-pane/sleeping-record-park-exemption.ts
  • src/renderer/src/components/terminal-pane/terminal-cold-park-subscription-narrowing.react185.test.tsx
  • src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.test.ts
  • src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.ts
  • src/renderer/src/hooks/ipc-events/agent-status-listeners.ts
  • src/renderer/src/hooks/useIpcEvents-lifecycle.test.ts
  • src/renderer/src/lib/agent-hibernation-coordinator.ts
  • src/renderer/src/lib/agent-hibernation-pane-eligibility-resume-fence.test.ts
  • src/renderer/src/lib/agent-hibernation-pane-eligibility.ts
  • src/renderer/src/lib/agent-hibernation-planner-snapshot.ts
  • src/renderer/src/lib/agent-hibernation-planner.test.ts
  • src/renderer/src/lib/agent-hibernation-planner.ts
  • src/renderer/src/lib/automatic-resume-spawn-settlement.test.ts
  • src/renderer/src/lib/automatic-resume-spawn-settlement.ts
  • src/renderer/src/lib/host-mirrored-pane-resume-replay.test.ts
  • src/renderer/src/lib/legacy-worker-old-source-resume.test.ts
  • src/renderer/src/lib/legacy-worker-resume-fence-refresh.test.ts
  • src/renderer/src/lib/legacy-worker-resume-fence-refresh.ts
  • src/renderer/src/lib/live-resume-anchor-record.ts
  • src/renderer/src/lib/pi-session-resume-wake.test.ts
  • src/renderer/src/lib/resume-sleeping-agent-session-direct-ssh-hydration-gap.test.ts
  • src/renderer/src/lib/resume-sleeping-agent-session-legacy-worker.test.ts
  • src/renderer/src/lib/resume-sleeping-agent-session-remote-compat.test.ts
  • src/renderer/src/lib/resume-sleeping-agent-session-replay.test.ts
  • src/renderer/src/lib/resume-sleeping-agent-session.test.ts
  • src/renderer/src/lib/resume-sleeping-agent-session.ts
  • src/renderer/src/lib/sleeping-agent-session-launch.ts
  • src/renderer/src/lib/workspace-session-resume-fence-hydration.test.ts
  • src/renderer/src/lib/workspace-session-snapshot.ts
  • src/renderer/src/lib/workspace-session.ts
  • src/renderer/src/lib/worktree-reactivation-preserved-pane-replacement.test.ts
  • src/renderer/src/lib/worktree-reactivation-tab-forkbomb.test.ts
  • src/renderer/src/store/slices/agent-sleep-resume-fence-races.test.ts
  • src/renderer/src/store/slices/agent-status-manual-sleep-capture.test.ts
  • src/renderer/src/store/slices/agent-status-open-tab-resume-fence.test.ts
  • src/renderer/src/store/slices/agent-status-provider-session-actions.ts
  • src/renderer/src/store/slices/agent-status-provider-session.test.ts
  • src/renderer/src/store/slices/agent-status-recovery-actions.ts
  • src/renderer/src/store/slices/agent-status-recovery-collection.ts
  • src/renderer/src/store/slices/agent-status-sleeping-records.ts
  • src/renderer/src/store/slices/agent-status-slice-contract.ts
  • src/renderer/src/store/slices/agent-status.ts
  • src/renderer/src/store/slices/store-sleep-pane-hibernation.test.ts
  • src/renderer/src/store/slices/workspace-scoped-resume-fence-hydration.test.ts
  • src/renderer/src/store/terminals/terminal-pane-hibernation.ts
  • src/renderer/src/store/terminals/workspace-terminal-hydration-patch.ts
  • src/renderer/src/store/terminals/workspace-terminal-hydration.ts
  • src/renderer/src/web/preload-api/web-agent-status-api.ts
  • src/renderer/src/web/preload-api/web-app-api.ts
  • src/shared/protocol-version.ts
  • src/shared/pty-attach-absence-evidence.ts
  • src/shared/remote-runtime-client-capabilities.ts
  • src/shared/runtime-terminal-contracts.ts
  • src/shared/terminal-fenced-create-capability.test.ts
  • src/shared/terminal-pane-resume-fence.test.ts
  • src/shared/terminal-pane-resume-fence.ts
  • src/shared/workspace-session-host-field-ownership.ts
  • src/shared/workspace-session-resume-fences.ts
  • src/shared/workspace-session-schema-field-coverage.test.ts
  • src/shared/workspace-session-schema.ts
  • src/shared/workspace-session-state-types.ts
  • tests/e2e/completed-worker-retirement-resume.spec.ts
  • tests/e2e/helpers/completed-worker-retirement-fixture.ts
  • tests/e2e/settled-worker-resume-fence-restart.spec.ts
  • tests/e2e/settled-worker-tab-survives-restart.spec.ts
  • tests/fixtures/terminal/README.md
  • tests/fixtures/terminal/pr-base-1d1b73c40850-pty-transport.txt
💤 Files with no reviewable changes (7)
  • src/renderer/src/lib/sleeping-agent-session-launch.ts
  • src/renderer/src/store/slices/agent-status-provider-session-actions.ts
  • tests/e2e/completed-worker-retirement-resume.spec.ts
  • src/renderer/src/store/slices/agent-status-recovery-actions.ts
  • src/renderer/src/components/terminal-pane/pty-connection/retained-legacy-pty-attach.ts
  • src/renderer/src/components/terminal-pane/pty-connection/run-deferred-connect.ts
  • src/renderer/src/store/slices/agent-status-recovery-collection.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.

Comment on lines +75 to +77
if (ctx.result.exitedBeforeAttach || ctx.result.reattachUnverifiable) {
return
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Reset the provisional execution context before this return.

This return bypasses the cleanup at Lines 123-130. If preparePtyExecutionContext ran and spawnForStablePane returns exitedBeforeAttach or reattachUnverifiable, the runtime retains the provisional context for ctx.effectiveSessionAppId.

Apply the same reset used by the catch path before returning. This prevents stale execution state after an attach-only outcome.

Proposed fix
 if (ctx.result.exitedBeforeAttach || ctx.result.reattachUnverifiable) {
+  if (
+    (ctx.isMintedSessionId || ctx.preparedProvisionalExecutionContext) &&
+    ctx.effectiveSessionAppId
+  ) {
+    ctx.deps.runtime?.preparePtyExecutionContext?.(ctx.effectiveSessionAppId, null, {
+      resetIncarnation: true
+    })
+  }
   return
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (ctx.result.exitedBeforeAttach || ctx.result.reattachUnverifiable) {
return
}
if (ctx.result.exitedBeforeAttach || ctx.result.reattachUnverifiable) {
if (
(ctx.isMintedSessionId || ctx.preparedProvisionalExecutionContext) &&
ctx.effectiveSessionAppId
) {
ctx.deps.runtime?.preparePtyExecutionContext?.(ctx.effectiveSessionAppId, null, {
resetIncarnation: true
})
}
return
}

blockedPanes: [{ paneKey: 'a', worktreeId: 'local', contractVersion: 1, settled: true }]
}
p.prepare()
expect(notify).toHaveBeenCalled()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Make the retry assertion detect the second announcement.

Line 49 already asserts one notify call. After the second prepare(), expect(notify).toHaveBeenCalled() passes from that first call alone. The retry announcement is not verified, so a regression that stops publishing after the sibling host disappears still passes.

Assert the exact call count, or reset the mock before the second pass.

💚 Proposed test fix
   fail = false
   plan = {
     ...plan,
     blockedPanes: [{ paneKey: 'a', worktreeId: 'local', contractVersion: 1, settled: true }]
   }
+  notify.mockClear()
   p.prepare()
-  expect(notify).toHaveBeenCalled()
+  expect(notify).toHaveBeenCalledOnce()

Comment on lines +91 to +93
if (spawnResult.exitedBeforeAttach || spawnResult.reattachUnverifiable) {
return context.isDestroyed() ? undefined : spawnResult
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
# Description: Resolve PtyConnectResult and the projection helper to check the refusal return shape.
set -euo pipefail

# Definition of the transport result contract.
fd -t f 'pty-transport-types.ts' src/renderer --exec ast-grep outline {} --items all

echo '=== PtyConnectResult declaration ==='
rg -nP -A30 'type\s+PtyConnectResult\b' --type=ts src/renderer

echo '=== projectIpcPtyConnectResult implementation ==='
ast-grep run --lang typescript --pattern 'function projectIpcPtyConnectResult($$$) { $$$ }' src/renderer

echo '=== consumers that read the refusal flags ==='
rg -nP -C3 '\b(reattachUnverifiable|exitedBeforeAttach)\b' --type=ts src/renderer src/shared

Repository: stablyai/orca

Length of output: 5065


🤖 get_repo_knowledge executed:

get_repo_knowledge stablyai/orca /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4/learnings

Length of output: 34610


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== changed connect path ==='
cat -n src/renderer/src/components/terminal-pane/ipc-pty-connect.ts | sed -n '45,180p'

echo '=== projection helper definitions and call sites ==='
rg -n -C5 'projectIpcPtyConnectResult' src

echo '=== refusal flag usages ==='
rg -n -C4 '\b(reattachUnverifiable|exitedBeforeAttach)\b' src/renderer/src/components/terminal-pane src/shared

Repository: stablyai/orca

Length of output: 47075


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== projection implementation ==='
cat -n src/renderer/src/components/terminal-pane/ipc-pty-connect-result.ts | sed -n '1,120p'

echo '=== spawn result contract and refusal construction ==='
rg -n -C8 'spawnIpcPty|IpcPtySpawnResult|exitedBeforeAttach|reattachUnverifiable' \
  src/renderer/src/components/terminal-pane/ipc-pty-spawn-request.ts \
  src/renderer/src/components/terminal-pane/ipc-pty-connect-result.ts

Repository: stablyai/orca

Length of output: 6832


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n src/renderer/src/components/terminal-pane/ipc-pty-connect-result.ts

Repository: stablyai/orca

Length of output: 3154


🏁 Script executed:

cat -n src/renderer/src/components/terminal-pane/ipc-pty-connect-result.ts

Repository: stablyai/orca

Length of output: 3154


Return a narrow refusal result.

PtyConnectResult declares both refusal flags. This branch returns IpcPtySpawnResponse before projectIpcPtyConnectResult, so it can forward optional snapshot, replay, cold-restore, or internal incarnation fields. Return only id and the applicable refusal flag, consistent with the other refusal branch.

)
const legacyWorkerOwnsPane = session.isLegacyWorkerAutomaticResumeBlocked()
if (gate.enterDeferredFlow && (!legacyWorkerOwnsPane || !gate.sshConnected)) {
if (gate.enterDeferredFlow) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep a fenced binding until main admits replacement.

Line 63 now sends fenced SSH panes into this flow. If the output callback records isSshSessionGoneError and connect() resolves undefined, the later branch clears the layout and tab PTY binding before it starts recovery. A fence can then reject replacement and leave the pane without its retained binding.

Route this fenced outcome through recoverUnverifiableReattach, or defer binding removal until main returns an explicit admitted or terminal result.

Comment on lines +31 to +32
return (error instanceof Error ? error.message : String(error)).includes(
SESSION_NOT_FOUND_MESSAGE_PREFIX

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Match the owner-authored marker at the start of the message.

decodeDaemonResponseError and SessionNotFoundError treat SESSION_NOT_FOUND_MESSAGE_PREFIX as a prefix, but this helper accepts it anywhere. A different provider or transport error can therefore pass isPtyAlreadyGoneError as definitive session absence and enter the wrong recovery path. Use startsWith here, or preserve an explicit owner marker through every wrapper.

Proposed fix
-  return (error instanceof Error ? error.message : String(error)).includes(
+  return (error instanceof Error ? error.message : String(error)).startsWith(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (error instanceof Error ? error.message : String(error)).includes(
SESSION_NOT_FOUND_MESSAGE_PREFIX
return (error instanceof Error ? error.message : String(error)).startsWith(
SESSION_NOT_FOUND_MESSAGE_PREFIX

Comment on lines +9 to +16
return (
session.legacyWorkerResumeFencesByPaneKey ??
Object.fromEntries(
Object.entries(session.sleepingAgentSessionsByPaneKey ?? {})
.filter(([, record]) => record.automaticResumeBlockedBy === 'legacy-orchestration-worker')
.map(([key]) => [key, true as const])
)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
# Description: Find callers of readWorkspaceSessionResumeFences that mutate or store the returned map.
set -euo pipefail

echo '=== call sites ==='
rg -nP -C6 '\breadWorkspaceSessionResumeFences\s*\(' --type=ts

echo '=== direct writes to the canonical field ==='
rg -nP -C4 'legacyWorkerResumeFencesByPaneKey\s*(\[|\.|=)' --type=ts

Repository: stablyai/orca

Length of output: 170


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== target file ==='
cat -n src/shared/workspace-session-resume-fences.ts

echo '=== matching symbol and related field references ==='
rg -n -P -C5 'readWorkspaceSessionResumeFences|legacyWorkerResumeFencesByPaneKey|sleepingAgentSessionsByPaneKey' --type=ts .

Repository: stablyai/orca

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail
log=/tmp/coderabbit-shell-logs/shell-output-thjD5A

echo '=== target implementation from captured output ==='
sed -n '1,80p' "$log"

echo '=== reader references only ==='
rg -n -C8 'readWorkspaceSessionResumeFences' "$log" || true

echo '=== canonical field references in non-test source ==='
rg -n -C5 'legacyWorkerResumeFencesByPaneKey' src --type=ts \
  -g '!*.test.ts' -g '!*.test.tsx' | head -n 240

Repository: stablyai/orca

Length of output: 47246


🤖 get_repo_knowledge executed:

get_repo_knowledge stablyai/orca /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4/learnings /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4/conventions

Length of output: 14187


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== hydration caller ==='
sed -n '1,225p' src/renderer/src/store/terminals/workspace-terminal-hydration.ts

echo '=== persistence caller and writer ==='
sed -n '160,245p' src/main/runtime/runtime-legacy-worker-terminal-recovery-persistence.ts

echo '=== shared predicate caller ==='
cat -n src/shared/terminal-pane-resume-fence.ts

echo '=== non-test renderer updates to the canonical field ==='
rg -n -C6 'legacyWorkerResumeFencesByPaneKey' src/renderer/src --type=ts \
  -g '!*.test.ts' -g '!*.test.tsx' | head -n 320

Repository: stablyai/orca

Length of output: 44220


Return a copy on the canonical branch so callers cannot mutate the source session.

readWorkspaceSessionResumeFences returns session.legacyWorkerResumeFencesByPaneKey by reference when present, but creates a new object on the fallback branch. Because the return type is mutable, a caller that mutates the result can modify the source session in place. Return a copy on both branches to keep the contract uniform.

🛡️ Proposed fix
 ): Record<string, true> {
-  return (
-    session.legacyWorkerResumeFencesByPaneKey ??
-    Object.fromEntries(
-      Object.entries(session.sleepingAgentSessionsByPaneKey ?? {})
-        .filter(([, record]) => record.automaticResumeBlockedBy === 'legacy-orchestration-worker')
-        .map(([key]) => [key, true as const])
-    )
-  )
+  const canonical = session.legacyWorkerResumeFencesByPaneKey
+  if (canonical) {
+    return { ...canonical }
+  }
+  return Object.fromEntries(
+    Object.entries(session.sleepingAgentSessionsByPaneKey ?? {})
+      .filter(([, record]) => record.automaticResumeBlockedBy === 'legacy-orchestration-worker')
+      .map(([key]) => [key, true as const])
+  )
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
session.legacyWorkerResumeFencesByPaneKey ??
Object.fromEntries(
Object.entries(session.sleepingAgentSessionsByPaneKey ?? {})
.filter(([, record]) => record.automaticResumeBlockedBy === 'legacy-orchestration-worker')
.map(([key]) => [key, true as const])
)
)
const canonical = session.legacyWorkerResumeFencesByPaneKey
if (canonical) {
return { ...canonical }
}
return Object.fromEntries(
Object.entries(session.sleepingAgentSessionsByPaneKey ?? {})
.filter(([, record]) => record.automaticResumeBlockedBy === 'legacy-orchestration-worker')
.map(([key]) => [key, true as const])
)

Comment on lines +63 to +65
// Why re-materialized rather than written once at import: the module is shared across spec files in
// a worker, and the first file's `cleanupCompletedWorkerFixture` removes the whole directory. A
// later file in the same worker would otherwise launch a `codex` that no longer exists.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the stale reference to the removed cleanupCompletedWorkerFixture export.

This change deletes cleanupCompletedWorkerFixture and replaces it with the process.once('exit', ...) handler at Line 111. The comment still names that export as the reason for re-materializing the directory. It also contradicts the comment at Lines 106-110, which states that only the process may end the directory's lifetime. A reader can conclude that a per-file teardown still exists and re-add one.

📝 Proposed comment fix
-// Why re-materialized rather than written once at import: the module is shared across spec files in
-// a worker, and the first file's `cleanupCompletedWorkerFixture` removes the whole directory. A
-// later file in the same worker would otherwise launch a `codex` that no longer exists.
+// Why re-materialized rather than written once at import: the module is shared across spec files in
+// a worker, and the temp directory can be gone by the time a later spec runs (an OS temp sweep, or a
+// stray teardown). A later file in the same worker would otherwise launch a `codex` that no longer
+// exists. Only the process-exit handler below may remove the directory.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Why re-materialized rather than written once at import: the module is shared across spec files in
// a worker, and the first file's `cleanupCompletedWorkerFixture` removes the whole directory. A
// later file in the same worker would otherwise launch a `codex` that no longer exists.
// Why re-materialized rather than written once at import: the module is shared across spec files in
// a worker, and the temp directory can be gone by the time a later spec runs (an OS temp sweep, or a
// stray teardown). A later file in the same worker would otherwise launch a `codex` that no longer
// exists. Only the process-exit handler below may remove the directory.
🧰 Tools
🪛 ast-grep (0.45.2)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFileSync } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)


await session.close(firstApp)
firstApp = null
expect(readPersistedWorkerResumeFence(session.userDataDir, workerPaneKey)).toBe(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
# Description: Determine whether the restart helper's close() awaits the profile session flush.
set -euo pipefail

fd -t f 'orca-restart' tests/e2e/helpers | while IFS= read -r file; do
  echo "===== $file ====="
  cat -n "$file"
done

# Show every other e2e read of the persisted fence, polled or sampled.
rg -n -C4 'readPersistedWorkerResumeFence' tests

Repository: stablyai/orca

Length of output: 18868


🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -t f . tests/e2e/helpers | rg 'electron-process-shutdown|completed-worker-retirement-fixture'
for file in $(fd -t f . tests/e2e/helpers | rg 'electron-process-shutdown|completed-worker-retirement-fixture'); do
  echo "===== $file ====="
  ast-grep outline "$file" || true
done

echo "===== shutdown helper implementation ====="
file=$(fd -t f . tests/e2e/helpers | rg 'electron-process-shutdown' | head -n1)
cat -n "$file"

echo "===== persisted fence reader ====="
file=$(fd -t f . tests/e2e/helpers | rg 'completed-worker-retirement-fixture' | head -n1)
sed -n '190,225p' "$file"

Repository: stablyai/orca

Length of output: 12939


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "===== closeElectronAppForE2E references ====="
rg -n -C5 'function closeElectronAppForE2E|const closeElectronAppForE2E|export .*closeElectronAppForE2E|closeElectronAppForE2E' tests src

echo "===== Electron close contract references ====="
rg -n -C4 '\.close\(\)|app\.close|beforeunload|session.*flush|persist.*session|orca-data\.json' tests/e2e src | head -n 300

Repository: stablyai/orca

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "===== fence/session persistence definitions ====="
rg -n -C6 'legacyWorkerResumeFencesByPaneKey|resumeFence|beforeunload|workspaceSession' src tests/e2e \
  -g '*.ts' -g '*.tsx' | head -n 400

echo "===== app quit and session persistence wiring ====="
rg -n -C8 'app\.on\(['"'"'"]before-quit|window-all-closed|will-quit|beforeunload|persist.*session|session.*persist|writeFile.*orca-data|orca-data\.json' src \
  -g '*.ts' -g '*.tsx' | head -n 500

Repository: stablyai/orca

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "===== targeted fence/session persistence matches ====="
rg -n -C5 'legacyWorkerResumeFencesByPaneKey|resumeFence' src tests/e2e -g '*.ts' -g '*.tsx'

echo "===== targeted quit/persistence matches ====="
rg -n -C6 'beforeunload|before-quit|will-quit|persist.*session|session.*persist|orca-data\.json' src -g '*.ts' -g '*.tsx'

Repository: stablyai/orca

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "===== all declarations related to persisted worker fences ====="
rg -n -C3 'legacyWorkerResumeFencesByPaneKey|set.*ResumeFence|resume.*Fence|worker.*Fence' src -g '*.ts' -g '*.tsx' | head -n 500

echo "===== renderer-to-main persistence/quit hooks ====="
rg -n -C5 'beforeunload|sendBeacon|ipcRenderer|contextBridge|app\.quit|app\.exit|BrowserWindow.*close|close.*window|persist' src/main src/renderer -g '*.ts' -g '*.tsx' 2>/dev/null | head -n 500

Repository: stablyai/orca

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "===== renderer shutdown persistence ====="
sed -n '90,220p' src/renderer/src/app-shell/use-app-session-persistence.ts

echo "===== shutdown checkpoint implementation ====="
cat -n src/renderer/src/app-shell/shutdown-checkpoint-persist.ts
cat -n src/renderer/src/app-shell/shutdown-checkpoint-guard.ts | sed -n '1,260p'

echo "===== runtime fence write and store flush contract ====="
cat -n src/main/runtime/runtime-legacy-worker-terminal-recovery-persistence.ts | sed -n '1,125p'
rg -n -C8 'setWorkspaceSession|flushPendingOrThrowAsync|waitForPendingWrite|stageWorkspaceSessionBeforeUnload' src/main/persistence src/main -g '*.ts' | head -n 350

Repository: stablyai/orca

Length of output: 12541


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "===== stageBeforeUnloadSync bindings ====="
rg -n -C10 'stageBeforeUnloadSync' src -g '*.ts' -g '*.tsx'

echo "===== shutdown handler registration ====="
sed -n '220,330p' src/renderer/src/app-shell/use-app-session-persistence.ts
rg -n -C8 'createShutdownCheckpointBeforeUnloadHandler|onWindowCloseRequested|beforeunload' src/renderer/src/app-shell src/main -g '*.ts' -g '*.tsx' | head -n 300

echo "===== runtime session write scheduling ====="
rg -n -C10 'setWorkspaceSession\s*=|setWorkspaceSession\(|flushPendingOrThrowAsync|waitForPendingWrite|writeWorkspaceSession|stageWorkspaceSessionBeforeUnload' src/main/persistence src/main/runtime -g '*.ts' | head -n 400

Repository: stablyai/orca

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "===== preload app bridge ====="
file=$(rg -l 'stageBeforeUnloadSync' src/preload src/main src/renderer/src/web -g '*.ts' | head -n 1)
echo "$file"
cat -n "$file"

echo "===== main IPC registration ====="
rg -l 'stageBeforeUnloadSync|stage-before-unload|beforeUnloadSync' src/main src/preload -g '*.ts' | while IFS= read -r file; do
  echo "===== $file ====="
  rg -n -C12 'stageBeforeUnloadSync|stage-before-unload|beforeUnloadSync' "$file"
done

echo "===== Store synchronous staging implementation ====="
rg -l 'stageWorkspaceSessionBeforeUnload|stageBeforeUnload' src/main -g '*.ts' | while IFS= read -r file; do
  echo "===== $file ====="
  rg -n -C15 'stageWorkspaceSessionBeforeUnload|stageBeforeUnload' "$file"
done

Repository: stablyai/orca

Length of output: 36642


Poll the persisted fence read instead of sampling it once.

session.close waits for app close and process exit, but the shutdown handler only starts the asynchronous persistence flush. It does not await app:await-before-unload-checkpoint. The single read can therefore observe stale disk state.

♻️ Proposed change
-      expect(readPersistedWorkerResumeFence(session.userDataDir, workerPaneKey)).toBe(true)
+      await expect
+        .poll(() => readPersistedWorkerResumeFence(session.userDataDir, workerPaneKey), {
+          message: 'the runtime-authored fence never reached disk'
+        })
+        .toBe(true)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(readPersistedWorkerResumeFence(session.userDataDir, workerPaneKey)).toBe(true)
await expect
.poll(() => readPersistedWorkerResumeFence(session.userDataDir, workerPaneKey), {
message: 'the runtime-authored fence never reached disk'
})
.toBe(true)

@@ -0,0 +1,10 @@
`pr-base-1d1b73c40850-pty-transport.txt` is the exact
`src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts` from PR
#19358's base commit `1d1b73c40850`, excluding only its leading max-lines lint

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Reflow Line 3 so the issue reference does not start the line.

#19358 begins Line 3, so markdownlint parses it as a malformed ATX heading and reports MD018. Move the reference off the line start to clear the rule without changing the text.

🔧 Proposed fix
 `pr-base-1d1b73c40850-pty-transport.txt` is the exact
 `src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts` from PR
-#19358's base commit `1d1b73c40850`, excluding only its leading max-lines lint
-suppression comment. Keep this historical reader frozen.
+#19358's base commit `1d1b73c40850`, excluding only its leading
+max-lines lint suppression comment. Keep this historical reader frozen.

The reference still needs to leave the line start. Use this form instead:

`pr-base-1d1b73c40850-pty-transport.txt` is the exact
`src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts` from
PR `#19358`'s base commit `1d1b73c40850`, excluding only its leading max-lines
lint suppression comment. Keep this historical reader frozen.
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 3-3: No space after hash on atx style heading

(MD018, no-missing-space-atx)

Source: Linters/SAST tools

… module

A dynamic store import inside the refresh was the workaround for the
store -> hydration -> refresh -> store cycle. The counter itself has no
dependencies, so it lives alone and both writers import it statically.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/ipc/pty/runtime/spawn-execute.ts (1)

217-220: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clear the provisional execution context on StablePaneResumeBlockedError.

cancelPendingPtyRegistration only removes registration state. It does not clear the execution context prepared for ctx.effectiveSessionAppId. This return bypasses the guarded cleanup that calls preparePtyExecutionContext(..., null, { resetIncarnation: true }), so WSL and headless-terminal state can remain after refusal. Run the same guarded cleanup before returning. Extend the fresh-pane fence test to assert this cleanup.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: e5dec47a-6c4c-4cbb-af02-a8e18970a5e8

📥 Commits

Reviewing files that changed from the base of the PR and between 2120f09 and 69ea26a.

📒 Files selected for processing (23)
  • src/main/ipc/pty-attach-only-fenced-pane-spawn.test.ts
  • src/main/ipc/pty-resume-claim-ensure-fence.test.ts
  • src/main/ipc/pty-ssh-undelivered-kill.test.ts
  • src/main/ipc/pty/ipc/renderer-kill.ts
  • src/main/ipc/pty/ipc/spawn-begin.ts
  • src/main/ipc/pty/ipc/spawn-execute.ts
  • src/main/ipc/pty/pane/hibernation-admission.ts
  • src/main/ipc/pty/pane/stable-owner.ts
  • src/main/ipc/pty/pane/stable-pane-resume-fence-host.test.ts
  • src/main/ipc/pty/pane/stable-pane-resume-fence.ts
  • src/main/ipc/pty/runtime/kill.ts
  • src/main/ipc/pty/runtime/spawn-execute.ts
  • src/main/ipc/pty/runtime/spawn.ts
  • src/main/persistence/loading-store/store-runtime-authored-session-fences.test.ts
  • src/main/persistence/runtime-authored-workspace-session-fields.ts
  • src/main/runtime/mobile-session-terminal-persistence-retirement.ts
  • src/renderer/src/components/terminal-pane/pty-connection/reattach-result-handler.ts
  • src/renderer/src/lib/automatic-resume-spawn-settlement.test.ts
  • src/renderer/src/lib/automatic-resume-spawn-settlement.ts
  • src/renderer/src/lib/legacy-worker-resume-fence-generation.ts
  • src/renderer/src/lib/legacy-worker-resume-fence-refresh.test.ts
  • src/renderer/src/lib/legacy-worker-resume-fence-refresh.ts
  • src/renderer/src/store/terminals/workspace-terminal-hydration.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — minor suggestions inline.

Reviewed changes — the round-1 follow-ups that move main's fence decisions onto the layer that owns each identity.

  • Consolidated resume admission — isFreshPaneResumeBlocked (source or destination) now runs at the provider.spawn site (spawnForStablePane) and in the agentSessionEnsure claim callback, replacing the four redundant entry checks; stablePaneOwnerCandidate is now resolved for claims too.
  • Retained fenced identity at the write boundary — preserveRuntimeAuthoredWorkspaceSessionFields re-adds a fenced pane's sleeping record when a renderer write drops it, so duplicate cleanup can no longer delete the identity main uses to map a resume source; retireTerminalSurfaceFromPersistence drops the fence entry so the record can be removed afterward.
  • Hardened hibernation admission — assertPtyHibernationAllowed iterates fenced pane keys directly and refuses when either the live runtime owner or the persisted owner is the target PTY, so a stale persisted binding can no longer hide the live owner.
  • Kept user-owned tabs on refusal — handleReattachResult clears only the resume claim (keeping the tab) when input was already accepted or preconnect input exists, instead of closing the tab.
  • Generation-based reply supersession — the renderer fence refresh drops replies older than a later request or hydration via an import-free generation counter, so a stale positive fence reply cannot overwrite a newer hydration.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

Comment thread src/main/ipc/pty/runtime/spawn-execute.ts

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — the round-2 follow-up that removes the renderer's fence veto and preserves resume ownership.

  • Switched the resume veto to the durable record flag — resumeSleepingAgentSessionsForWorktree now skips a record on automaticResumeBlockedBy === 'legacy-orchestration-worker' (hydrated atomically with the record) instead of the advisory legacyWorkerResumeFencesByPaneKey set, so the renderer no longer depends on a racy fence mirror.
  • Reverted the generation counter — legacy-worker-resume-fence-generation.ts is deleted and refreshLegacyWorkerResumeFences no longer drops stale replies; with the fence set now purely advisory, a stale reply can only cause reversible misses, and main remains the authority on launch/kill.
  • Preserved resume ownership — settleAutomaticResumeSpawn writes launchAgent + resumeProviderSession onto the admitted tab (new optional persisted field on TerminalTab, schema .catch(undefined)), and activeOrQueuedResumeClaimsProviderSession matches tab/pending-startup/claim ownership so the sweep dedupes correctly after the claim clears.
  • Hardened hibernation admission — assertPtyHibernationAllowed resolves the live owner via runtime.resolveTerminalPane(paneKey) and throws terminal_pane_owner_host_mismatch on a host mismatch, or StablePaneResumeBlockedError for a fenced owner, falling back to the persisted owner.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@Jinwoo-H

Jinwoo-H commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Superseded: the settled-worker resume fence is being deleted as a product decision (finished workers resume exactly like any other agent terminal). Replacement branch: settled-worker-policy.

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.

1 participant