Files
orca/src/main/codex/codex-structured-session-state.ts
T
Brennan BensonandMerge Sim 872bd51d47 fix(native-chat): reland large structured command results (#17720)
* fix(native-chat): preserve large structured command results (#17707)

* fix(native-chat): preserve large structured command results

* chore: place native chat validation artifacts under docs

* chore: drop stale root package config

* fix(native-chat): enforce rebuilt lifecycle append slots

---------

Co-authored-by: Merge Sim <sim@local>

* chore: omit native-chat reland planning docs

* fix(native-chat): remove journal store import cycle

* fix(native-chat): keep journal factory acyclic

---------

Co-authored-by: Merge Sim <sim@local>
2026-08-31 13:12:19 -07:00

200 lines
6.2 KiB
TypeScript

import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types'
import { randomUUID } from 'node:crypto'
import { cancelProcessAcquisition } from '../../shared/child-process/cancel-process-acquisition'
import type {
CodexAppServerConnection,
openCodexAppServerConnection
} from './codex-app-server-connection'
import { CodexAcquisitionWindow } from './codex-structured-acquisition-window'
import type { CodexJournalTranslator } from './codex-structured-journal-translation'
import type { CodexTurnProcessSnapshot } from './codex-structured-turn-processes'
import type { StructuredAgentSessionLifecycleEvent } from '../native-chat/agent-session-wire/structured-agent-session-adapter'
export type CodexStructuredLaunch = {
command: string
args: string[]
cwd: string
codexHome: string | null
resumeThreadId: string | null
resumePath?: string | null
env?: Record<string, string>
}
export type CodexStructuredSessionEvent =
| { type: 'notification'; sessionId: string; threadId: string; method: string; params: unknown }
| { type: 'server-request'; sessionId: string; threadId: string; method: string; params: unknown }
| { type: 'provider-frame'; sessionId: string; threadId: string; kind: string; payload: unknown }
| {
type: 'prompt'
sessionId: string
threadId: string
method: string
params: unknown
codexItemId: string
promptKey: string
}
| StructuredAgentSessionLifecycleEvent
/** Translator-only compatibility for callers that do not participate in host recovery. */
| { type: 'ended'; sessionId: string; reason: string }
export type CodexStructuredSessionAdapterDeps = {
resolveLaunch: (input: {
identity: AgentSessionJournalIdentity
}) => Promise<CodexStructuredLaunch>
onEvent?: (event: CodexStructuredSessionEvent) => void
openConnection?: typeof openCodexAppServerConnection
readProcessStartTime?: (pid: number) => Promise<number | null>
mintLinkId?: () => string
mintAcquisitionGeneration?: () => string
now?: () => number
requestTimeoutMs?: number
captureTurnProcesses?: (rootPid: number) => Promise<CodexTurnProcessSnapshot | null>
terminateTurnProcesses?: (
rootPid: number,
baseline: CodexTurnProcessSnapshot | null
) => Promise<boolean>
}
export type CodexSession = {
connection: CodexAppServerConnection
ended: boolean
requestedClose: boolean
fence: number
acquisitionGeneration: string
threadId: string
historyPath: string | null
prompts: CodexAcquisitionWindow['prompts']
options: Map<string, string>
reportedOptions: { model?: string; effort?: string }
turnIdWaiters: ((turnId: string) => void)[]
translator: CodexJournalTranslator | null
unbindReadingControl?: () => void
/** Terminates this exact child as an unexpected death and enters host recovery. */
forceCloseUnexpected?: (reason: Error) => Promise<boolean>
}
export function mintCodexAcquisitionGeneration(deps: CodexStructuredSessionAdapterDeps): string {
return deps.mintAcquisitionGeneration?.() ?? randomUUID()
}
export function codexSessionLifecycle(
fence: number,
acquisitionGeneration: string
): Pick<CodexSession, 'ended' | 'requestedClose' | 'fence' | 'acquisitionGeneration'> {
return { ended: false, requestedClose: false, fence, acquisitionGeneration }
}
export function requireLiveCodexSession(
sessions: Map<string, CodexSession>,
sessionId: string
): CodexSession {
const session = sessions.get(sessionId)
if (!session || session.ended) {
throw new Error(`no live codex app-server for session ${sessionId}`)
}
return session
}
export type CodexAcquisitionAttempt = {
window: CodexAcquisitionWindow
cancelled: boolean
exitProven: boolean
finished: Promise<void>
finish: () => void
}
export function createCodexAcquisitionAttempt(): CodexAcquisitionAttempt {
let finish = (): void => {}
const finished = new Promise<void>((resolve) => {
finish = resolve
})
return {
window: new CodexAcquisitionWindow(),
cancelled: false,
exitProven: false,
finished,
finish
}
}
export class CodexAcquisitionRegistry {
private readonly attempts = new Map<string, CodexAcquisitionAttempt>()
private closing = false
get size(): number {
return this.attempts.size
}
start(sessionId: string): {
previousAttempt: CodexAcquisitionAttempt | undefined
attempt: CodexAcquisitionAttempt
} {
if (this.closing) {
throw new Error('codex structured session adapter is closing')
}
const previousAttempt = this.attempts.get(sessionId)
const attempt = createCodexAcquisitionAttempt()
this.attempts.set(sessionId, attempt)
return { previousAttempt, attempt }
}
assertCurrent(sessionId: string, attempt: CodexAcquisitionAttempt): void {
if (this.closing || attempt.cancelled || this.attempts.get(sessionId) !== attempt) {
throw new Error(`codex session ${sessionId} was superseded while being acquired`)
}
}
get(sessionId: string): CodexAcquisitionAttempt | undefined {
return this.attempts.get(sessionId)
}
deleteIfCurrent(sessionId: string, attempt: CodexAcquisitionAttempt): void {
if (this.attempts.get(sessionId) === attempt) {
this.attempts.delete(sessionId)
}
}
restoreIfCurrent(
sessionId: string,
replacement: CodexAcquisitionAttempt,
previous: CodexAcquisitionAttempt
): void {
if (this.attempts.get(sessionId) === replacement) {
this.attempts.set(sessionId, previous)
}
}
async closeFailedAttempt(sessionId: string, attempt: CodexAcquisitionAttempt): Promise<boolean> {
const stopped = (await attempt.window.connection?.close()) ?? true
if (stopped) {
attempt.exitProven = true
this.deleteIfCurrent(sessionId, attempt)
}
return stopped
}
sessionIds(): IterableIterator<string> {
return this.attempts.keys()
}
close(): void {
this.closing = true
}
}
export async function cancelCodexAcquisitionAttempt(
attempt: CodexAcquisitionAttempt | undefined
): Promise<boolean> {
if (!attempt) {
return true
}
return cancelProcessAcquisition({
cancel: () => {
attempt.cancelled = true
},
connection: () => attempt.window.connection,
exitProven: () => attempt.exitProven,
finished: attempt.finished
})
}