fix(runtime): preserve pane reservation creator identity

This commit is contained in:
Brennan Benson
2026-08-01 10:14:34 -07:00
parent 5ad243ec9d
commit ced9fd5321
4 changed files with 403 additions and 7 deletions
+310
View File
@@ -631,6 +631,132 @@ describe('registerPtyHandlers', () => {
return spawn
}
function createFullRuntimePaneRaceHarness() {
type ProviderSpawnOptions = {
cwd?: string
env?: Record<string, string>
sessionId?: string
}
const folder = {
id: 'full-runtime-pane-race-folder',
projectGroupId: 'full-runtime-pane-race-group',
name: 'Full runtime pane race',
folderPath: process.cwd(),
connectionId: null,
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 1,
createdAt: 1,
updatedAt: 1
}
const group = {
id: folder.projectGroupId,
name: folder.projectGroupId,
parentPath: folder.folderPath,
connectionId: null,
parentGroupId: null,
createdFrom: 'manual' as const,
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const pending: {
options: ProviderSpawnOptions
resolve: (result: { id: string }) => void
}[] = []
const providerSpawn = vi.fn(
(options: ProviderSpawnOptions) =>
new Promise<{ id: string }>((resolve) => pending.push({ options, resolve }))
)
const shutdown = vi.fn()
installDaemonTestProvider({ spawn: providerSpawn, shutdown })
const store = {
getFolderWorkspace: vi.fn(() => folder),
getFolderWorkspaces: vi.fn(() => [folder]),
getProjectGroups: vi.fn(() => [group]),
getRepos: vi.fn(() => []),
getSettings: vi.fn(() => ({ claudeAgentTeamsMode: 'native-panes-shim' as const })),
persistPtyBinding: vi.fn(),
removePtyBindingIfMatches: vi.fn()
}
const runtime = new OrcaRuntimeService(store as never)
vi.spyOn(runtime, 'createPreAllocatedTerminalHandle').mockReturnValue('term_renderer_creator')
registerPtyHandlers(
mainWindow as never,
runtime,
undefined,
undefined,
undefined,
store as never
)
const folderKey = `folder:${folder.id}`
const tabId = 'tab-full-runtime-pane-race'
const leafId = '16161616-1616-4616-8616-161616161616'
const paneKey = makePaneKey(tabId, leafId)
const launchConfig = (owner: string) => ({
agentCommand: 'claude --teammate-mode auto',
agentArgs: `--${owner}`,
agentEnv: { OWNER: owner }
})
const spawnRenderer = (sessionId?: string) =>
handlers.get('pty:spawn')!(null, {
cols: 80,
rows: 24,
cwd: folder.folderPath,
worktreeId: folderKey,
tabId,
leafId,
...(sessionId ? { sessionId } : {}),
env: { ORCA_PANE_KEY: paneKey },
command: 'claude --teammate-mode auto',
launchAgent: 'claude',
launchConfig: launchConfig('creator')
}) as Promise<{ id: string; spawnDisposition: string }>
const spawnRuntime = (opts: {
sessionId?: string
signal?: AbortSignal
acceptWorkspaceInventory?: () => boolean
preAllocatedHandle?: string
}) =>
runtime.createTerminal(`id:${folderKey}`, {
command: 'claude --teammate-mode auto',
launchAgent: 'claude',
launchConfig: launchConfig('waiter'),
title: 'waiter title',
tabId,
leafId,
persistHostSessionBinding: true,
spawnReservationFreshness:
runtime.getTerminalSpawnReservationFreshness(folderKey, null) ?? undefined,
...opts
})
const teams = (runtime as unknown as { claudeAgentTeams: ClaudeAgentTeamsService })
.claudeAgentTeams
return {
folder,
group,
folderKey,
tabId,
leafId,
paneKey,
pending,
providerSpawn,
shutdown,
store,
runtime,
teams,
launchConfig,
spawnRenderer,
spawnRuntime
}
}
function installObservableDaemonTestProvider() {
const spawn = vi.fn(async (options: { sessionId?: string }) => ({
id: options.sessionId ?? 'daemon-pty'
@@ -8630,6 +8756,190 @@ describe('registerPtyHandlers', () => {
clearProviderPtyState(creatorSessionId)
})
it('adopts a renderer creator without rebinding its full runtime identity', async () => {
const harness = createFullRuntimePaneRaceHarness()
const sessionId = 'caller-owned-full-runtime-session'
const rendererSpawn = harness.spawnRenderer(sessionId)
await vi.waitFor(() => expect(harness.providerSpawn).toHaveBeenCalledOnce())
const creatorOptions = harness.pending[0]!.options
expect(creatorOptions.env?.ORCA_TERMINAL_HANDLE).toBe('term_renderer_creator')
const runtimeSpawn = harness.spawnRuntime({
sessionId,
preAllocatedHandle: 'term_runtime_waiter',
acceptWorkspaceInventory: () => true
})
await vi.waitFor(() => expect(harness.teams.getActiveTeamCount()).toBe(2))
harness.runtime.registerPreAllocatedHandleForPty(sessionId, 'term_renderer_creator')
harness.runtime.registerPty(sessionId, harness.folderKey, null, {
tabId: harness.tabId,
leafId: harness.leafId
})
const creatorLaunchConfig = harness.launchConfig('stored-creator')
const creatorPty = (
harness.runtime as unknown as {
ptysById: Map<
string,
{
runtimeSessionOwned: boolean
title: string | null
titleUpdatedAt: number | null
launchConfig: ReturnType<typeof harness.launchConfig> | null
launchToken: string | null
launchAgent: TuiAgent | null
}
>
}
).ptysById.get(sessionId)!
Object.assign(creatorPty, {
runtimeSessionOwned: false,
title: 'creator title',
titleUpdatedAt: Date.now(),
launchConfig: creatorLaunchConfig,
launchToken: 'creator-launch-token',
launchAgent: null
})
harness.pending[0]!.resolve({ id: sessionId })
await expect(Promise.all([rendererSpawn, runtimeSpawn])).resolves.toEqual([
expect.objectContaining({ id: sessionId, spawnDisposition: 'created' }),
expect.objectContaining({
handle: 'term_renderer_creator',
ptyId: sessionId,
tabId: harness.tabId,
paneKey: harness.paneKey,
worktreeId: harness.folderKey,
title: 'creator title',
surface: 'background'
})
])
expect(harness.providerSpawn).toHaveBeenCalledOnce()
expect(harness.teams.getActiveTeamCount()).toBe(1)
expect(creatorPty).toMatchObject({
runtimeSessionOwned: false,
title: 'creator title',
launchConfig: creatorLaunchConfig,
launchToken: 'creator-launch-token',
launchAgent: null
})
expect(openCodeClearPtyMock).not.toHaveBeenCalledWith(sessionId)
expect(piClearPtyMock).not.toHaveBeenCalledWith(sessionId)
clearProviderPtyState(sessionId)
})
it.each([
{ fence: 'inventory', expected: 'tab_not_found' },
{ fence: 'abort', expected: 'client_disconnected' }
] as const)('cleans a provisional runtime team before a $fence spawn exit', async (testCase) => {
const harness = createFullRuntimePaneRaceHarness()
const abort = new AbortController()
if (testCase.fence === 'abort') {
abort.abort()
}
const removeTeam = vi.spyOn(harness.teams, 'removeTeamForLaunchEnv')
await expect(
harness.spawnRuntime({
sessionId: `caller-owned-pre-spawn-${testCase.fence}`,
preAllocatedHandle: `term_pre_spawn_${testCase.fence}`,
signal: abort.signal,
acceptWorkspaceInventory: () => testCase.fence !== 'inventory'
})
).rejects.toThrow(testCase.expected)
expect(harness.providerSpawn).not.toHaveBeenCalled()
expect(removeTeam).toHaveBeenCalledOnce()
expect(harness.teams.getActiveTeamCount()).toBe(0)
expect(openCodeClearPtyMock).not.toHaveBeenCalledWith(
`caller-owned-pre-spawn-${testCase.fence}`
)
})
it.each([
{ fence: 'inventory', expected: 'tab_not_found' },
{ fence: 'abort', expected: 'client_disconnected' }
] as const)(
'abandons only the runtime waiter when a $fence changes after reservation',
async (testCase) => {
const harness = createFullRuntimePaneRaceHarness()
const sessionId = `caller-owned-post-spawn-${testCase.fence}`
const rendererSpawn = harness.spawnRenderer(sessionId)
await vi.waitFor(() => expect(harness.providerSpawn).toHaveBeenCalledOnce())
let inventoryAccepted = true
const abort = new AbortController()
const runtimeSpawn = harness.spawnRuntime({
sessionId,
preAllocatedHandle: `term_post_spawn_${testCase.fence}`,
signal: abort.signal,
acceptWorkspaceInventory: () => inventoryAccepted
})
await vi.waitFor(() => expect(harness.teams.getActiveTeamCount()).toBe(2))
if (testCase.fence === 'inventory') {
inventoryAccepted = false
} else {
abort.abort()
}
harness.pending[0]!.resolve({ id: sessionId })
await expect(rendererSpawn).resolves.toMatchObject({
id: sessionId,
spawnDisposition: 'created'
})
await expect(runtimeSpawn).rejects.toThrow(testCase.expected)
expect(harness.providerSpawn).toHaveBeenCalledOnce()
expect(harness.shutdown).not.toHaveBeenCalled()
expect(harness.teams.getActiveTeamCount()).toBe(1)
expect(
(harness.runtime as unknown as { handleByPtyId: Map<string, string> }).handleByPtyId.get(
sessionId
)
).toBe('term_renderer_creator')
expect(openCodeClearPtyMock).not.toHaveBeenCalledWith(sessionId)
expect(piClearPtyMock).not.toHaveBeenCalledWith(sessionId)
clearProviderPtyState(sessionId)
}
)
it('isolates a changed folder route from the renderer creator reservation', async () => {
const harness = createFullRuntimePaneRaceHarness()
const oldPath = harness.folder.folderPath
const rendererSpawn = harness.spawnRenderer()
await vi.waitFor(() => expect(harness.providerSpawn).toHaveBeenCalledOnce())
harness.folder.folderPath = join(process.cwd(), 'src')
harness.group.parentPath = harness.folder.folderPath
const runtimeSpawn = harness.spawnRuntime({
preAllocatedHandle: 'term_current_folder_route',
acceptWorkspaceInventory: () => harness.folder.folderPath.endsWith('src')
})
expect(harness.providerSpawn).toHaveBeenCalledOnce()
const oldSessionId = harness.pending[0]!.options.sessionId!
harness.pending[0]!.resolve({ id: oldSessionId })
await expect(rendererSpawn).resolves.toMatchObject({ id: oldSessionId })
await vi.waitFor(() => expect(harness.providerSpawn).toHaveBeenCalledTimes(2))
expect(harness.pending.map(({ options }) => options.cwd)).toEqual([
oldPath,
harness.folder.folderPath
])
const currentSessionId = harness.pending[1]!.options.sessionId!
expect(currentSessionId).not.toBe(oldSessionId)
harness.pending[1]!.resolve({ id: currentSessionId })
await expect(runtimeSpawn).resolves.toMatchObject({
handle: 'term_current_folder_route',
ptyId: currentSessionId,
worktreeId: harness.folderKey
})
expect(harness.providerSpawn).toHaveBeenCalledTimes(2)
expect(harness.shutdown).not.toHaveBeenCalled()
clearProviderPtyState(oldSessionId)
clearProviderPtyState(currentSessionId)
})
it.each(['runtime', 'renderer'] as const)(
'preserves caller-owned session state for a %s pane waiter',
async (waiterKind) => {
@@ -73,6 +73,28 @@ function createServiceWithLeader(): {
}
describe('ClaudeAgentTeamsService', () => {
it('removes only the team named by its launch identity', () => {
const service = new ClaudeAgentTeamsService()
const first = service.createLaunchEnv({
leaderHandle: 'shared-handle',
baseEnv: {},
shimDir: '/tmp/orca-shim',
shimBin: 'orca'
})
const second = service.createLaunchEnv({
leaderHandle: 'shared-handle',
baseEnv: {},
shimDir: '/tmp/orca-shim',
shimBin: 'orca'
})
service.removeTeamForLaunchEnv(first.env)
expect(service.getActiveTeamCount()).toBe(1)
service.removeTeamForLaunchEnv(second.env)
expect(service.getActiveTeamCount()).toBe(0)
})
it('supports Claude core tmux teammate sequence with native splits', async () => {
const { service, teamId, token, leaderPane, api, splitCalls } = createServiceWithLeader()
const request = (argv: string[]) =>
@@ -81,6 +81,15 @@ export class ClaudeAgentTeamsService {
}
}
removeTeamForLaunchEnv(env: Readonly<Record<string, string>>): void {
const teamId = env.ORCA_AGENT_TEAMS_TEAM_ID
const token = env.ORCA_AGENT_TEAMS_TOKEN
const team = teamId ? this.teams.get(teamId) : undefined
if (team && team.token === token) {
this.teams.delete(teamId)
}
}
getActiveTeamCount(): number {
return this.teams.size
}
+62 -7
View File
@@ -25429,13 +25429,26 @@ export class OrcaRuntimeService {
)
const terminalColorQueryReplies =
launchOpts.terminalColorQueryReplies ?? getTerminalViewColorQueryReplyColors()
if (launchOpts.signal?.aborted) {
throw new Error('client_disconnected')
let ownsProvisionalAgentTeam = agentTeamsPlan !== null
const discardProvisionalAgentTeam = (): void => {
if (!ownsProvisionalAgentTeam || !agentTeamsPlan) {
return
}
ownsProvisionalAgentTeam = false
this.claudeAgentTeams.removeTeamForLaunchEnv(agentTeamsPlan.env)
}
if (launchOpts.acceptWorkspaceInventory?.() === false) {
throw new Error('tab_not_found')
const getPreAdoptionRejection = (): string | null => {
if (launchOpts.signal?.aborted) {
return 'client_disconnected'
}
return launchOpts.acceptWorkspaceInventory?.() === false ? 'tab_not_found' : null
}
const result = await this.ptyController.spawn({
const preSpawnRejection = getPreAdoptionRejection()
if (preSpawnRejection) {
discardProvisionalAgentTeam()
throw new Error(preSpawnRejection)
}
const spawnPromise = this.ptyController.spawn({
cols: 120,
rows: 40,
cwd,
@@ -25492,7 +25505,16 @@ export class OrcaRuntimeService {
? { persistHostSessionBinding: true }
: {})
})
if (launchOpts.acceptWorkspaceInventory?.() === false) {
let result: Awaited<typeof spawnPromise>
try {
result = await spawnPromise
} catch (error) {
discardProvisionalAgentTeam()
throw error
}
const postSpawnRejection = getPreAdoptionRejection()
if (postSpawnRejection) {
discardProvisionalAgentTeam()
const shouldKill = result.spawnRetirementToken
? this.ptyController.releaseSpawnReservation?.(result.id, result.spawnRetirementToken) ===
true
@@ -25503,7 +25525,38 @@ export class OrcaRuntimeService {
if (result.spawnDisposition === 'created') {
this.releaseRejectedPtyRegistrationFence(result.id, result.incarnationId)
}
throw new Error('tab_not_found')
throw new Error(postSpawnRejection)
}
if (result.spawnDisposition === 'awaited') {
discardProvisionalAgentTeam()
const creatorPty = this.ptysById.get(result.id)
if (!creatorPty) {
if (
result.spawnRetirementToken &&
this.ptyController.releaseSpawnReservation?.(result.id, result.spawnRetirementToken) ===
true
) {
this.ptyController.kill(result.id)
}
throw new Error('terminal_not_found')
}
if (result.spawnRetirementToken) {
this.ptyController.adoptSpawnReservation?.(result.id, result.spawnRetirementToken)
}
reportPtySpawnCommitted()
return {
handle: this.issuePtyHandle(creatorPty),
...(creatorPty.tabId ? { tabId: creatorPty.tabId } : {}),
paneKey: creatorPty.paneKey,
ptyId: creatorPty.ptyId,
worktreeId: creatorPty.worktreeId,
title: getLatestPtyTitle(creatorPty),
...this.getPtyExecutionHostMetadata(creatorPty.ptyId),
surface: 'background',
...(result.agentSessionEnsure
? { agentSessionDisposition: result.agentSessionEnsure.disposition }
: {})
}
}
if (result.spawnRetirementToken) {
this.ptyController.adoptSpawnReservation?.(result.id, result.spawnRetirementToken)
@@ -25519,12 +25572,14 @@ export class OrcaRuntimeService {
try {
this.assertPtyDidNotExitBeforeRegistration(result.id, result.incarnationId)
} catch (error) {
discardProvisionalAgentTeam()
if (error instanceof Error && error.message === 'agent_session_exited_during_start') {
this.releaseRejectedPtyRegistrationFence(result.id, result.incarnationId)
}
throw error
}
this.registerPreAllocatedHandleForPty(result.id, preAllocatedHandle)
ownsProvisionalAgentTeam = false
if (result.wslDistro) {
this.preparePtyExecutionContext(result.id, result.wslDistro)
}