diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 62c9b3c29ce..5d8564531e9 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -3883,6 +3883,7 @@ "file": "src/main/runtime/orca-runtime.test.ts", "assertions": [ "terminal.stop awaits stopAndWait and fresh post-stop liveness before acknowledging provider shutdown", + "remote terminal.close rejects until provider stopAndWait verifies shutdown", "provider shutdown rejection fails terminal.stop closed", "project removal fences paired terminal admission through provider stop and repo mutation", "project removal drains create-first headless, renderer-backed mobile, renderer split, and selector-less renderer admissions before its liveness snapshot", diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index ff84c713f63..3e38530dfda 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -17690,12 +17690,13 @@ describe('OrcaRuntimeService', () => { it('operates PTY-backed mobile session terminals without a renderer graph', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'laptop-created-pty' }) - const kill = vi.fn(() => true) + const stopAndWait = vi.fn(async () => true) const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ spawn, write: () => true, - kill, + kill: () => false, + stopAndWait, getForegroundProcess: async () => null }) @@ -17719,7 +17720,29 @@ describe('OrcaRuntimeService', () => { tabId: 'laptop-tab', ptyKilled: true }) - expect(kill).toHaveBeenCalledWith('laptop-created-pty') + expect(stopAndWait).toHaveBeenCalledWith('laptop-created-pty') + }) + + it('rejects remote terminal close until provider shutdown is verified', async () => { + const runtime = new OrcaRuntimeService(store) + runtime.setPtyController({ + spawn: vi.fn().mockResolvedValue({ id: 'laptop-created-pty' }), + write: () => true, + kill: () => true, + stopAndWait: async () => false, + getForegroundProcess: async () => null + }) + const terminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { + tabId: 'laptop-tab', + leafId: HEADLESS_LEAF_ID + }) + + await expect(runtime.closeTerminal(terminal.handle)).rejects.toThrow( + 'terminal_verified_stop_failed' + ) + await expect(runtime.readTerminal(terminal.handle)).resolves.toMatchObject({ + status: 'running' + }) }) it('lists PTY-backed mobile session terminals without a renderer graph', async () => { diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index d9ed408c7ea..0c0493bc877 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -18913,17 +18913,29 @@ export class OrcaRuntimeService { async closeTerminal(handle: string): Promise { const pty = this.getLivePtyForHandle(handle) - this.claudeAgentTeams.removeTeamForLeaderHandle(handle) if (pty) { - const ptyKilled = this.ptyController?.kill(pty.pty.ptyId) ?? false - return { handle, tabId: pty.pty.tabId ?? pty.record.tabId, ptyKilled } + if (!this.ptyController?.stopAndWait) { + throw new Error('terminal_verified_stop_unavailable') + } + if (!(await this.ptyController.stopAndWait(pty.pty.ptyId))) { + throw new Error('terminal_verified_stop_failed') + } + this.claudeAgentTeams.removeTeamForLeaderHandle(handle) + return { handle, tabId: pty.pty.tabId ?? pty.record.tabId, ptyKilled: true } } this.assertGraphReady() const { leaf } = this.getLiveLeafForHandle(handle) let ptyKilled = false if (leaf.ptyId) { - ptyKilled = this.ptyController?.kill(leaf.ptyId) ?? false + if (!this.ptyController?.stopAndWait) { + throw new Error('terminal_verified_stop_unavailable') + } + ptyKilled = await this.ptyController.stopAndWait(leaf.ptyId) + if (!ptyKilled) { + throw new Error('terminal_verified_stop_failed') + } } + this.claudeAgentTeams.removeTeamForLeaderHandle(handle) // Why: killing the PTY in a multi-pane tab is sufficient — the renderer's // PTY exit handler already calls PaneManager.closePane() for split layouts. // Sending an additional IPC close would race with the exit handler and