Skip to content

fix(runtime): make terminal tab closure durable#8774

Closed
chsong1 wants to merge 3 commits into
stablyai:mainfrom
chsong1:chsong/orca-terminal-close-tab-r01
Closed

fix(runtime): make terminal tab closure durable#8774
chsong1 wants to merge 3 commits into
stablyai:mainfrom
chsong1:chsong/orca-terminal-close-tab-r01

Conversation

@chsong1

@chsong1 chsong1 commented Jul 14, 2026

Copy link
Copy Markdown

Summary

  • keep the explicit orca terminal close --tab lifecycle primitive while making final tab closure durable across persistence and runtime reconstruction
  • fence stopped-terminal selectors so stale terminal IDs cannot alias browser/file companions or resurrect closed tabs
  • preserve foreground workspace switching, final-surface landing, mixed legacy browser/file recovery, durable close receipts, bounded RPC errors, and rollback semantics
  • leave visible UI source unchanged

Why

Completed automation runs were closing the PTY but leaving or reconstructing the renderer tab. Repeated schedules therefore accumulated completed worktrees and tabs in Orca. The durable close path now validates the exact terminal identity, commits a replay-safe projection, and prevents stale selectors from recreating the closed container.

Verification

  • changed-path cohort: 518 tests passed
  • independent runtime/integration audit: 531 focused tests and 675 runtime tests passed
  • worker full-runtime gate: 2,014 passed, 2 skipped
  • Node, CLI, and web TypeScript checks passed
  • regular and type-aware oxlint, oxfmt, reliability manifest, max-lines ratchet, and git diff --check passed
  • two independent read-only audits: PASS
  • exact scope: six runtime/persistence/RPC files; no visible UI source files changed

Operational follow-up

After CI, the patched macOS app will be installed through a rollback-safe local transaction. mctl will then reconcile only receipt-owned completed automation tabs, followed by an idempotent second cleanup and scheduled no-growth canary cycles.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Terminal closure now supports separate terminal and renderer-tab modes. Runtime results include close-mode metadata, tab identity is validated, and tab requests notify the renderer without killing the PTY. Workspace persistence adds durable tab removal and replay fencing. A new terminal.closeTab RPC is available, including for mobile clients, with dedicated error handling. The CLI parses and documents --tab, routes to the tab RPC, and formats tab-close responses distinctly. Tests cover persistence, runtime, RPC, CLI, and updated result shapes.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description is missing required template sections like Screenshots, Testing checkboxes, AI Review Report, Security Audit, and Notes. Add the missing template sections and include testing checkboxes, screenshots or no visual change, AI review, security audit, and notes.
✅ Passed checks (3 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly summarizes the main change: durable terminal tab closure in runtime.

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: Pro

Run ID: ab4663ce-dbb7-4c48-8a25-63d89e0b494d

📥 Commits

Reviewing files that changed from the base of the PR and between 31cf94f and b498292.

📒 Files selected for processing (2)
  • src/main/runtime/orca-runtime-terminal-close.test.ts
  • src/main/runtime/orca-runtime.ts

Comment thread src/main/runtime/orca-runtime.ts
@chsong1 chsong1 changed the title fix(cli): close terminal tabs explicitly fix(runtime): make terminal tab closure durable Jul 15, 2026

@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: 2

🧹 Nitpick comments (1)
src/main/runtime/orca-runtime.ts (1)

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

Wire the dormant-close state into this helper closeHeadlessMobileTerminalTab only ever gets the default alreadyKilledPtyIds/durableRemovalCompleted values from its current call sites, so the duplicate-kill and durable-removal branches never run. Either pass that state through from the dormant close path or drop the extra parameters.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f7a3b0a2-b1ba-4ac7-8ae5-ef45691fe156

📥 Commits

Reviewing files that changed from the base of the PR and between b498292 and 9c79e69.

📒 Files selected for processing (6)
  • src/main/persistence.test.ts
  • src/main/persistence.ts
  • src/main/runtime/orca-runtime-terminal-close.test.ts
  • src/main/runtime/orca-runtime.ts
  • src/main/runtime/rpc/errors.test.ts
  • src/main/runtime/rpc/errors.ts

Comment thread src/main/persistence.ts
durableRemoval: true
}

const TERMINAL_TAB_REMOVAL_REPLAY_FENCE_MAX = 32

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 | 🏗️ Heavy lift

Do not evict live replay fences after 32 closes.

The test in src/main/persistence.test.ts at Line 9667 confirms that the 33rd removal lets a stale snapshot resurrect bounded-tab-0. This breaks deterministic cleanup and can persist a tab that was already closed. Retain fences until renderer/session-generation acknowledgement rather than evicting solely by count.

