Skip to content

fix(pty): keep settled orchestration worker tabs open after an app restart - #19320

Closed
Jinwoo-H wants to merge 2 commits into
mainfrom
fix/settled-worker-tab-close
Closed

Jinwoo-H wants to merge 2 commits into
mainfrom
fix/settled-worker-tab-close

Conversation

@Jinwoo-H

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

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​540 $\color{#cf222e}{\Huge{\mathbf{−}}}$​2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​538
Prod 3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​29 $\color{#cf222e}{\Huge{\mathbf{−}}}$​3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​26

Release blocker from the Slack thread "disappearing orchestrator-worker tabs" (P0+). Clicking a completed orchestration worker's tab after an app restart removed the tab while the daemon still ran the process. Regression trigger: #16904, which extended the legacy-worker resume fence to settled workers whose terminal is still owned and neither released nor retained.

ELI5

After a restart, a finished worker's tab re-attaches to its live shell without telling main's daemon adapter. When the user reveals the tab, the renderer asks main "is this PTY alive?" Main looked only in its own attach cache, found nothing, and said "no" with full confidence. The renderer treats that "no" as a death certificate and closes the tab.

Root cause

  • pty:hasPty answered from DaemonPtyAdapter.hasPty, an in-process cache of the sessions this process attached.
  • A fenced (automaticResumeBlockedBy: legacy-orchestration-worker) pane takes the renderer-only attachRetainedLegacyPty path, which never warms that cache.
  • shouldReconcileMissingSession reconciles on exactly false, so the cache miss became tab teardown on hidden→visible.

Before #16904 settled workers never entered the fenced path, so the latent cache/attach mismatch was unreachable for them.

Fix

pty:hasPty now resolves presence through the provider's authoritative readback (probePtyLiveness) whenever the cache misses. A cache hit stays a sync answer; a provider without a readback (in-process LocalPtyProvider) keeps its cache as the process table; an unreachable daemon answers null, never false. DaemonPtyAdapter.probePtyLiveness connects first when the adapter has never connected, otherwise a fresh app process would answer null for every session an older process spawned.

Verdict vocabulary unchanged: only an owner's readback may say false (docs/reference/ssh-execution-boundary.md).

Verification

  • New src/main/ipc/pty-has-pty-cache-miss-presence.test.ts: cache miss → readback true/false/null; cache hit skips the readback; readback-less sole provider stays authoritative.
  • New src/main/daemon/daemon-pty-adapter-unattached-presence.test.ts: real daemon over a temp socket, a second adapter that never attached answers live / absent / absent-after-exit.
  • New tests/e2e/settled-worker-tab-survives-restart.spec.ts: starts a real worker, settles it with worker_done, verifies the fence stamped, quits and relaunches the app on the same profile, reveals the worker's worktree, and asserts no store transition ever dropped the tab or its leaf binding.
    • Fixed bundle: 3/3 passes.
    • Same bundle rebuilt with origin/main's inspect.ts: fails on both assertions (hasPty → false, transitions show leaf cleared → tab removed → worktree deselected → tab re-added by graph sync), which matches the videos in the thread.
  • The Slack thread's own repro (reproduce-findings-tab-disappearance.test.ts) passes when routed through the new helper; it is not committed because it depends on git show of historical refs.
  • pnpm tc:node, check:code-quality:changed, and 556 test files across src/main/ipc, src/main/daemon, renderer reconcile, and worker-recovery suites pass.

Not changed

  • The renderer-only attach path itself. Making main attach the daemon session for a fenced pane would also work but changes output routing for a retained worker; this PR fixes the fabricated verdict, which is the boundary rule the reconciler already relies on.

A retained orchestration worker's pane re-attaches renderer-only after an
app restart, so the daemon adapter's hasPty cache never learns the session.
The visibility reconciler tore the tab down on that fabricated false while
the daemon still ran the process (#16904 regression: settled workers now
enter the retained path). pty:hasPty now asks the provider's readback
before answering absent; a cache hit stays a cheap sync answer.
@coderabbitai

coderabbitai Bot commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds daemon-backed PTY presence resolution for cache misses. Disconnected daemon clients reconnect before liveness probes. IPC tests cover cache hits, daemon readback, unreachable daemons, and absent sessions. A new end-to-end test verifies that a settled worker tab and its PTY remain available after an application restart.

Merge Risk: 🔵 Low · up to a2649

The restart test can pass without confirming that revealing the worker tab restores its terminal binding. Tighten this assertion before merge to preserve regression coverage.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the problem, root cause, fix, scope, and verification in detail. However, it omits required template content, including the Linked Issue entry, Visual Proof or an explicit N/A… Add the required template sections. Include a valid issue reference after “Fixes #”, provide before-and-after visual proof or write “N/A” with a reason, complete the Testing checklist, and address the Agent skill upstream boundary, Notes, a…
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 7 functions across 7 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 identifies the primary fix: keeping settled orchestration worker tabs open after an app restart. It is concise and related to the changes.
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 explains the problem, root cause, fix, scope, and verification in detail. However, it omits required template content, including the Linked Issue entry, Visual Proof or an explicit N/A statement, the Testing checklist, and the remaining checklist sections.

Resolution

Add the required template sections. Include a valid issue reference after “Fixes #”, provide before-and-after visual proof or write “N/A” with a reason, complete the Testing checklist, and address the Agent skill upstream boundary, Notes, and final Checklist items.

  • 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: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: 50deaace-1ee7-49d6-a24b-47d84796ae2b

📥 Commits

Reviewing files that changed from the base of the PR and between 83b1558 and a26496e.

📒 Files selected for processing (7)
  • src/main/daemon/daemon-pty-adapter-protocol-compatibility.test.ts
  • src/main/daemon/daemon-pty-adapter-unattached-presence.test.ts
  • src/main/daemon/daemon-pty-session-control.ts
  • src/main/ipc/pty-has-pty-cache-miss-presence.test.ts
  • src/main/ipc/pty/ipc/inspect.ts
  • src/main/ipc/pty/provider/liveness.ts
  • tests/e2e/settled-worker-tab-survives-restart.spec.ts

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

Comment on lines +366 to +373
expect(transitions.at(-1)?.activeWorktreeId).toBe(targetWorktreeId)
expect(
await second.page.evaluate((tabId) => Boolean(window.__paneManagers?.get(tabId)), workerTabId)
).toBe(true)
expect(
(await listRuntimeTerminals(client)).find((terminal) => terminal.ptyId === workerPtyId)
?.connected
).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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the renderer PTY binding after reveal.

backgroundMountTab mounts workerTabId without changing selection. The pane-manager and runtime-terminal checks do not prove that the reveal selected the worker tab or bound its active pane to workerPtyId. Assert activeTabIdByWorktree[targetWorktreeId] equals workerTabId, then compare await waitForActivePanePtyId(second.page) with workerPtyId.

@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

  • fix(pty): answer pty:hasPty from the daemon readback on a cache miss — pty:hasPty now resolves presence through resolveProviderPtyPresence, so a daemon-adapter cache miss consults the authoritative probePtyLiveness readback instead of fabricating a false; a cache hit stays a sync answer and a provider without a readback keeps its cache authoritative.
  • ensureConnected guard in DaemonPtyAdapter.probePtyLiveness — a fresh app process connects to the daemon before probing so it reads real liveness for sessions an older process spawned, rather than answering null from an empty attach cache.
  • New unit coverage (daemon-pty-adapter-unattached-presence, pty-has-pty-cache-miss-presence) plus an e2e restart/reveal spec that asserts the settled worker tab never leaves the store.

I verified the boundary contract end to end. The false fallback in resolveProviderPtyPresence (for a provider with hasPty but no probePtyLiveness) is only reachable by LocalPtyProvider (whose cache is the process table, so false is authoritative) and SshPtyProvider (which has no readback and whose behavior is unchanged); the daemon router/adapter both expose probePtyLiveness. The daemon's getSize readback returns null only for a missing session, so the probe cannot report a false-absent for a live PTY. Verdict vocabulary is preserved: false only on an owner readback, null when unverifiable. No daemon wire/protocol change is introduced (getSize/listSessions already exist), so there is no remote-wire compatibility concern.

ℹ️ probePtyLiveness now establishes a connection, not just a read

The new if (!this.client.isConnected()) guard means a "liveness probe" is no longer a pure RPC against an open socket — on a fresh adapter it performs the full connection lifecycle (setupEventRouting, scheduleCheckpointTimer, background resync) via the adapter-level ensureConnected. I traced the other callers (reapRestoredSubagentsWithoutLiveAgent, the runtime controller, and DaemonPtyRouter.ownerResolver.probe) and all are either already connected or safe to connect, and the connect is bounded by LIVENESS_PROBE_TIMEOUT_MS. Noting only because a method named "probe" having connection side effects is worth keeping in mind for future callers; nothing requires a change.

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

@Jinwoo-H

Jinwoo-H commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #19358, which fixes the same P0+ at the owning layer (main's pty:spawn attach path) instead of guarding pty:hasPty. Branch kept for reference.

@Jinwoo-H Jinwoo-H closed this Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant