Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions src/main/runtime/orca-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31367,6 +31367,88 @@ describe('OrcaRuntimeService', () => {
expect(getSession().terminalLayoutsByTabId['host-tab']).toBeUndefined()
})

it('closes headless terminal parents whose persisted record is missing instead of resurrecting them', async () => {
const layout = makeHeadlessTerminalLayout({
[HEADLESS_LEAF_ID]: 'pty-a',
[HEADLESS_SECOND_LEAF_ID]: 'pty-b'
})
const { runtimeStore, getSession, setSession } = makeRuntimeStoreWithWorkspaceSession(
makeWorkspaceSessionWithHeadlessTerminal({
tabsByWorktree: {
[TEST_WORKTREE_ID]: [
{
id: 'host-tab',
ptyId: 'pty-a',
worktreeId: TEST_WORKTREE_ID,
title: 'Persisted Terminal',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1
}
]
},
terminalLayoutsByTabId: { 'host-tab': layout }
})
)
const kill = vi.fn(() => true)
const runtime = new OrcaRuntimeService(runtimeStore as never)
runtime.setPtyController({
write: () => true,
kill,
getForegroundProcess: async () => null,
listProcesses: async () => []
})
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'host-tab',
worktreeId: TEST_WORKTREE_ID,
title: 'Persisted Terminal',
activeLeafId: HEADLESS_LEAF_ID,
layout: null
}
],
leaves: [
{
tabId: 'host-tab',
worktreeId: TEST_WORKTREE_ID,
leafId: HEADLESS_LEAF_ID,
paneRuntimeId: 1,
ptyId: 'pty-a',
paneTitle: 'A'
},
{
tabId: 'host-tab',
worktreeId: TEST_WORKTREE_ID,
leafId: HEADLESS_SECOND_LEAF_ID,
paneRuntimeId: 2,
ptyId: 'pty-b',
paneTitle: 'B'
}
]
})

// Why: warm the live snapshot, then drop the persisted row out-of-band to
// reproduce the persisted/live-state drift (#10747, #12920) — a
// never-persisted background tab or a close that de-persisted then failed.
await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)
setSession({
...getSession(),
tabsByWorktree: { [TEST_WORKTREE_ID]: [] },
terminalLayoutsByTabId: {}
})

await runtime.closeMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab')

expect(kill).toHaveBeenCalledWith('pty-a')
expect(kill).toHaveBeenCalledWith('pty-b')
const relisted = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)
expect(
relisted.tabs.filter((tab) => tab.type === 'terminal' && tab.parentTabId === 'host-tab')
).toEqual([])
})

it('closes persisted headless terminal parents before any prior list call', async () => {
const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession(
makeWorkspaceSessionWithHeadlessTerminal()
Expand Down
27 changes: 10 additions & 17 deletions src/main/runtime/orca-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7285,11 +7285,7 @@ export class OrcaRuntimeService {
}
}

private removePersistedHeadlessTerminalTab(
worktreeId: string,
parentTabId: string,
options: { allowMissing?: boolean } = {}
): string[] {
private removePersistedHeadlessTerminalTab(worktreeId: string, parentTabId: string): string[] {
const session = this.getWorkspaceSessionForWorktree(worktreeId)
if (!session || !this.store?.setWorkspaceSession) {
throw new Error('workspace_session_unavailable')
Expand All @@ -7299,10 +7295,12 @@ export class OrcaRuntimeService {
throw new Error('terminal_tab_pinned')
}
if (!result.closed) {
if (options.allowMissing) {
return []
}
throw new Error('tab_not_found')
// Why: the caller resolved the tab from the live snapshot, so a missing
// persisted row is always drift (never-persisted background tab, or an
// earlier close that de-persisted then failed). Aborting here would skip
// the PTY kill and snapshot prune below and the republished snapshot
// would resurrect the tab on every client forever (#10747, #12920).
return []
}
this.setWorkspaceSessionForWorktree(
worktreeId,
Expand Down Expand Up @@ -7927,10 +7925,7 @@ export class OrcaRuntimeService {
this.isRuntimeOwnedHeadlessMobileTab(worktreeId, remainingTab)
) {
// Why: after relay recovery the renderer can acknowledge a tab it no longer mirrors; the HUB must still retire its SSH-owned surface.
this.closeHeadlessMobileTerminalTab(worktreeId, remainingSnapshot, remainingTab, {
// Why: the renderer may already have durably removed the tab before acknowledging.
allowMissingPersistedTab: true
})
this.closeHeadlessMobileTerminalTab(worktreeId, remainingSnapshot, remainingTab)
this.notifyRendererOfHeadlessTerminalClose(tab.parentTabId)
this.store?.flushOrThrow?.()
}
Expand Down Expand Up @@ -8104,13 +8099,11 @@ export class OrcaRuntimeService {
worktreeId: string,
snapshot: RuntimeMobileSessionTabsSnapshot,
tab: RuntimeMobileSessionTerminalTab,
options: { allowMissingPersistedTab?: boolean; killPtys?: boolean } = {}
options: { killPtys?: boolean } = {}
): void {
const closedParentTabId = tab.parentTabId
this.clearRuntimeSessionOwnershipForMobileTab(worktreeId, snapshot, closedParentTabId)
const projectedPtyIds = this.removePersistedHeadlessTerminalTab(worktreeId, closedParentTabId, {
allowMissing: options.allowMissingPersistedTab
})
const projectedPtyIds = this.removePersistedHeadlessTerminalTab(worktreeId, closedParentTabId)
// Why: local provider ids can be reused after restart, so a dormant
// persisted id is not kill authority. SSH relay ids remain durable exact
// identities even before pane metadata reconnects.
Expand Down
Loading