Also applies to: 6314-6320

Comment thread src/main/persistence.ts
Comment on lines +3014 to +3053
const activeGroup = groups.find((group) => group.id === activeGroupId) ?? groups[0] ?? null
const activeUnifiedId = activeGroup?.activeTabId ?? activeGroup?.tabOrder[0]
const activeUnifiedTab = next.unifiedTabs?.[identity.worktreeId]?.find(
(tab) => tab.id === activeUnifiedId
)
const browserSurvivors = next.browserTabsByWorktree?.[identity.worktreeId] ?? []
const fileSurvivors = next.openFilesByWorktree?.[identity.worktreeId] ?? []
const hasSurfaceSurvivor =
nextTabs.length > 0 ||
(next.unifiedTabs?.[identity.worktreeId]?.length ?? 0) > 0 ||
browserSurvivors.length > 0 ||
fileSurvivors.length > 0
const restoredBrowserId = next.activeBrowserTabIdByWorktree?.[identity.worktreeId] ?? null
const restoredFileId = next.activeFileIdByWorktree?.[identity.worktreeId] ?? null
const restoredTerminalId = next.activeTabIdByWorktree?.[identity.worktreeId] ?? null
const browserStillOpen = browserSurvivors.some((browser) => browser.id === restoredBrowserId)
const fileStillOpen = fileSurvivors.some((file) => file.filePath === restoredFileId)
const terminalStillExists = nextTabs.some((tab) => tab.id === restoredTerminalId)
const hasGroupOwnedSurface = groups.length > 0 || groupLayout !== undefined
let projectedBrowserId: string | null
let projectedFileId: string | null
let projectedType: 'terminal' | 'browser' | 'simulator' | 'editor'
if (activeUnifiedTab) {
projectedFileId =
terminalTabRemovalVisibleType(activeUnifiedTab.contentType) === 'editor'
? activeUnifiedTab.entityId
: fileStillOpen
? restoredFileId
: null
projectedBrowserId =
activeUnifiedTab.contentType === 'browser'
? activeUnifiedTab.entityId
: browserStillOpen
? restoredBrowserId
: (browserSurvivors[0]?.id ?? null)
projectedType = terminalTabRemovalVisibleType(activeUnifiedTab.contentType)
} else if (hasGroupOwnedSurface) {
projectedFileId = fileStillOpen ? restoredFileId : null
projectedBrowserId = browserStillOpen ? restoredBrowserId : (browserSurvivors[0]?.id ?? null)
projectedType = 'terminal'

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

Prioritize real browser/file survivors over empty group chrome.

A retained empty tab group makes hasGroupOwnedSurface true, so Line 3050 selects terminal before checking surviving legacy browser or file surfaces. Closing the last terminal can therefore leave the workspace in terminal mode with no terminal.

Proposed branch ordering
-  } else if (hasGroupOwnedSurface) {
-    projectedFileId = fileStillOpen ? restoredFileId : null
-    projectedBrowserId = browserStillOpen ? restoredBrowserId : (browserSurvivors[0]?.id ?? null)
-    projectedType = 'terminal'
-  } else if (browserStillOpen) {
+  } else if (browserStillOpen) {
     projectedFileId = fileStillOpen ? restoredFileId : null
     projectedBrowserId = restoredBrowserId
     projectedType = 'browser'
   } else if (fileStillOpen) {
     projectedFileId = restoredFileId
     projectedBrowserId = browserSurvivors[0]?.id ?? null
     projectedType = 'editor'
+  } else if (hasGroupOwnedSurface) {
+    projectedFileId = null
+    projectedBrowserId = browserSurvivors[0]?.id ?? null
+    projectedType = 'terminal'
📝 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
const activeGroup = groups.find((group) => group.id === activeGroupId) ?? groups[0] ?? null
const activeUnifiedId = activeGroup?.activeTabId ?? activeGroup?.tabOrder[0]
const activeUnifiedTab = next.unifiedTabs?.[identity.worktreeId]?.find(
(tab) => tab.id === activeUnifiedId
)
const browserSurvivors = next.browserTabsByWorktree?.[identity.worktreeId] ?? []
const fileSurvivors = next.openFilesByWorktree?.[identity.worktreeId] ?? []
const hasSurfaceSurvivor =
nextTabs.length > 0 ||
(next.unifiedTabs?.[identity.worktreeId]?.length ?? 0) > 0 ||
browserSurvivors.length > 0 ||
fileSurvivors.length > 0
const restoredBrowserId = next.activeBrowserTabIdByWorktree?.[identity.worktreeId] ?? null
const restoredFileId = next.activeFileIdByWorktree?.[identity.worktreeId] ?? null
const restoredTerminalId = next.activeTabIdByWorktree?.[identity.worktreeId] ?? null
const browserStillOpen = browserSurvivors.some((browser) => browser.id === restoredBrowserId)
const fileStillOpen = fileSurvivors.some((file) => file.filePath === restoredFileId)
const terminalStillExists = nextTabs.some((tab) => tab.id === restoredTerminalId)
const hasGroupOwnedSurface = groups.length > 0 || groupLayout !== undefined
let projectedBrowserId: string | null
let projectedFileId: string | null
let projectedType: 'terminal' | 'browser' | 'simulator' | 'editor'
if (activeUnifiedTab) {
projectedFileId =
terminalTabRemovalVisibleType(activeUnifiedTab.contentType) === 'editor'
? activeUnifiedTab.entityId
: fileStillOpen
? restoredFileId
: null
projectedBrowserId =
activeUnifiedTab.contentType === 'browser'
? activeUnifiedTab.entityId
: browserStillOpen
? restoredBrowserId
: (browserSurvivors[0]?.id ?? null)
projectedType = terminalTabRemovalVisibleType(activeUnifiedTab.contentType)
} else if (hasGroupOwnedSurface) {
projectedFileId = fileStillOpen ? restoredFileId : null
projectedBrowserId = browserStillOpen ? restoredBrowserId : (browserSurvivors[0]?.id ?? null)
projectedType = 'terminal'
const activeGroup = groups.find((group) => group.id === activeGroupId) ?? groups[0] ?? null
const activeUnifiedId = activeGroup?.activeTabId ?? activeGroup?.tabOrder[0]
const activeUnifiedTab = next.unifiedTabs?.[identity.worktreeId]?.find(
(tab) => tab.id === activeUnifiedId
)
const browserSurvivors = next.browserTabsByWorktree?.[identity.worktreeId] ?? []
const fileSurvivors = next.openFilesByWorktree?.[identity.worktreeId] ?? []
const hasSurfaceSurvivor =
nextTabs.length > 0 ||
(next.unifiedTabs?.[identity.worktreeId]?.length ?? 0) > 0 ||
browserSurvivors.length > 0 ||
fileSurvivors.length > 0
const restoredBrowserId = next.activeBrowserTabIdByWorktree?.[identity.worktreeId] ?? null
const restoredFileId = next.activeFileIdByWorktree?.[identity.worktreeId] ?? null
const restoredTerminalId = next.activeTabIdByWorktree?.[identity.worktreeId] ?? null
const browserStillOpen = browserSurvivors.some((browser) => browser.id === restoredBrowserId)
const fileStillOpen = fileSurvivors.some((file) => file.filePath === restoredFileId)
const terminalStillExists = nextTabs.some((tab) => tab.id === restoredTerminalId)
const hasGroupOwnedSurface = groups.length > 0 || groupLayout !== undefined
let projectedBrowserId: string | null
let projectedFileId: string | null
let projectedType: 'terminal' | 'browser' | 'simulator' | 'editor'
if (activeUnifiedTab) {
projectedFileId =
terminalTabRemovalVisibleType(activeUnifiedTab.contentType) === 'editor'
? activeUnifiedTab.entityId
: fileStillOpen
? restoredFileId
: null
projectedBrowserId =
activeUnifiedTab.contentType === 'browser'
? activeUnifiedTab.entityId
: browserStillOpen
? restoredBrowserId
: (browserSurvivors[0]?.id ?? null)
projectedType = terminalTabRemovalVisibleType(activeUnifiedTab.contentType)
} else if (browserStillOpen) {
projectedFileId = fileStillOpen ? restoredFileId : null
projectedBrowserId = restoredBrowserId
projectedType = 'browser'
} else if (fileStillOpen) {
projectedFileId = restoredFileId
projectedBrowserId = browserSurvivors[0]?.id ?? null
projectedType = 'editor'
} else if (hasGroupOwnedSurface) {
projectedFileId = null
projectedBrowserId = browserSurvivors[0]?.id ?? null
projectedType = 'terminal'

@Jinwoo-H

Copy link
Copy Markdown
Contributor

Thanks for identifying and driving this cleanup gap. Independent validation found that this implementation could acknowledge before renderer persistence, did not cover headless or dormant-tab closure, and could resurrect closes after the bounded replay fence rolled over. The CLI contract and product direction are preserved in #8958, which uses canonical terminal retirement plus an awaited disk flush and a headless persisted-session projection. Closing this version in favor of #8958 so there is one canonical implementation to review.

@Jinwoo-H Jinwoo-H closed this Jul 16, 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.

2 participants