Files
orca/src/main/codex/codex-structured-session-acquire.ts
T
Brennan Benson b161aa38b1 Native chat: a subagent row is written through the child whose report it copies
Review of c0add67cca: the subagent row writer read its execution owner from
the host's installed child when a report landed. A child that is still being
acquired reports a resumed subagent before the host installs it, so its row
was written at once under the old owner, then the attach's settlement of what
the previous child left marked it unverifiable, and nothing rewrote it until
the next report.

Each adapter (Claude, Codex, ACP) now hands its child-work report to the host
with that child's own event sink, the write path its rows already take. The
host passes it through the status feed to the subagent row writer, which writes
the rows that report changes through that sink with `executes`. They land in
the child's own order, after the attach's settlement, at the fence the sink is
bound to, and that fence is the row's execution owner. A running subagent a
child has not written yet is written once even when its text is unchanged.
Changes the host makes itself (a Stop's acknowledgement, an end) still write
directly and keep the row's owner. The shared child-work evidence type is
unchanged: the sink travels as a separate argument.

Test: a Claude helper the next child reports again while it is still starting
stays working after that start's settlement, through the shipping adapter and
host. It fails on c0add67cca (the row lands `unverifiable`).
2026-10-14 22:13:19 -07:00

283 lines
11 KiB
TypeScript

import type { AgentSessionAccountKind } from '../../shared/agent-session-availability'
import {
CODEX_STRUCTURED_HANDLE_NAMESPACE,
isAgentSessionProviderHandleInNamespace
} from '../../shared/agent-session-provider-handle-encoding'
import {
AgentSessionAcquisitionRefusal,
AgentSessionPreSpawnError,
type AgentSessionAcquisition,
type StructuredAgentSessionAcquireInput
} from '../native-chat/agent-session-wire/structured-agent-session-adapter'
import {
closeFailedCodexAcquisition,
stopSupersededCodexAcquisition
} from './codex-structured-acquisition-lifecycle'
import { CodexBackgroundTaskTracker, codexChildWorkSink } from './codex-background-task-tracker'
import { CodexSubagentExecutions } from './codex-subagent-executions'
import { createCodexDispatchEchoes } from './codex-structured-dispatch-echo'
import { createCodexSessionJournalTranslator } from './codex-structured-session-journal'
import { openCodexAppServerConnection } from './codex-app-server-connection'
import {
codexProviderHandleLink,
codexSpawnedProcessIdentity
} from './codex-structured-owner-identity'
import { openCodexThread } from './codex-structured-thread-open'
import { withCodexVisualsThreadConfig } from './codex-structured-visuals'
import {
closeCodexPublishedSession,
handleCodexSessionExit
} from './codex-structured-session-close'
import { restoredCodexSessionOptions } from './codex-structured-session-options'
import { startBackgroundCodexCatalogRefresh } from './codex-structured-background-catalog'
import { codexAcquireCatalogAccess } from './codex-structured-acquire-catalog'
import { reportedCodexThreadOptions } from './codex-structured-service-tier'
import {
assertCodexConnectionOpen,
codexSessionLifecycle,
mintCodexAcquisitionGeneration,
type CodexAcquisitionRegistry,
type CodexAcquisitionAttempt,
type CodexSession,
type CodexStructuredSessionAdapterDeps
} from './codex-structured-session-state'
import type { CodexStructuredSessionTeardown } from './codex-structured-session-teardown'
import type { CodexStructuredNotificationRetry } from './codex-structured-notification-retry'
import type { deliverCodexServerRequest } from './codex-structured-provider-events'
import { codexAcquisitionNotificationHandler } from './codex-structured-acquisition-notification'
export async function acquireCodexStructuredSession(input: {
input: StructuredAgentSessionAcquireInput
deps: CodexStructuredSessionAdapterDeps
sessions: Map<string, CodexSession>
acquisitions: CodexAcquisitionRegistry
notificationRetries: CodexStructuredNotificationRetry
deliver: (
acquisition: CodexAcquisitionAttempt['window'],
sessionId: string,
event: () => unknown,
retainedBytes?: number
) => void
handleServerRequest: (
sessionId: string,
request: Parameters<typeof deliverCodexServerRequest>[2]
) => void
handleUnhandledFrame: (sessionId: string, kind: string, payload: unknown) => void
forceCloseUnexpected: CodexStructuredSessionTeardown['forceCloseUnexpected']
}): Promise<AgentSessionAcquisition> {
const { input: acquireInput, deps, sessions, acquisitions, notificationRetries } = input
const sessionId = acquireInput.identity.sessionId
const { previousAttempt, attempt } = acquisitions.start(sessionId)
const acquisition = attempt.window
let unbindReadingControl: (() => void) | undefined
const provenHandle = acquireInput.identity.providerHandle
let primaryThreadId =
provenHandle &&
isAgentSessionProviderHandleInNamespace(provenHandle, CODEX_STRUCTURED_HANDLE_NAMESPACE)
? provenHandle.nativeId
: null
const subagentExecutions = new CodexSubagentExecutions()
const dispatchEchoes = createCodexDispatchEchoes()
let account: AgentSessionAccountKind | undefined
// Minted before the translator, which names this connection's frame rows with it.
const acquisitionGeneration = mintCodexAcquisitionGeneration(deps)
const translator = createCodexSessionJournalTranslator({
sink: acquireInput.events,
account: () => account,
sessionId,
acquisitionId: acquisitionGeneration,
deps,
primaryThreadId: () => primaryThreadId,
dispatchEchoes,
subagentExecutions,
prompts: acquisition.prompts
})
const open = deps.openConnection ?? openCodexAppServerConnection
const spawnIdentity = codexSpawnedProcessIdentity(acquireInput, deps.readProcessStartTime)
try {
await stopSupersededCodexAcquisition({
sessionId,
registry: acquisitions,
replacement: attempt,
previous: previousAttempt
})
acquisitions.assertCurrent(sessionId, attempt)
if (
!(await closeCodexPublishedSession(
sessions,
sessionId,
deps.onEvent,
deps.logger ? { logger: deps.logger } : {}
))
) {
throw new Error(`codex app-server for session ${sessionId} could not be stopped`)
}
acquisitions.assertCurrent(sessionId, attempt)
const launch = await deps
.resolveLaunch({
identity: acquireInput.identity,
spawnToken: acquireInput.spawnToken,
...(acquireInput.permissionMode ? { permissionMode: acquireInput.permissionMode } : {})
})
.catch((error: unknown) => {
throw new AgentSessionPreSpawnError(error)
})
acquisitions.assertCurrent(sessionId, attempt)
account = launch.codexHome ? deps.resolveAccountKind?.(launch.codexHome) : undefined
const connection = await open(
{
command: launch.command,
args: launch.args,
cwd: launch.cwd,
...(launch.env ? { env: launch.env } : {}),
...(launch.envToDelete ? { envToDelete: launch.envToDelete } : {})
},
{
onNotification: codexAcquisitionNotificationHandler({
acquisition,
sessionId,
dispatchEchoes,
notificationRetries,
deliver: input.deliver,
now: deps.now ?? Date.now
}),
onServerRequest: (request) =>
input.deliver(
acquisition,
sessionId,
() => input.handleServerRequest(sessionId, request),
Buffer.byteLength(JSON.stringify(request), 'utf8')
),
onUnhandledFrame: (kind, payload) =>
input.deliver(
acquisition,
sessionId,
() => input.handleUnhandledFrame(sessionId, kind, payload),
Buffer.byteLength(JSON.stringify(payload ?? null), 'utf8')
),
onSpawned: spawnIdentity.onSpawned,
...(acquireInput.onOutput ? { onOutput: acquireInput.onOutput } : {}),
onExit: (error, exit) => {
try {
handleCodexSessionExit({
sessions,
sessionId,
connection: acquisition.connection,
error,
// The end of a close Orca began, even one that came back unproven before it.
...(exit?.expected ? { closedByOrca: true as const } : {}),
...(deps.logger ? { logger: deps.logger } : {}),
prompts: acquisition.prompts,
...(deps.onEvent ? { onEvent: deps.onEvent } : {})
})
} finally {
notificationRetries.clear(sessionId, acquisition.connection)
}
}
}
)
acquisition.connection = connection
if (connection.pauseReading && connection.resumeReading) {
unbindReadingControl = acquireInput.events?.bindReadingControl?.({
pauseReading: connection.pauseReading,
resumeReading: () => {
connection.resumeReading?.()
notificationRetries.retry(sessionId, connection)
}
})
}
acquisitions.assertCurrent(sessionId, attempt)
const threadLaunch = await withCodexVisualsThreadConfig(connection, launch, {
sessionId,
...(deps.logger ? { logger: deps.logger } : {})
})
acquisitions.assertCurrent(sessionId, attempt)
const opened = await openCodexThread(connection, threadLaunch, deps.requestTimeoutMs)
acquisitions.assertCurrent(sessionId, attempt)
primaryThreadId = opened.threadId
const restoreAdmission = translator?.restoreThread(opened.threadId, opened.thread ?? {})
if (restoreAdmission && !restoreAdmission.accepted) {
throw AgentSessionAcquisitionRefusal.historyTooLarge(
'Codex thread history exceeds the bounded restore queue; history was not partially imported.'
)
}
const process = await spawnIdentity.read(connection.pid)
acquisitions.assertCurrent(sessionId, attempt)
const acquired: AgentSessionAcquisition = {
process,
link: codexProviderHandleLink({
threadId: opened.threadId,
...(opened.supersededThreadId
? { resumed: false, supersedesThreadId: opened.supersededThreadId }
: { resumed: launch.resumeThreadId !== null }),
fence: acquireInput.fence,
linkId: deps.mintLinkId?.(),
observedAt: deps.now?.() ?? Date.now()
}),
acquisitionGeneration
}
assertCodexConnectionOpen(connection, sessionId)
acquisitions.assertCurrent(sessionId, attempt)
const options = restoredCodexSessionOptions(acquireInput.options)
const catalogAccess = codexAcquireCatalogAccess(deps, launch)
acquisitions.assertCurrent(sessionId, attempt)
assertCodexConnectionOpen(connection, sessionId)
acquisitions.deleteIfCurrent(sessionId, attempt)
// Where this session's child work goes: the host's records, after each frame is journaled.
const sink = codexChildWorkSink(sessionId, deps, acquireInput.events)
const session: CodexSession = {
account,
connection,
...codexSessionLifecycle(acquireInput.fence, acquired.acquisitionGeneration as string),
threadId: opened.threadId,
historyMode: opened.historyMode,
activeTurnIds: new Set(),
abortedTurnIds: new Set(),
prompts: acquisition.prompts,
options,
reportedOptions: reportedCodexThreadOptions(opened),
...(catalogAccess ? { catalogAccess } : {}),
dispatchEchoes,
translator,
backgroundTasks: new CodexBackgroundTaskTracker(opened.threadId, subagentExecutions, sink),
forceCloseUnexpected: (reason) =>
input.forceCloseUnexpected(
sessionId,
acquireInput.fence,
acquired.acquisitionGeneration as string,
reason
),
...(unbindReadingControl ? { unbindReadingControl } : {})
}
sessions.set(sessionId, session)
for (const event of acquisition.drain()) {
event()
}
startBackgroundCodexCatalogRefresh({
session,
sessionId,
sessions,
timeoutMs: deps.requestTimeoutMs,
logger: deps.logger
})
return acquired
} catch (error) {
if (sessions.get(sessionId)?.connection !== acquisition.connection) {
return closeFailedCodexAcquisition({
sessionId,
registry: acquisitions,
attempt,
cause: error,
dispose: () => {
unbindReadingControl?.()
translator?.dispose()
}
})
}
acquisitions.deleteIfCurrent(sessionId, attempt)
throw error
} finally {
attempt.finish()
}
}