Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions config/scripts/ci-shard-timings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1211,8 +1211,8 @@
"src/main/codex/codex-app-server-capability-cache.test.ts": 64,
"src/main/codex/codex-app-server-client.test.ts": 1502,
"src/main/codex/codex-app-server-connection.test.ts": 5199,
"src/main/codex/codex-app-server-posix-supervisor.test.ts": 38,
"src/main/codex/codex-app-server-process-teardown.test.ts": 71,
"src/main/provider-process/provider-process-supervisor.test.ts": 38,
"src/main/provider-process/provider-process-teardown.test.ts": 71,
"src/main/codex/codex-app-server-session.test.ts": 129,
"src/main/codex/codex-app-server-teardown.integration.test.ts": 21677,
"src/main/codex/codex-background-command-tracker.test.ts": 221,
Expand Down
4 changes: 3 additions & 1 deletion config/scripts/pr-code-change-scope.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ const CODEX_INDEX_HEAL_CONTRACT_PREFIXES = [
'src/main/codex/codex-state-db',
'src/main/sqlite/sync-database',
'src/main/codex/codex-app-server-capability-signal',
'src/main/codex/codex-process-exit-deadline',
'src/main/provider-process/provider-process-exit-deadline',
'src/main/provider-process/provider-process-launch',
'src/main/provider-process/provider-record-reader',
'src/main/codex/codex-session-backfill',
'src/main/codex/codex-session-index-heal-state',
'src/main/codex-cli/command',
Expand Down
4 changes: 3 additions & 1 deletion config/scripts/pr-code-change-scope.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,9 @@ describe('per-job path classification', () => {
// Keep the real-binary gate live when a transport or launch dependency changes.
for (const file of [
'src/main/codex/codex-app-server-capability-signal.ts',
'src/main/codex/codex-process-exit-deadline.ts',
'src/main/provider-process/provider-process-exit-deadline.ts',
'src/main/provider-process/provider-process-launch.ts',
'src/main/provider-process/provider-record-reader.ts',
'src/main/codex/codex-session-backfill.ts',
'src/main/codex/codex-session-index-heal-state.ts',
'src/main/codex-cli/command.ts',
Expand Down
5 changes: 3 additions & 2 deletions config/tsconfig.cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
"../src/main/codex/codex-app-server-capability-signal.ts",
"../src/main/codex/codex-app-server-client.ts",
"../src/main/codex/codex-app-server-process-tree-kill.ts",
"../src/main/codex/codex-app-server-record-reader.ts",
"../src/main/provider-process/provider-record-reader.ts",
"../src/main/codex/codex-app-server-session.ts",
"../src/main/codex/codex-config-mirror.ts",
"../src/main/codex/codex-config-mirror-merge.ts",
Expand Down Expand Up @@ -149,7 +149,8 @@
"../src/main/codex/codex-hook-retired-commands.ts",
"../src/main/codex/codex-hook-wsl-runtime.ts",
"../src/main/codex/codex-managed-trust-reconciliation.ts",
"../src/main/codex/codex-process-exit-deadline.ts",
"../src/main/provider-process/provider-process-exit-deadline.ts",
"../src/main/provider-process/provider-process-launch.ts",
"../src/main/codex/codex-real-home-background-grant.ts",
"../src/main/codex/codex-real-home-entry-trust.ts",
"../src/main/codex/codex-real-home-hook-entry-plan.ts",
Expand Down
8 changes: 4 additions & 4 deletions src/main/claude/claude-agent-sdk-process-spawn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { describe, expect, it, vi } from 'vitest'
import type { SpawnOptions as SdkSpawnOptions } from '@anthropic-ai/claude-agent-sdk'
import { resolveSpawn, type spawnProcess } from '../../shared/child-process/run-process'
import type { ProcessSpec } from '../../shared/child-process/process-spec'
import type * as ProviderSupervisor from '../codex/codex-app-server-posix-supervisor'
import { createProviderSpawnSpec } from '../codex/codex-app-server-posix-supervisor'
import type * as ProviderSupervisor from '../provider-process/provider-process-supervisor'
import { createProviderSpawnSpec } from '../provider-process/provider-process-supervisor'
import { createClaudeCodeProcessSpawn } from './claude-agent-sdk-process-spawn'
import { proveClaudeChildExitWithReaper } from './claude-child-exit-proof-ladder'

vi.mock('../codex/codex-app-server-posix-supervisor', async (importOriginal) => {
vi.mock('../provider-process/provider-process-supervisor', async (importOriginal) => {
const actual = await importOriginal<typeof ProviderSupervisor>()
return { ...actual, createProviderSpawnSpec: vi.fn(actual.createProviderSpawnSpec) }
})
Expand Down Expand Up @@ -110,7 +110,7 @@ describe('claude agent SDK process spawn', () => {
'stops Claude by the spawn spec\u2019s supervision on $platform, never the platform',
async ({ platform, specSupervised }) => {
const actual = await vi.importActual<typeof ProviderSupervisor>(
'../codex/codex-app-server-posix-supervisor'
'../provider-process/provider-process-supervisor'
)
vi.mocked(createProviderSpawnSpec).mockImplementationOnce((...args) => ({
...actual.createProviderSpawnSpec(...args),
Expand Down
2 changes: 1 addition & 1 deletion src/main/claude/claude-agent-sdk-process-spawn.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { SpawnOptions as ClaudeAgentSdkSpawnOptions } from '@anthropic-ai/claude-agent-sdk'
import { spawnProcess } from '../../shared/child-process/run-process'
import { createProviderSpawnSpec } from '../codex/codex-app-server-posix-supervisor'
import { createProviderSpawnSpec } from '../provider-process/provider-process-supervisor'

/** Derived rather than imported: only src/shared/child-process may name node:child_process. */
type ClaudeCodeChild = ReturnType<typeof spawnProcess>
Expand Down
2 changes: 1 addition & 1 deletion src/main/claude/claude-child-exit-proof-ladder.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import { PROVIDER_SUPERVISOR_MAX_STOP_MS } from '../codex/codex-app-server-posix-supervisor'
import { PROVIDER_SUPERVISOR_MAX_STOP_MS } from '../provider-process/provider-process-supervisor'
import type { ClaudeChildTreeReaper } from './claude-agent-sdk-exit-proof'
import { proveClaudeChildExitWithReaper } from './claude-child-exit-proof-ladder'

Expand Down
4 changes: 2 additions & 2 deletions src/main/claude/claude-child-exit-proof-ladder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { SpawnedProcess } from '../../shared/child-process/run-process'
import { waitForProcessExitUntil } from '../codex/codex-process-exit-deadline'
import { PROVIDER_SUPERVISOR_MAX_STOP_MS } from '../codex/codex-app-server-posix-supervisor'
import { waitForProcessExitUntil } from '../provider-process/provider-process-exit-deadline'
import { PROVIDER_SUPERVISOR_MAX_STOP_MS } from '../provider-process/provider-process-supervisor'
import type { ClaudeChildTreeReaper } from './claude-agent-sdk-exit-proof'

export const GRACEFUL_EXIT_MS = 1_500
Expand Down
2 changes: 1 addition & 1 deletion src/main/claude/claude-supervised-stop.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
PROVIDER_SIGTERM_GRACE_MS,
PROVIDER_STDIN_END_GRACE_MS,
PROVIDER_SUPERVISOR_MAX_STOP_MS
} from '../codex/codex-app-server-posix-supervisor'
} from '../provider-process/provider-process-supervisor'
import { proveClaudeChildExit } from './claude-agent-sdk-exit-proof'
import { createClaudeCodeProcessSpawn } from './claude-agent-sdk-process-spawn'

Expand Down
2 changes: 1 addition & 1 deletion src/main/codex/codex-app-server-connection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
type CodexAppServerConnection,
type CodexAppServerConnectionHandlers
} from './codex-app-server-connection'
import { PROVIDER_SUPERVISOR_MAX_STOP_MS } from './codex-app-server-posix-supervisor'
import { PROVIDER_SUPERVISOR_MAX_STOP_MS } from '../provider-process/provider-process-supervisor'
import { isCodexAppServerUnsupportedError } from './codex-app-server-session'

// close() waits out the supervisor's own stop before forcing the tree.
Expand Down
30 changes: 9 additions & 21 deletions src/main/codex/codex-app-server-connection.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { spawnProcess } from '../../shared/child-process/run-process'
import { RetryableProcessExitProof } from '../../shared/child-process/retryable-process-exit-proof'
import type { ProviderProcessLaunch } from '../provider-process/provider-process-launch'
import {
createProviderSpawnSpec,
PROVIDER_SUPERVISOR_MAX_STOP_MS
} from './codex-app-server-posix-supervisor'
} from '../provider-process/provider-process-supervisor'
import { buildCodexAppServerExitError } from './codex-app-server-exit-error'
import { initializeCodexAppServerConnection } from './codex-app-server-handshake'
import { CodexAppServerHandshakeExitUnprovenError } from './codex-app-server-handshake-exit-proof'
import { terminateCodexAppServerProcessTree } from './codex-app-server-process-teardown'
import { waitForProcessExitUntil } from './codex-process-exit-deadline'
import { terminateProviderProcessTree } from '../provider-process/provider-process-teardown'
import { waitForProcessExitUntil } from '../provider-process/provider-process-exit-deadline'
import {
CodexAppServerTimeoutError,
CodexAppServerUnsupportedError
} from './codex-app-server-session'
import { createCodexAppServerRecordDispatcher } from './codex-app-server-record-dispatch'
import { createCodexAppServerRecordReader } from './codex-app-server-record-reader'
import { createProviderRecordReader } from '../provider-process/provider-record-reader'
import type {
CodexAppServerConnection,
CodexAppServerConnectionHandlers
Expand All @@ -34,16 +35,7 @@ export { CodexAppServerFrameSizeError } from './codex-app-server-frame-size-erro
// Structured chat needs a persistent bidirectional child and per-request deadlines;
// the request-scoped app-server runner cannot carry approvals or streamed turns.

export type CodexAppServerLaunch = {
command: string
args: string[]
/** Workspace directory used by the provider process itself. */
cwd?: string
/** Overlay on the inherited environment — the pinned CODEX_HOME lives here. */
env?: Record<string, string>
/** Keys stripped after the overlay, matching `CodexAppServerInvocation`. */
envToDelete?: readonly string[]
}
export type CodexAppServerLaunch = ProviderProcessLaunch

const DEFAULT_REQUEST_TIMEOUT_MS = 30_000
export const GRACEFUL_EXIT_MS = 1_500
Expand All @@ -60,17 +52,13 @@ export async function openCodexAppServerConnection(
handlers: CodexAppServerConnectionHandlers = {},
spawnImpl: typeof spawnProcess = spawnProcess
): Promise<CodexAppServerConnection> {
const childEnv: NodeJS.ProcessEnv = { ...process.env, ...launch.env }
for (const key of launch.envToDelete ?? []) {
delete childEnv[key]
}
const spawnSpec = createProviderSpawnSpec(launch, childEnv, process.platform)
const spawnSpec = createProviderSpawnSpec(launch, process.env, process.platform)
const child = spawnImpl(spawnSpec)

function terminateProcessTree(): Promise<boolean> {
// The supervisor and provider own separate POSIX groups so the supervisor can prove the
// provider group empty before relaying its exit. Forced wrapper teardown uses descendant proof.
return terminateCodexAppServerProcessTree(child)
return terminateProviderProcessTree(child, { site: 'codex-app-server-teardown' })
}

let stderrTail = ''
Expand Down Expand Up @@ -156,7 +144,7 @@ export async function openCodexAppServerConnection(
void terminateProcessTree()
})

const recordReader = createCodexAppServerRecordReader({
const recordReader = createProviderRecordReader({
stdout: child.stdout,
onRecord: (parsed, line) => {
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
Expand Down
12 changes: 0 additions & 12 deletions src/main/codex/codex-app-server-jsonl.ts

This file was deleted.

6 changes: 3 additions & 3 deletions src/main/codex/codex-app-server-record-dispatch.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { NdjsonRejectedRecord } from '../../shared/main-process-ndjson-framer'
import type { CodexAppServerConnectionHandlers } from './codex-app-server-connection-types'
import { CodexAppServerFrameSizeError } from './codex-app-server-frame-size-error'
import { isAppServerRecord } from './codex-app-server-jsonl'
import { isProviderRecord } from '../provider-process/provider-json-record'
import { CodexAppServerRequestError } from './codex-app-server-request-error'
import {
CodexAppServerUnsupportedError,
Expand Down Expand Up @@ -92,7 +92,7 @@ export function createCodexAppServerRecordDispatcher(input: {
// interpreted this reply, and it already reported its own outcome.
const timedOutMethod = timedOutMethods.get(message.id)
timedOutMethods.delete(message.id)
const error = isAppServerRecord(message.error) ? message.error.message : undefined
const error = isProviderRecord(message.error) ? message.error.message : undefined
console.warn(
timedOutMethod
? `[codex-app-server] late reply to ${timedOutMethod} after timeout (id ${message.id})`
Expand All @@ -104,7 +104,7 @@ export function createCodexAppServerRecordDispatcher(input: {
pending.delete(message.id)
clearTimeout(waiter.timer)
const error = message.error
if (isAppServerRecord(error)) {
if (isProviderRecord(error)) {
const detail = typeof error.message === 'string' ? error.message : 'unknown error'
waiter.reject(
isCodexMethodNotFoundError(error)
Expand Down
12 changes: 5 additions & 7 deletions src/main/codex/codex-app-server-session.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { ChildProcessWithoutNullStreams } from 'node:child_process'
import { waitForProcessExitUntil } from './codex-process-exit-deadline'
import { waitForProcessExitUntil } from '../provider-process/provider-process-exit-deadline'
import { resolveProviderChildEnv } from '../provider-process/provider-process-launch'
import { stderrIndicatesMissingAppServer } from './codex-app-server-capability-signal'
import { withCliRuntimeOnPath } from '../../shared/node-cli-command-resolution'
import {
killCodexAppServerProcessTree,
spawnCodexAppServerProcess,
type CodexAppServerSpawn
} from './codex-app-server-process-tree-kill'
import { createCodexAppServerRecordReader } from './codex-app-server-record-reader'
import { createProviderRecordReader } from '../provider-process/provider-record-reader'

// Why: `codex app-server` is Orca's sanctioned RPC surface into Codex-owned
// state (hook trust hashes, the sqlite thread index). This module owns the
Expand Down Expand Up @@ -102,10 +103,7 @@ export async function runCodexAppServerSession<T>(
): Promise<T> {
// Why: a default-home grant must run against the real ~/.codex, so strip an
// inherited CODEX_HOME (envToDelete) after applying the overlay, not before.
const childEnv: NodeJS.ProcessEnv = { ...process.env, ...invocation.env }
for (const key of invocation.envToDelete ?? []) {
delete childEnv[key]
}
const childEnv = resolveProviderChildEnv(invocation, process.env)
const pairedEnv = invocation.cliPath
? withCliRuntimeOnPath(invocation.cliPath, childEnv)
: childEnv
Expand Down Expand Up @@ -154,7 +152,7 @@ export async function runCodexAppServerSession<T>(
failPending(error)
})

createCodexAppServerRecordReader({
createProviderRecordReader({
stdout: child.stdout,
onRecord: (parsed) => {
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
SUPERVISED_GRACEFUL_EXIT_MS
} from '../../claude/claude-child-exit-proof-ladder'
import { GRACEFUL_EXIT_MS as CODEX_WINDOWS_GRACEFUL_EXIT_MS } from '../../codex/codex-app-server-connection'
import { PROVIDER_SUPERVISOR_MAX_STOP_MS } from '../../codex/codex-app-server-posix-supervisor'
import { PROVIDER_SUPERVISOR_MAX_STOP_MS } from '../../provider-process/provider-process-supervisor'
import { SNAPSHOT_DRAIN_TIMEOUT_MS } from './structured-agent-session-eviction'
import {
CHILD_EVICTION_TIMEOUT_MS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import type { AgentSessionResumeTrigger } from '../../../shared/agent-session-resume-marker'
import { SUPERVISED_GRACEFUL_EXIT_MS } from '../../claude/claude-child-exit-proof-ladder'
import { PROVIDER_SUPERVISOR_MAX_STOP_MS } from '../../codex/codex-app-server-posix-supervisor'
import { PROVIDER_SUPERVISOR_MAX_STOP_MS } from '../../provider-process/provider-process-supervisor'
import { SNAPSHOT_DRAIN_TIMEOUT_MS } from './structured-agent-session-eviction'
import type { StructuredAgentSessionRestartResume } from './structured-agent-session-restart-resume-host'
import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '../../runtime/agent-session-older-build-lease.test-fixture'
import type { AgentSessionRecordStore } from '../../runtime/agent-session-record-store'
import { openTestAgentSessionRecordStore } from '../../runtime/agent-session-record-store-test-harness'
import { supervisedPosixLaunch } from '../../codex/codex-app-server-posix-supervisor'
import { supervisedPosixLaunch } from '../../provider-process/provider-process-supervisor'
import {
resolveStructuredSessionRecovery,
type StructuredSessionRecoveryResolutionDeps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
type AgentSessionOwnerProbe
} from '../../../shared/agent-session-lease-adjudication'
import type { AgentSessionRecord } from '../../../shared/agent-session-record'
import { PROVIDER_SUPERVISOR_MAX_STOP_MS } from '../../codex/codex-app-server-posix-supervisor'
import { PROVIDER_SUPERVISOR_MAX_STOP_MS } from '../../provider-process/provider-process-supervisor'
import { releaseUnprovenAgentSessionOwner } from '../../runtime/agent-session-lease-transitions'
import type { AgentSessionRecordStore } from '../../runtime/agent-session-record-store'

Expand Down
3 changes: 3 additions & 0 deletions src/main/provider-process/provider-json-record.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isProviderRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export async function waitForProcessExitUntil(
await Promise.race([exitPromise, timeout])
} finally {
// Why: this runs in a short-lived entry; a live grace timer delays the
// parent spawnSync even after the app-server process has already exited.
// parent spawnSync even after the provider process has already exited.
if (timer !== undefined) {
clearTimeout(timer)
}
Expand Down
27 changes: 27 additions & 0 deletions src/main/provider-process/provider-process-launch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, expect, it } from 'vitest'
import { resolveProviderChildEnv } from './provider-process-launch'

describe('resolveProviderChildEnv', () => {
it('overlays the launch env on the inherited env, then strips deleted keys', () => {
const baseEnv = { PATH: '/bin', AGENT_HOME: '/inherited', PARENT_AGENT: 'parent' }
const overlay = { AGENT_HOME: '/pinned', LAUNCH_ONLY: 'added', PARENT_AGENT: 'overlay' }

const childEnv = resolveProviderChildEnv(
{ env: overlay, envToDelete: ['PARENT_AGENT'] },
baseEnv
)

expect(childEnv).toEqual({ PATH: '/bin', AGENT_HOME: '/pinned', LAUNCH_ONLY: 'added' })
expect(baseEnv).toEqual({ PATH: '/bin', AGENT_HOME: '/inherited', PARENT_AGENT: 'parent' })
expect(overlay.PARENT_AGENT).toBe('overlay')
})

it('copies the inherited env when the launch carries no env', () => {
const baseEnv = { PATH: '/bin' }

const childEnv = resolveProviderChildEnv({}, baseEnv)

expect(childEnv).toEqual(baseEnv)
expect(childEnv).not.toBe(baseEnv)
})
})
22 changes: 22 additions & 0 deletions src/main/provider-process/provider-process-launch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export type ProviderProcessLaunch = {
command: string
args: string[]
/** Workspace directory used by the provider process itself. */
cwd?: string
/** Overlay on the inherited environment. */
env?: Record<string, string>
/** Keys stripped after the overlay. */
envToDelete?: readonly string[]
}

/** The one env rule for provider children: inherit, overlay, then strip (so a delete beats an overlay). */
export function resolveProviderChildEnv(
launch: Pick<ProviderProcessLaunch, 'env' | 'envToDelete'>,
baseEnv: NodeJS.ProcessEnv
): NodeJS.ProcessEnv {
const childEnv: NodeJS.ProcessEnv = { ...baseEnv, ...launch.env }
for (const key of launch.envToDelete ?? []) {
delete childEnv[key]
}
return childEnv
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
PROVIDER_SUPERVISOR_MAX_STOP_MS,
supervisedPosixLaunch,
type ProviderSupervisorOptions
} from './codex-app-server-posix-supervisor'
} from './provider-process-supervisor'

// The provider leads its own group; its grandchild shares that group and ignores SIGTERM.
const PROVIDER = String.raw`
Expand Down
Loading
Loading