fix(agents): stop forking a duplicate Pi tab for a live background session (#9729)
This commit is contained in:
parent
92696558c3
commit
877bbdebf8
|
|
@ -0,0 +1,82 @@
|
|||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import type { SleepingAgentSessionRecord } from '../../../shared/agent-session-resume'
|
||||
import { useAppStore } from '@/store'
|
||||
import { resumeSleepingAgentSessionsForWorktree } from './resume-sleeping-agent-session'
|
||||
|
||||
const initialAppStoreState = useAppStore.getState()
|
||||
const LEAF_ID = '11111111-1111-1111-8111-111111111111'
|
||||
const PANE_KEY = `pi-tab:${LEAF_ID}`
|
||||
|
||||
afterEach(() => {
|
||||
useAppStore.setState(initialAppStoreState, true)
|
||||
})
|
||||
|
||||
describe('Pi live session does not spawn a duplicate resume tab', () => {
|
||||
it('keeps a done-but-alive Pi pane instead of forking a new tab', () => {
|
||||
const providerSession = {
|
||||
key: 'session_id' as const,
|
||||
id: 'pi-1',
|
||||
transcriptPath: '/tmp/pi-session-1.jsonl'
|
||||
}
|
||||
const record: SleepingAgentSessionRecord = {
|
||||
paneKey: PANE_KEY,
|
||||
tabId: 'pi-tab',
|
||||
worktreeId: 'wt-1',
|
||||
agent: 'pi',
|
||||
providerSession,
|
||||
prompt: '',
|
||||
state: 'working',
|
||||
capturedAt: 1,
|
||||
updatedAt: 1,
|
||||
origin: 'live'
|
||||
}
|
||||
useAppStore.setState({
|
||||
activeWorktreeId: 'wt-1',
|
||||
activeTabType: 'editor',
|
||||
activeTabId: null,
|
||||
tabsByWorktree: {
|
||||
'wt-1': [
|
||||
{
|
||||
id: 'pi-tab',
|
||||
ptyId: 'pty-1',
|
||||
worktreeId: 'wt-1',
|
||||
title: 'pi',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
ptyIdsByTabId: { 'pi-tab': ['pty-1'] },
|
||||
terminalLayoutsByTabId: {
|
||||
'pi-tab': {
|
||||
root: { type: 'leaf', leafId: LEAF_ID },
|
||||
activeLeafId: LEAF_ID,
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: { [LEAF_ID]: 'pty-1' }
|
||||
}
|
||||
},
|
||||
agentStatusByPaneKey: {
|
||||
[PANE_KEY]: {
|
||||
state: 'done',
|
||||
prompt: '',
|
||||
updatedAt: 10,
|
||||
stateStartedAt: 10,
|
||||
agentType: 'pi',
|
||||
paneKey: PANE_KEY,
|
||||
worktreeId: 'wt-1',
|
||||
tabId: 'pi-tab',
|
||||
providerSession
|
||||
}
|
||||
},
|
||||
sleepingAgentSessionsByPaneKey: { [PANE_KEY]: record }
|
||||
} as never)
|
||||
|
||||
const launched = resumeSleepingAgentSessionsForWorktree('wt-1')
|
||||
|
||||
expect(launched).toBe(0)
|
||||
expect(useAppStore.getState().tabsByWorktree['wt-1']).toHaveLength(1)
|
||||
expect(useAppStore.getState().sleepingAgentSessionsByPaneKey[PANE_KEY]).toBe(record)
|
||||
})
|
||||
})
|
||||
|
|
@ -78,6 +78,30 @@ function hasRestorableStablePanePty(
|
|||
)
|
||||
}
|
||||
|
||||
// Why: a pane whose PTY is live *right now* already owns its running session
|
||||
// — e.g. a Pi TUI that finished a turn but stays alive in a background tab.
|
||||
// Resume must never fork such a pane into a duplicate tab, even when it isn't
|
||||
// the pane that reconnects on activation. Liveness comes from the runtime
|
||||
// live-PTY map (ptyIdsByTabId), not the layout's ptyIdsByLeafId snapshot, which
|
||||
// persists stale across sleep/restart.
|
||||
function stablePaneHasLivePty(
|
||||
tabId: string,
|
||||
leafId: string,
|
||||
ptyIdsByTabId: Record<string, string[]>,
|
||||
layout: TerminalLayoutSnapshot | undefined
|
||||
): boolean {
|
||||
const livePtyIds = ptyIdsByTabId[tabId] ?? []
|
||||
if (livePtyIds.length === 0) {
|
||||
return false
|
||||
}
|
||||
const leafPtyId = layout?.ptyIdsByLeafId?.[leafId]
|
||||
if (leafPtyId) {
|
||||
return livePtyIds.includes(leafPtyId)
|
||||
}
|
||||
// Single-leaf tabs have no per-leaf binding; the tab's live PTY is this leaf's.
|
||||
return layout?.root?.type === 'leaf' && layout.root.leafId === leafId
|
||||
}
|
||||
|
||||
function paneWillConnectOnActivation(
|
||||
worktreeId: string,
|
||||
tabId: string,
|
||||
|
|
@ -119,6 +143,18 @@ export function recordPaneIsOwnedByPreservedPane(
|
|||
if (isPassiveCompletedHibernationEvidence(record)) {
|
||||
return true
|
||||
}
|
||||
// Why: a pane with a live PTY owns its running session regardless of which
|
||||
// pane reconnects on activation; forking it would duplicate the session.
|
||||
if (
|
||||
stablePaneHasLivePty(
|
||||
tabId,
|
||||
stable.leafId,
|
||||
state.ptyIdsByTabId,
|
||||
state.terminalLayoutsByTabId[tabId]
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
// Why: active sessions rely on pane-level cold restore. A preserved leaf
|
||||
// without a PTY/session id can repaint scrollback but cannot resume.
|
||||
return (
|
||||
|
|
|
|||
Loading…
Reference in New Issue