fix(native-chat): preserve handoff launch authority

This commit is contained in:
Brennan Benson
2026-08-12 18:32:07 -07:00
parent 8b2601b2f2
commit 53a3a9442b
4 changed files with 80 additions and 32 deletions
@@ -10,6 +10,7 @@ import type {
StructuredAgentSessionHandoffFlowContext,
StructuredTuiOwner
} from './structured-agent-session-handoff-types'
import { StructuredTuiLaunchCleanupError } from './structured-agent-session-handoff-types'
export async function handoffStructuredSessionToTui(
context: StructuredAgentSessionHandoffFlowContext,
@@ -52,19 +53,32 @@ export async function handoffStructuredSessionToTui(
})
context.publishStage(record, 'to-tui')
let owner: StructuredTuiOwner | null = null
let processIdentityCommitted = false
try {
await deps.prepareTuiHistoryCatchup?.(sessionId, record.lease.runtimeFence)
owner = await deps.transport!.launchTui({
record,
fence: record.lease.runtimeFence,
spawnToken
})
await deps.store.commitProcessIdentity({
sessionId,
fence: record.lease.runtimeFence,
process: owner.process,
now: deps.now()
spawnToken,
onSpawned: async (spawnedOwner) => {
owner = spawnedOwner
await deps.store.commitProcessIdentity({
sessionId,
fence: record.lease.runtimeFence,
process: spawnedOwner.process,
now: deps.now()
})
processIdentityCommitted = true
}
})
if (!processIdentityCommitted) {
await deps.store.commitProcessIdentity({
sessionId,
fence: record.lease.runtimeFence,
process: owner.process,
now: deps.now()
})
}
record = await deps.store.proveOwner({
sessionId,
fence: record.lease.runtimeFence,
@@ -73,14 +87,20 @@ export async function handoffStructuredSessionToTui(
})
} catch (error) {
deps.stopTuiHistoryCatchup?.(sessionId)
if (!owner && error instanceof StructuredTuiLaunchCleanupError) {
await markManualRecovery(context, sessionId, operationId)
throw error
}
if (owner) {
if (!deps.transport?.stopFailedTuiLaunch) {
context.retainOwner(sessionId, owner)
await markManualRecovery(context, sessionId, operationId)
throw error
}
try {
await deps.transport.stopFailedTuiLaunch(owner)
} catch (stopError) {
context.retainOwner(sessionId, owner)
await markManualRecovery(context, sessionId, operationId)
throw new AggregateError(
[error, stopError],
@@ -14,12 +14,23 @@ export type StructuredTuiOwner = {
transcriptPath?: string
}
export class StructuredTuiLaunchCleanupError extends Error {
constructor(
launchError: unknown,
readonly cleanupError: unknown
) {
super('The failed terminal launch could not be proven stopped.', { cause: launchError })
this.name = 'StructuredTuiLaunchCleanupError'
}
}
export type StructuredAgentSessionHandoffTransport = {
hostLabel: string
launchTui(input: {
record: AgentSessionRecord
fence: number
spawnToken: string
onSpawned?: (owner: StructuredTuiOwner) => Promise<void>
}): Promise<StructuredTuiOwner>
reproveTuiOwner(input: {
record: AgentSessionRecord
@@ -785,6 +785,7 @@ describe('structured session ownership handoff', () => {
stage: 'manual-recovery',
error: { recoverableOwner: 'none' }
})
expect(acquireNativeCalls).toBe(0)
expect(store.getRecord(SESSION)?.lease.handoffStage).toBe('manual-recovery')
await coordinator.restore(SESSION)
expect(coordinator.status(SESSION)).toMatchObject({
+41 -25
View File
@@ -75,9 +75,10 @@ import {
import { ensureStructuredAgentSessionHost as installStructuredAgentSessionHost } from './structured-agent-session-runtime'
import { getStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry'
import type { AgentSessionAttachParams } from '../native-chat/agent-session-wire/structured-agent-session-attach'
import type {
StructuredAgentSessionHandoffTransport,
StructuredTuiOwner
import {
StructuredTuiLaunchCleanupError,
type StructuredAgentSessionHandoffTransport,
type StructuredTuiOwner
} from '../native-chat/agent-session-wire/structured-agent-session-handoff-types'
import type { AgentSessionRecord } from '../../shared/agent-session-record'
import {
@@ -9121,7 +9122,7 @@ export class OrcaRuntimeService {
private createStructuredAgentSessionHandoffTransport(): StructuredAgentSessionHandoffTransport {
return {
hostLabel: hostname(),
launchTui: async ({ record, fence, spawnToken }) => {
launchTui: async ({ record, fence, spawnToken, onSpawned }) => {
const head = record.providerHandleChain.at(-1)
if (head?.handle.provider !== 'codex') {
throw new Error('agent_session_identity_required')
@@ -9142,23 +9143,10 @@ export class OrcaRuntimeService {
if (!terminal.processId || !terminal.paneKey || !terminal.tabId || !terminal.ptyId) {
throw new Error('The resumed terminal did not publish a process identity.')
}
await this.waitForTerminal(terminal.handle, {
condition: 'tui-idle',
timeoutMs: 30_000
})
const proof = await this.waitForStructuredTuiProof({
handle: terminal.handle,
paneKey: terminal.paneKey,
threadId: head.handle.threadId,
spawnToken,
codexHome: record.accountHome.path,
sessionId: record.sessionId
})
const revealed = await this.focusTerminal(terminal.handle)
return this.refreshStructuredTuiOwnerBinding({
const spawnedOwner = this.refreshStructuredTuiOwnerBinding({
terminal: {
handle: terminal.handle,
tabId: revealed.tabId,
tabId: terminal.tabId,
paneKey: terminal.paneKey,
ptyId: terminal.ptyId
},
@@ -9173,15 +9161,43 @@ export class OrcaRuntimeService {
resumed: true,
fence,
observedAt: Date.now()
}),
})
})
await onSpawned?.(spawnedOwner)
await this.waitForTerminal(terminal.handle, {
condition: 'tui-idle',
timeoutMs: 30_000
})
const proof = await this.waitForStructuredTuiProof({
handle: terminal.handle,
paneKey: terminal.paneKey,
threadId: head.handle.threadId,
spawnToken,
codexHome: record.accountHome.path,
sessionId: record.sessionId
})
const revealed = await this.focusTerminal(terminal.handle)
return this.refreshStructuredTuiOwnerBinding({
...spawnedOwner,
terminal: {
handle: terminal.handle,
tabId: revealed.tabId,
paneKey: terminal.paneKey,
ptyId: terminal.ptyId
},
process: spawnedOwner.process,
...(proof.transcriptPath ? { transcriptPath: proof.transcriptPath } : {})
})
} catch (error) {
await this.closeTerminal(terminal.handle)
await this.waitForTerminal(terminal.handle, {
condition: 'exit',
timeoutMs: 15_000
})
try {
await this.closeTerminal(terminal.handle)
await this.waitForTerminal(terminal.handle, {
condition: 'exit',
timeoutMs: 15_000
})
} catch (cleanupError) {
throw new StructuredTuiLaunchCleanupError(error, cleanupError)
}
throw error
}
},