Files
orca/src/main/codex/codex-structured-thread-open.ts
T
Brennan Benson 4d70b189de Harden the Codex app-server session against partial acquisition
Six findings from the review of the structured Codex session, all of them
about a session that is live in the map while the thing it names is not:

- A prompt is addressed by its own approval id, not by the tool item it is
  about. A shell bridge re-asks per command under one parent item, and the
  second ask used to evict the first, leaving that turn blocked on a request
  nobody could answer.
- A subagent runs its own thread over the same connection. Its `turn/started`
  no longer satisfies the root thread's waiter, and every event now carries the
  thread it actually came from.
- Publication is one step at the end of acquire: the process identity is in
  hand, the child is still alive, and the attempt still owns the session. A
  failure reaps only its own child, so a replacement already published keeps
  running.
- Codex talks before its thread is named. Those events wait in the acquisition
  window and are delivered in arrival order once the session is published,
  instead of being dropped.
- A broken stdin pipe ends the transport rather than one write: the child is
  reaped and the owner learns its lease is worthless.
- A death that reaches us through several listeners reports once, keeping the
  first cause rather than the generic exit that follows it.
2026-08-09 16:48:51 -07:00

38 lines
1.4 KiB
TypeScript

// Starting or resuming the single Codex thread a structured session owns.
//
// The reply is verified before the caller registers the session, because a
// resume that lands on a different thread is a fork wearing a resume's name —
// recording it would make the durable handle chain lie about what this session
// actually proved.
import type { CodexAppServerConnection } from './codex-app-server-connection'
import { readCodexThreadId, readCodexThreadPath } from './codex-structured-thread-facts'
export type CodexOpenedThread = {
threadId: string
/** Rollout file Codex named, when it named one. */
historyPath: string | null
}
export async function openCodexThread(
connection: CodexAppServerConnection,
launch: { cwd: string; resumeThreadId: string | null },
timeoutMs: number | undefined
): Promise<CodexOpenedThread> {
const opened = await connection.request(
launch.resumeThreadId ? 'thread/resume' : 'thread/start',
launch.resumeThreadId
? { threadId: launch.resumeThreadId, cwd: launch.cwd }
: { cwd: launch.cwd },
{ timeoutMs }
)
const threadId = readCodexThreadId(opened)
if (!threadId) {
throw new Error('codex app-server did not name the thread it opened')
}
if (launch.resumeThreadId && threadId !== launch.resumeThreadId) {
throw new Error(`codex app-server resumed ${threadId} instead of ${launch.resumeThreadId}`)
}
return { threadId, historyPath: readCodexThreadPath(opened) }
}