fix: guard synthetic handles, keep session listing off the repo inventory

This commit is contained in:
Merge Sim
2026-09-03 23:11:56 -07:00
parent 89991fffbb
commit 1b45ebf743
9 changed files with 258 additions and 14 deletions
@@ -10,7 +10,7 @@ import {
isInboundMessageTabMount,
type TerminalTabMountIntent
} from '../../shared/terminal-tab-mount-intent'
import { makePaneKey } from '../../shared/stable-pane-id'
import { isTerminalLeafId, makePaneKey } from '../../shared/stable-pane-id'
import { findSleepingAgentSessionRecord } from './sleeping-pane-record-lookup'
import { resolveSleepingPaneWakeTarget } from './orchestration/sleeping-pane-wake-target'
import { SleepingPaneWakeScheduler } from './orchestration/sleeping-pane-wake-scheduler'
@@ -85,11 +85,15 @@ export class OrcaRuntimeWithWaitForLeafPtyId extends OrcaRuntimeWithRestoreLiveP
if (!record?.worktreeId) {
return false
}
const tabId = record.tabId.startsWith('pty:') ? undefined : record.tabId
return this.requestRendererTerminalTabMountForPane({
worktreeId: record.worktreeId,
tabId: record.tabId.startsWith('pty:') ? undefined : record.tabId,
tabId,
ptyId: record.ptyId ?? undefined,
paneKey: record.leafId ? makePaneKey(record.tabId, record.leafId) : undefined,
// Why: synthetic pty-form handles carry no real pane identity, and
// makePaneKey rejects their ids outright.
paneKey:
tabId && isTerminalLeafId(record.leafId) ? makePaneKey(tabId, record.leafId) : undefined,
intent
})
}
@@ -133,7 +133,13 @@ describe('terminal list with slept panes', () => {
const result = await list.list('wt-1', 10, { includeVisualLayouts: false })
const rows = result.terminals.filter((terminal) => terminal.tabId === 'tab-slept')
expect(rows).toHaveLength(1)
expect(rows[0]).toMatchObject({ connected: false, resumable: true })
// The leaf row, not the synthesized one: it carries the pane's real title,
// preview and last-output time that persistence alone cannot supply.
expect(rows[0]).toMatchObject({
handle: 'term_tab-slept',
connected: false,
resumable: true
})
})
it('still drops a PTY-less leaf that has no resume record', async () => {
@@ -110,19 +110,20 @@ export class RuntimeWorkspaceSessionController {
}
/** Every persisted host partition. Callers that key off pane identity — which
* carries no host — must search all of them, not just 'local'. */
* carries no host — must search all of them, not just 'local'.
*
* Why persisted ids only, not the repo list: reading repos would drag the
* worktree inventory into hot poll paths that must never resolve it (#9343),
* and a host with no persisted partition has nothing to find anyway. */
listSessions(): WorkspaceSessionState[] {
const store = this.deps.getStore()
if (!store) {
return []
}
const hostIds = new Set<ExecutionHostId>([LOCAL_EXECUTION_HOST_ID])
for (const repo of store.getRepos?.() ?? []) {
hostIds.add(getRepoExecutionHostId(repo))
}
for (const hostId of store.getWorkspaceSessionHostIds?.() ?? []) {
hostIds.add(hostId)
}
const hostIds = new Set<ExecutionHostId>([
LOCAL_EXECUTION_HOST_ID,
...(store.getWorkspaceSessionHostIds?.() ?? [])
])
return [...hostIds].flatMap((hostId) => {
const session = store.getWorkspaceSession?.(hostId)
return session ? [session] : []
@@ -9,6 +9,7 @@ import {
isLiveResumeAnchorForCompletedAgent
} from './live-resume-anchor-record'
import type { AgentHibernationPlannerSnapshot } from './agent-hibernation-planner-snapshot'
import { isSupervisingUnsettledDispatch } from './agent-hibernation-supervised-runs'
export type EligiblePane = {
paneKey: string
@@ -66,6 +67,7 @@ export function getEligiblePane(args: {
ptyBindingFirstSeenAtByPaneKey: Record<string, number | undefined>
boundaryResolvedAtByPaneKey: Record<string, number | undefined>
mobileLockedPtyIds: Set<string>
supervisorLeafIdsWithUnsettledDispatch: ReadonlySet<string>
now: number
idleMs: number
}): EligiblePane | null {
@@ -79,7 +81,8 @@ export function getEligiblePane(args: {
foregroundTerminalLastSeenAtByTabId,
ptyBindingFirstSeenAtByPaneKey,
boundaryResolvedAtByPaneKey,
mobileLockedPtyIds
mobileLockedPtyIds,
supervisorLeafIdsWithUnsettledDispatch
} = args
const sleepingRecord = sleepingAgentSessionsByPaneKey[entry.paneKey]
// Why: a completed turn leaves the TUI alive and resumable, so every resumable
@@ -95,6 +98,9 @@ export function getEligiblePane(args: {
entry.interrupted === true ||
Boolean(entry.subagents?.length) ||
hasUnsettledOrUnknownDispatch(entry) ||
// Why: this pane supervises a worker that has not reported back. Sleeping it
// is what the wake path exists to recover from; don't create the case.
isSupervisingUnsettledDispatch(entry.paneKey, supervisorLeafIdsWithUnsettledDispatch) ||
(sleepingRecord && !hasOnlyLiveResumeAnchor) ||
// Why: a fenced worker must never be auto-relaunched; killing it would also
// erase the fence, since the capture does not copy it.
@@ -0,0 +1,120 @@
/**
* The cheap half of the obligation guard: never sleep a pane whose dispatched
* worker has not reported back. Belt-and-braces — correctness rests on the
* wake-on-message path, so a gap here is no longer a silent deadlock.
*/
import { describe, expect, it } from 'vitest'
import type { AgentStatusEntry } from '../../../shared/agent-status-types'
import type { TerminalLayoutSnapshot, TerminalTab } from '../../../shared/terminal-tab-types'
import {
DEFAULT_AGENT_HIBERNATION_IDLE_MS,
planAgentHibernationCandidates,
type AgentHibernationPlannerSnapshot
} from './agent-hibernation-planner'
const NOW = 2_000_000
const OLD = NOW - DEFAULT_AGENT_HIBERNATION_IDLE_MS - 1
const LEAF = '11111111-1111-4111-8111-111111111111'
const OTHER_LEAF = '22222222-2222-4222-8222-222222222222'
function tab(id = 'tab-1', worktreeId = 'wt-bg'): TerminalTab {
return {
id,
ptyId: null,
worktreeId,
title: 'Agent',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1
}
}
function layout(leafId = LEAF, ptyId = 'pty-1'): TerminalLayoutSnapshot {
return {
root: { type: 'leaf', leafId },
activeLeafId: leafId,
expandedLeafId: null,
ptyIdsByLeafId: { [leafId]: ptyId }
}
}
function entry(overrides: Partial<AgentStatusEntry> = {}): AgentStatusEntry {
const paneKey = overrides.paneKey ?? `tab-1:${LEAF}`
return {
state: 'done',
prompt: 'make it so',
updatedAt: OLD,
stateStartedAt: OLD,
paneKey,
tabId: 'tab-1',
worktreeId: 'wt-bg',
agentType: 'claude',
providerSession: { key: 'session_id', id: 'session-1' },
stateHistory: [],
...overrides
}
}
function snapshotWith(
agentStatusByPaneKey: Record<string, AgentStatusEntry>
): AgentHibernationPlannerSnapshot {
return {
settings: {
experimentalAgentHibernation: true,
agentHibernationIdleMs: DEFAULT_AGENT_HIBERNATION_IDLE_MS
},
activeWorktreeId: 'wt-active',
foregroundTerminalTabIds: [],
tabsByWorktree: { 'wt-bg': [tab()] },
terminalLayoutsByTabId: { 'tab-1': layout() },
ptyIdsByTabId: { 'tab-1': ['pty-1'] },
mobileLockedPtyIds: [],
agentStatusByPaneKey,
sleepingAgentSessionsByPaneKey: {},
lastTerminalInputAtByPaneKey: {},
foregroundTerminalLastSeenAtByTabId: {},
now: NOW
}
}
function workerOf(
coordinator: AgentStatusEntry,
dispatchStatus: 'dispatched' | 'completed'
): AgentStatusEntry {
return {
...entry({ paneKey: `tab-2:${OTHER_LEAF}`, tabId: 'tab-2' }),
orchestration: {
taskId: 'task-1',
dispatchId: 'ctx-1',
dispatchStatus,
parentPaneKey: coordinator.paneKey
}
}
}
function plannedPaneKeys(input: AgentHibernationPlannerSnapshot): string[] {
return planAgentHibernationCandidates(input).map((candidate) => candidate.paneKey)
}
describe('agent sleep planner and supervised runs', () => {
it('refuses to sleep a coordinator whose dispatched worker has not reported back', () => {
const coordinator = entry()
const worker = workerOf(coordinator, 'dispatched')
const input = snapshotWith({
[coordinator.paneKey]: coordinator,
[worker.paneKey]: worker
})
expect(plannedPaneKeys(input)).toEqual([])
})
it('sleeps the same coordinator once its worker settles', () => {
const coordinator = entry()
const worker = workerOf(coordinator, 'completed')
const input = snapshotWith({
[coordinator.paneKey]: coordinator,
[worker.paneKey]: worker
})
expect(plannedPaneKeys(input)).toEqual([coordinator.paneKey])
})
})
@@ -1,5 +1,6 @@
import type { AgentStatusEntry } from '../../../shared/agent-status-types'
import type { TerminalTab } from '../../../shared/terminal-tab-types'
import { getSupervisorLeafIdsWithUnsettledDispatch } from './agent-hibernation-supervised-runs'
import {
getEligiblePane,
getEntryTabId,
@@ -110,6 +111,9 @@ export function planAgentHibernationCandidates(
snapshot.runtimeLivenessRequiredWorktreeIds ?? []
)
const agentEntriesByTabId = getAgentEntriesByTabId(snapshot.agentStatusByPaneKey)
const supervisorLeafIdsWithUnsettledDispatch = getSupervisorLeafIdsWithUnsettledDispatch(
snapshot.agentStatusByPaneKey
)
const candidates: AgentHibernationCandidate[] = []
for (const [worktreeId, tabs] of Object.entries(snapshot.tabsByWorktree)) {
// Why: the tab on screen is `foregroundTerminalTabIds` below, and a tab just left is held by
@@ -151,6 +155,7 @@ export function planAgentHibernationCandidates(
ptyBindingFirstSeenAtByPaneKey: snapshot.ptyBindingFirstSeenAtByPaneKey ?? {},
boundaryResolvedAtByPaneKey: snapshot.boundaryResolvedAtByPaneKey ?? {},
mobileLockedPtyIds,
supervisorLeafIdsWithUnsettledDispatch,
now: snapshot.now,
idleMs
})
@@ -0,0 +1,61 @@
import { describe, expect, it } from 'vitest'
import type { AgentStatusEntry } from '../../../shared/agent-status-types'
import {
getSupervisorLeafIdsWithUnsettledDispatch,
isSupervisingUnsettledDispatch
} from './agent-hibernation-supervised-runs'
const COORDINATOR_LEAF = '33333333-3333-4333-8333-333333333333'
const COORDINATOR_PANE = `tab-coord:${COORDINATOR_LEAF}`
function worker(
dispatchStatus: AgentStatusEntry['orchestration'] extends infer T
? T extends { dispatchStatus?: infer S }
? S
: never
: never,
parentPaneKey: string | null = COORDINATOR_PANE
): AgentStatusEntry {
return {
paneKey: 'tab-worker:leaf-worker',
state: 'working',
orchestration: {
taskId: 'task-1',
dispatchId: 'ctx-1',
dispatchStatus,
...(parentPaneKey ? { parentPaneKey } : {})
}
} as unknown as AgentStatusEntry
}
describe('supervisors with unsettled dispatches', () => {
it.each(['pending', 'dispatched'] as const)('collects a supervisor of a %s worker', (status) => {
const supervisors = getSupervisorLeafIdsWithUnsettledDispatch({ w: worker(status) })
expect(isSupervisingUnsettledDispatch(COORDINATOR_PANE, supervisors)).toBe(true)
})
it.each(['completed', 'failed', 'circuit_broken'] as const)('ignores a %s worker', (status) => {
const supervisors = getSupervisorLeafIdsWithUnsettledDispatch({ w: worker(status) })
expect(isSupervisingUnsettledDispatch(COORDINATOR_PANE, supervisors)).toBe(false)
})
it('matches a supervisor whose tab was reminted', () => {
const supervisors = getSupervisorLeafIdsWithUnsettledDispatch({ w: worker('dispatched') })
expect(isSupervisingUnsettledDispatch(`tab-new:${COORDINATOR_LEAF}`, supervisors)).toBe(true)
})
it('ignores a worker whose supervisor pane is unknown', () => {
expect(getSupervisorLeafIdsWithUnsettledDispatch({ w: worker('dispatched', null) }).size).toBe(
0
)
})
it('ignores entries with no orchestration context', () => {
expect(
getSupervisorLeafIdsWithUnsettledDispatch({
a: { paneKey: 'tab:leaf', state: 'done' } as unknown as AgentStatusEntry,
b: undefined
}).size
).toBe(0)
})
})
@@ -0,0 +1,41 @@
import type { AgentStatusEntry } from '../../../shared/agent-status-types'
import { parsePaneKey } from '../../../shared/stable-pane-id'
const UNSETTLED_DISPATCH_STATUSES = new Set(['pending', 'dispatched'])
/**
* Panes supervising a worker whose Dispatch has not settled.
*
* Belt-and-braces only: the wake path is what makes a slept supervisor safe, so
* an obligation this misses is no longer a silent deadlock. It avoids the common
* case ever needing a wake, and it reads the supervisor identity each worker
* already publishes rather than enumerating obligation kinds.
*
* Keyed by stable leaf id: a pane key's tab half is reminted on break-out.
*/
export function getSupervisorLeafIdsWithUnsettledDispatch(
agentStatusByPaneKey: Record<string, AgentStatusEntry | undefined>
): Set<string> {
const leafIds = new Set<string>()
for (const entry of Object.values(agentStatusByPaneKey)) {
const orchestration = entry?.orchestration
if (!orchestration || !UNSETTLED_DISPATCH_STATUSES.has(orchestration.dispatchStatus ?? '')) {
continue
}
const supervisorLeafId = orchestration.parentPaneKey
? parsePaneKey(orchestration.parentPaneKey)?.leafId
: undefined
if (supervisorLeafId) {
leafIds.add(supervisorLeafId)
}
}
return leafIds
}
export function isSupervisingUnsettledDispatch(
paneKey: string,
supervisorLeafIds: ReadonlySet<string>
): boolean {
const leafId = parsePaneKey(paneKey)?.leafId
return leafId !== undefined && supervisorLeafIds.has(leafId)
}
@@ -402,7 +402,7 @@ const DIRECT_SINGLE_SOURCE_SURFACES: readonly {
marker: 'resolveLeafCloseCopyKind'
},
{
path: 'src/main/runtime/orchestration/mailbox-pointer-delivery.ts',
path: 'src/main/runtime/orchestration/mailbox-pointer-stage.ts',
classification: 'action-consumer',
marker: 'isCursorAgentTitle'
},