mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 00:03:47 +00:00
fix(native-chat): preserve handoff launch authority
This commit is contained in:
+27
-7
@@ -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({
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user