Files
openclaw/extensions/codex/src/app-server/attempt-startup.ts
Vito Cappello 6d7bc062e3 fix(session-catalog): hide OpenClaw-managed provider sessions (#125424)
* fix(session-catalog): hide OpenClaw-managed upstream sessions

* fix(codex): filter managed paired-node sessions

* fix(codex): classify legacy managed sessions

* fix(session-catalog): classify managed provider sessions

* fix(session-catalog): backfill inter-session ownership

* fix(session-catalog): classify Claude internal prompts

* fix(session-catalog): retain durable provenance

* fix(codex): keep rollout home derivation private

* fix(anthropic): declare catalog schema dependency

* fix(anthropic): avoid catalog schema dependency

* fix(session-catalog): scope managed ownership to Codex

* fix(codex): contain catalog provenance reads

* fix(codex): bind managed threads to catalog home

---------

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
Co-authored-by: Josh Lehman <550978+jalehman@users.noreply.github.com>
2026-08-20 19:51:55 -07:00

711 lines
32 KiB
TypeScript

/**
* Startup orchestration for Codex app-server attempts, including shared-client
* leasing, plugin thread config, sandbox environment, and thread lifecycle binding.
*/
import {
AgentHarnessPreflightError,
embeddedAgentLog,
formatErrorMessage,
type AgentHarnessRuntimeArtifactBinding,
type CodexBundleMcpThreadConfig,
type EmbeddedRunAttemptParamsV2 as EmbeddedRunAttemptParams,
type resolveSandboxContext,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import {
CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS,
CodexAppServerUnsafeSubscriptionError,
closeCodexStartupClientBestEffort,
unsubscribeCodexThreadBestEffort,
} from "./attempt-client-cleanup.js";
import { buildCodexPluginThreadConfigEligibilityLogData } from "./attempt-diagnostics.js";
import { verifyStartupArtifact } from "./attempt-runtime-artifact.js";
import {
CodexAppServerStartupError,
isCodexAppServerStartupError,
withCodexStartupTimeout,
} from "./attempt-timeouts.js";
import { ensureCodexAppServerClientRuntime } from "./client-runtime.js";
import {
isCodexAppServerBrokenPipeError,
isCodexAppServerConnectionClosedError,
isCodexAppServerRequestTimeoutError,
type CodexAppServerClient,
} from "./client.js";
import { startCodexComputerUseHealthMonitor } from "./computer-use-health.js";
import { ensureCodexComputerUse } from "./computer-use.js";
import {
withMcpElicitationsApprovalPolicy,
type CodexAppServerRuntimeOptions,
type CodexPluginConfig,
type ResolvedCodexComputerUseConfig,
} from "./config.js";
import {
resolveCodexAppServerExecutionCwd,
resolveCodexExternalSandboxPolicyForOpenClawSandbox,
resolveCodexSandboxEnvironmentSelection,
shouldRequireCodexSandboxExecServerEnvironment,
} from "./dynamic-tool-build.js";
import {
buildCodexAppServerRuntimeFingerprint,
buildCodexPluginAppCacheKey,
} from "./plugin-app-cache-key.js";
import {
createCodexPluginThreadConfigStartupProvider,
resolveCodexPluginThreadConfigStartupPolicy,
} from "./plugin-thread-config-deadline.js";
import {
buildCodexPluginThreadConfigInputFingerprint,
mergeCodexThreadConfigs,
} from "./plugin-thread-config.js";
import type {
CodexDynamicToolSpec,
CodexSandboxPolicy,
CodexTurnEnvironmentParams,
JsonObject,
} from "./protocol.js";
import {
ensureCodexSandboxExecServerEnvironment,
releaseCodexSandboxExecServerEnvironment,
type CodexSandboxExecEnvironment,
} from "./sandbox-exec-server.js";
import { buildScheduledCodexAppAuthorityInputFingerprint } from "./scheduled-app-authority.js";
import type { CodexAppServerBindingStore } from "./session-binding.js";
import {
clearSharedCodexAppServerClientIfCurrent,
clearSharedCodexAppServerClientIfCurrentAndUnclaimed,
isCodexAppServerStartSelectionChangedError,
releaseLeasedSharedCodexAppServerClient,
retireSharedCodexAppServerClientIfCurrent,
type CodexAppServerClientOptions,
type CodexAppServerClientFactory,
} from "./shared-client.js";
import {
startOrResumeThread,
type CodexAppServerThreadLifecycleBinding,
type CodexContextEngineThreadBootstrapProjection,
} from "./thread-lifecycle.js";
import {
getCodexAppServerTurnRouter,
type CodexAppServerTurnRouter,
type CodexThreadRouteReservation,
} from "./turn-router.js";
import type { CodexNativeWebSearchSupport } from "./web-search.js";
const CODEX_APP_SERVER_STARTUP_CONNECTION_CLOSE_MAX_ATTEMPTS = 3;
const CODEX_APP_SERVER_CONTEXT_RESTART_SELECTION_CHANGED =
"CODEX_APP_SERVER_CONTEXT_RESTART_SELECTION_CHANGED";
/** True when a pre-write context restart must replay on the newly selected owner. */
export function isCodexContextRestartSelectionChangedError(
error: unknown,
): error is Error & { code: typeof CODEX_APP_SERVER_CONTEXT_RESTART_SELECTION_CHANGED } {
return (
error instanceof Error &&
"code" in error &&
error.code === CODEX_APP_SERVER_CONTEXT_RESTART_SELECTION_CHANGED
);
}
type CodexSandboxContext = Awaited<ReturnType<typeof resolveSandboxContext>>;
/** Resources and bindings returned after a Codex attempt thread starts. */
type StartCodexAttemptThreadResult = {
client: CodexAppServerClient;
turnRouter: CodexAppServerTurnRouter;
turnRoute: CodexThreadRouteReservation;
thread: CodexAppServerThreadLifecycleBinding;
pluginAppServer: CodexAppServerRuntimeOptions;
sandboxEnvironment: CodexSandboxExecEnvironment | undefined;
environmentSelection: CodexTurnEnvironmentParams[] | undefined;
executionCwd: string;
sandboxPolicy: CodexSandboxPolicy | undefined;
runtimeArtifact?: AgentHarnessRuntimeArtifactBinding;
releaseSharedClientLease: () => void;
restartContextEngineCodexThread: () => Promise<CodexAppServerThreadLifecycleBinding>;
};
/**
* Starts or resumes the Codex app-server thread and returns the resources the
* run loop must later release.
*/
export async function startCodexAttemptThread(params: {
attemptClientFactory: CodexAppServerClientFactory;
bindingStore: CodexAppServerBindingStore;
appServer: CodexAppServerRuntimeOptions;
pluginConfig: CodexPluginConfig;
computerUseConfig: ResolvedCodexComputerUseConfig;
startupAuthProfileId: string | null | undefined;
startupAuthRequirement?: CodexAppServerClientOptions["authRequirement"];
startupAuthBindingFingerprint: string | undefined;
runtimeArtifactRequest?: Readonly<{
expected?: AgentHarnessRuntimeArtifactBinding;
}>;
startupPreparedAuth?: CodexAppServerClientOptions["preparedAuth"];
startupAuthAccountCacheKey: string | undefined;
startupEnvApiKeyCacheKey: string | undefined;
agentDir: string;
config: EmbeddedRunAttemptParams["config"] | undefined;
shellEnvironment?: Readonly<Record<string, string>>;
disableLoginShell?: boolean;
buildAttemptParams: () => EmbeddedRunAttemptParams;
runtimeModelId?: string;
sessionAgentId: string;
effectiveWorkspace: string;
effectiveCwd: string;
dynamicTools: CodexDynamicToolSpec[];
persistentWebSearchAllowed?: boolean;
webSearchAllowed: boolean;
developerInstructions: string | undefined;
agentWorkspaceDeveloperInstructions?: string;
finalConfigPatch?: Parameters<typeof startOrResumeThread>[0]["finalConfigPatch"];
buildFinalConfigPatch?: Parameters<typeof startOrResumeThread>[0]["buildFinalConfigPatch"];
nativeHookRelayGeneration?: string;
bundleMcpThreadConfig: CodexBundleMcpThreadConfig;
/** OpenClaw owns configured MCP dynamically for this scheduled turn. */
configuredMcpOwnershipVersion?: 1;
nativeToolSurfaceEnabled: boolean;
nativeProviderWebSearchSupport: CodexNativeWebSearchSupport;
sandboxExecServerEnabled: boolean;
sandbox: CodexSandboxContext;
contextEngineProjection: CodexContextEngineThreadBootstrapProjection | undefined;
startupTimeoutMs: number;
signal: AbortSignal;
onStartupTimeout: () => void | Promise<void>;
spawnedBy: EmbeddedRunAttemptParams["spawnedBy"];
}): Promise<StartCodexAttemptThreadResult> {
let pluginAppServer = params.appServer;
const startupRuntimeAuthProfileId =
params.startupPreparedAuth?.kind === "profile"
? params.startupPreparedAuth.profileId
: (params.startupAuthProfileId ?? undefined);
const startupRuntimeAuthProfileStore =
params.startupPreparedAuth?.kind === "profile" ? params.startupPreparedAuth.store : undefined;
let releaseSharedClientLease: (() => void) | undefined;
let startupClientForAbandonedRequestCleanup: CodexAppServerClient | undefined;
let releaseStartupResourcesOnTimeout: (() => Promise<void>) | undefined;
let startupAbandoned = false;
const startupAbandonController = new AbortController();
const abandonStartupAcquire = () => startupAbandonController.abort();
params.signal.addEventListener("abort", abandonStartupAcquire, { once: true });
try {
const startupResult = await withCodexStartupTimeout({
timeoutMs: params.startupTimeoutMs,
signal: params.signal,
onTimeout: async () => {
startupAbandoned = true;
startupAbandonController.abort();
await params.onStartupTimeout();
await releaseStartupResourcesOnTimeout?.();
releaseSharedClientLease?.();
releaseSharedClientLease = undefined;
await closeCodexStartupClientBestEffort(startupClientForAbandonedRequestCleanup);
startupClientForAbandonedRequestCleanup = undefined;
},
operation: async () => {
const threadConfig = mergeCodexThreadConfigs(
params.configuredMcpOwnershipVersion === 1
? undefined
: (params.bundleMcpThreadConfig?.configPatch as JsonObject | undefined),
);
const pluginStartupPolicy = resolveCodexPluginThreadConfigStartupPolicy({
pluginConfig: params.pluginConfig,
nativeToolSurfaceEnabled: params.nativeToolSurfaceEnabled,
scheduledRuntimeAuthority: params.buildAttemptParams().scheduledRuntimeAuthority,
});
const {
pluginThreadConfigRequired,
pluginThreadConfigPluginConfig,
resolvedPluginPolicy,
enabledPluginConfigKeys,
} = pluginStartupPolicy;
const computerUseMcpElicitationDelegationRequired = params.computerUseConfig.enabled;
const mcpElicitationDelegationRequired =
resolvedPluginPolicy?.enabled === true || computerUseMcpElicitationDelegationRequired;
pluginAppServer = mcpElicitationDelegationRequired
? {
...params.appServer,
approvalPolicy: withMcpElicitationsApprovalPolicy(params.appServer.approvalPolicy),
}
: params.appServer;
let attemptedClient: CodexAppServerClient | undefined;
const startupAttempt = async () => {
let startupClientLease: (() => void) | undefined;
let startupClient: CodexAppServerClient | undefined;
let startupAttemptError: unknown;
let startupAttemptSucceeded = false;
try {
const attemptParams = params.buildAttemptParams();
startupClient = await params.attemptClientFactory({
startOptions: params.appServer.start,
pluginConfig: params.pluginConfig,
...(params.startupPreparedAuth
? { preparedAuth: params.startupPreparedAuth }
: { authProfileId: params.startupAuthProfileId }),
authRequirement: params.startupAuthRequirement,
authProfileStore: attemptParams.authProfileStore,
authBindingFingerprint: params.startupAuthBindingFingerprint,
...(params.runtimeArtifactRequest
? {
runtimeArtifactMode: "capture" as const,
...(params.runtimeArtifactRequest.expected
? { expectedRuntimeArtifact: params.runtimeArtifactRequest.expected }
: {}),
}
: {}),
agentId: params.sessionAgentId,
agentDir: params.agentDir,
config: params.config,
onStartedClient: (client) => {
// Timeout cleanup may fire before the client factory resolves;
// close any late-arriving client instead of leaking a lease.
startupClientForAbandonedRequestCleanup = client;
if (startupAbandoned || startupAbandonController.signal.aborted) {
void closeCodexStartupClientBestEffort(client);
}
},
abandonSignal: startupAbandonController.signal,
timeoutMs: params.appServer.requestTimeoutMs,
});
const activeStartupClient = startupClient;
let startupClientLeaseReleased = false;
startupClientLease = () => {
if (startupClientLeaseReleased) {
return;
}
startupClientLeaseReleased = true;
releaseLeasedSharedCodexAppServerClient(activeStartupClient);
};
releaseSharedClientLease = startupClientLease;
attemptedClient = activeStartupClient;
startupClientForAbandonedRequestCleanup = activeStartupClient;
if (startupAbandoned) {
throw new CodexAppServerStartupError("timed_out");
}
if (startupAbandonController.signal.aborted) {
throw new CodexAppServerStartupError("aborted");
}
const runtimeArtifact = await verifyStartupArtifact({
client: activeStartupClient,
request: params.runtimeArtifactRequest,
signal: startupAbandonController.signal,
});
ensureCodexAppServerClientRuntime(activeStartupClient, {
agentDir: params.agentDir,
authProfileId: startupRuntimeAuthProfileId,
authMode:
params.startupPreparedAuth?.kind === "api-key" ? "prepared-api-key" : "profile",
authProfileStore: startupRuntimeAuthProfileStore ?? attemptParams.authProfileStore,
config: params.config,
});
const turnRouter = getCodexAppServerTurnRouter(activeStartupClient);
try {
await ensureCodexComputerUse({
client: activeStartupClient,
pluginConfig: params.pluginConfig,
config: params.config,
agentDir: params.agentDir,
timeoutMs: params.appServer.requestTimeoutMs,
signal: startupAbandonController.signal,
});
} catch (error) {
if (startupAbandonController.signal.aborted) {
throw error;
}
throw new AgentHarnessPreflightError(
`Codex Computer Use readiness failed: ${formatErrorMessage(error)}`,
{ cause: error, scope: "harness" },
);
}
const startupRuntimeIdentity = activeStartupClient.getRuntimeIdentity();
const pluginAppCacheKey = buildCodexPluginAppCacheKey({
appServer: params.appServer,
agentDir: params.agentDir,
authProfileId: startupRuntimeAuthProfileId,
accountId: params.startupAuthAccountCacheKey,
envApiKeyFingerprint: params.startupEnvApiKeyCacheKey,
appServerVersion: activeStartupClient.getServerVersion(),
runtimeIdentity: startupRuntimeIdentity,
});
const appServerRuntimeFingerprint = buildCodexAppServerRuntimeFingerprint({
appServer: params.appServer,
appServerVersion: activeStartupClient.getServerVersion(),
runtimeIdentity: startupRuntimeIdentity,
});
const basePluginThreadConfigInputFingerprint = pluginThreadConfigRequired
? buildCodexPluginThreadConfigInputFingerprint({
pluginConfig: pluginThreadConfigPluginConfig,
appCacheKey: pluginAppCacheKey,
})
: undefined;
const pluginThreadConfigInputFingerprint = basePluginThreadConfigInputFingerprint
? buildScheduledCodexAppAuthorityInputFingerprint(
basePluginThreadConfigInputFingerprint,
attemptParams.scheduledRuntimeAuthority,
)
: undefined;
embeddedAgentLog.debug(
"codex plugin thread config eligibility",
buildCodexPluginThreadConfigEligibilityLogData({
sessionId: attemptParams.sessionId,
sessionKey: attemptParams.sessionKey ?? "",
pluginThreadConfigRequired,
resolvedPluginPolicy,
enabledPluginConfigKeys,
pluginAppCacheKey,
startupAuthProfileId: startupRuntimeAuthProfileId,
appServer: params.appServer,
}),
);
let startupSandboxEnvironment: CodexSandboxExecEnvironment | undefined;
let startupSandboxEnvironmentAcquired = false;
const releaseStartupSandboxEnvironment = async () => {
if (startupSandboxEnvironmentAcquired) {
startupSandboxEnvironmentAcquired = false;
await releaseCodexSandboxExecServerEnvironment(params.sandbox);
}
};
releaseStartupResourcesOnTimeout = releaseStartupSandboxEnvironment;
try {
const sandboxEnvironmentRequired = shouldRequireCodexSandboxExecServerEnvironment({
sandbox: params.sandbox,
nativeToolSurfaceEnabled: params.nativeToolSurfaceEnabled,
sandboxExecServerEnabled: params.sandboxExecServerEnabled,
});
startupSandboxEnvironment = sandboxEnvironmentRequired
? await ensureCodexSandboxExecServerEnvironment({
client: activeStartupClient,
sandbox: params.sandbox ?? null,
appServerStartOptions: params.appServer.start,
timeoutMs: params.appServer.requestTimeoutMs,
signal: startupAbandonController.signal,
})
: undefined;
startupSandboxEnvironmentAcquired = Boolean(startupSandboxEnvironment);
if (startupAbandonController.signal.aborted) {
await releaseStartupSandboxEnvironment();
throw new CodexAppServerStartupError("aborted");
}
if (sandboxEnvironmentRequired && !startupSandboxEnvironment) {
throw new Error(
"Codex app-server did not register an OpenClaw sandbox exec-server environment.",
);
}
} catch (error) {
await releaseStartupSandboxEnvironment();
throw error;
}
const startupEnvironmentSelection = resolveCodexSandboxEnvironmentSelection(
startupSandboxEnvironment,
params.nativeToolSurfaceEnabled,
);
const startupExecutionCwd = resolveCodexAppServerExecutionCwd({
effectiveCwd: params.effectiveCwd,
localWorkspaceRoot: params.effectiveWorkspace,
environment: startupSandboxEnvironment,
nativeToolSurfaceEnabled: params.nativeToolSurfaceEnabled,
remoteWorkspaceRoot: params.appServer.remoteWorkspaceRoot,
});
const startupSandboxPolicy = startupSandboxEnvironment
? resolveCodexExternalSandboxPolicyForOpenClawSandbox(params.sandbox)
: undefined;
let startupReservation: CodexThreadRouteReservation | undefined;
const releaseStartupReservation = () => {
startupReservation?.release();
startupReservation = undefined;
};
const reserveStartupThread = (threadId: string) => {
if (startupReservation) {
if (startupReservation.threadId !== threadId) {
throw new Error(
`codex app-server reserved ${startupReservation.threadId} but started ${threadId}`,
);
}
return { release: releaseStartupReservation };
}
startupReservation = turnRouter.reserveThread({
threadId,
});
return { release: releaseStartupReservation };
};
const releaseStartupResources = async () => {
releaseStartupReservation();
await releaseStartupSandboxEnvironment();
};
releaseStartupResourcesOnTimeout = releaseStartupResources;
const buildThreadLifecycleParams = (
signal: AbortSignal,
reserveResumeThread?: typeof reserveStartupThread,
) =>
({
client: activeStartupClient,
reserveResumeThread,
bindingStore: params.bindingStore,
params: params.buildAttemptParams(),
runtimeModelId: params.runtimeModelId,
agentId: params.sessionAgentId,
agentDir: params.agentDir,
cwd: startupExecutionCwd,
dynamicTools: params.dynamicTools,
persistentWebSearchAllowed: params.persistentWebSearchAllowed,
webSearchAllowed: params.webSearchAllowed,
appServer: pluginAppServer,
developerInstructions: params.developerInstructions,
agentWorkspaceDeveloperInstructions: params.agentWorkspaceDeveloperInstructions,
config: threadConfig,
shellEnvironment: params.shellEnvironment,
disableLoginShell: params.disableLoginShell,
finalConfigPatch: params.finalConfigPatch,
buildFinalConfigPatch: params.buildFinalConfigPatch,
nativeHookRelayGeneration: params.nativeHookRelayGeneration,
nativeCodeModeEnabled: params.nativeToolSurfaceEnabled,
nativeProviderWebSearchSupport: params.nativeProviderWebSearchSupport,
nativeCodeModeOnlyEnabled: params.appServer.codeModeOnly,
userMcpServersEnabled:
params.configuredMcpOwnershipVersion === 1
? false
: params.nativeToolSurfaceEnabled,
mcpServersFingerprint:
params.configuredMcpOwnershipVersion === 1
? undefined
: params.bundleMcpThreadConfig.fingerprint,
mcpServersFingerprintEvaluated:
params.configuredMcpOwnershipVersion === 1 ||
params.bundleMcpThreadConfig.evaluated,
configuredMcpOwnershipVersion: params.configuredMcpOwnershipVersion,
environmentSelection: startupEnvironmentSelection,
appServerRuntimeFingerprint,
contextEngineProjection: params.contextEngineProjection,
signal,
pluginThreadConfig: pluginThreadConfigRequired
? createCodexPluginThreadConfigStartupProvider({
inputFingerprint: pluginThreadConfigInputFingerprint,
enabledPluginConfigKeys,
policy: resolvedPluginPolicy,
requestTimeoutMs: params.appServer.requestTimeoutMs,
signal,
pluginConfig: pluginThreadConfigPluginConfig,
client: activeStartupClient,
configCwd: startupExecutionCwd,
appCacheKey: pluginAppCacheKey,
scheduledRuntimeAuthority: attemptParams.scheduledRuntimeAuthority,
})
: undefined,
}) satisfies Parameters<typeof startOrResumeThread>[0];
try {
const startupThread = await startOrResumeThread(
buildThreadLifecycleParams(startupAbandonController.signal, reserveStartupThread),
);
try {
// Fresh starts reach here unreserved; resumes reserved before
// thread/resume so their early notifications are already buffered.
reserveStartupThread(startupThread.threadId);
} catch (error) {
const unsubscribed = await unsubscribeCodexThreadBestEffort(activeStartupClient, {
threadId: startupThread.threadId,
timeoutMs: CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS,
});
if (!unsubscribed) {
throw new CodexAppServerUnsafeSubscriptionError(
"Codex startup subscription cleanup failed",
{ cause: error },
);
}
throw error;
}
if (startupAbandonController.signal.aborted) {
throw new CodexAppServerStartupError("aborted");
}
const startupRoute = startupReservation;
if (!startupRoute) {
throw new Error("codex app-server startup did not reserve its thread route");
}
startupSandboxEnvironmentAcquired = false;
startCodexComputerUseHealthMonitor({
client: activeStartupClient,
config: params.computerUseConfig,
});
startupAttemptSucceeded = true;
return {
client: activeStartupClient,
turnRouter,
turnRoute: startupRoute,
thread: startupThread,
sandboxEnvironment: startupSandboxEnvironment,
environmentSelection: startupEnvironmentSelection,
executionCwd: startupExecutionCwd,
sandboxPolicy: startupSandboxPolicy,
...(runtimeArtifact ? { runtimeArtifact } : {}),
restartContextEngineCodexThread: async () => {
try {
return await startOrResumeThread(buildThreadLifecycleParams(params.signal));
} catch (error) {
if (!isCodexAppServerStartSelectionChangedError(error)) {
throw error;
}
// The run loop cannot safely swap the physical client, router,
// and lease halfway through an overflow retry. Retire this
// generation so the next bounded attempt acquires the owner
// selected by the now-current native config.
retireSharedCodexAppServerClientIfCurrent(activeStartupClient);
throw Object.assign(
new Error("codex app-server client is closed", { cause: error }),
{ code: CODEX_APP_SERVER_CONTEXT_RESTART_SELECTION_CHANGED },
);
}
},
};
} catch (error) {
await releaseStartupResources();
throw error;
} finally {
if (releaseStartupResourcesOnTimeout === releaseStartupResources) {
releaseStartupResourcesOnTimeout = undefined;
}
}
} catch (error) {
startupAttemptError = error;
if (!startupAbandoned && !params.signal.aborted && !startupClient) {
const sharedClient = clearSharedCodexAppServerClientIfCurrentAndUnclaimed(
startupClientForAbandonedRequestCleanup,
);
if (sharedClient.found && !sharedClient.closed) {
// Shared acquisition already released this caller. A peer still
// owns the client, so outer cleanup must not retire it.
startupClientForAbandonedRequestCleanup = undefined;
}
}
throw error;
} finally {
if (!startupAttemptSucceeded) {
if (releaseSharedClientLease === startupClientLease) {
releaseSharedClientLease = undefined;
}
startupClientLease?.();
if (startupAbandoned || params.signal.aborted) {
if (startupClientForAbandonedRequestCleanup === startupClient) {
startupClientForAbandonedRequestCleanup = undefined;
}
await closeCodexStartupClientBestEffort(startupClient);
} else if (
!isCodexAppServerStartSelectionChangedError(startupAttemptError) &&
(shouldClearSharedClientAfterStartupRace(startupAttemptError) ||
shouldClearSharedClientAfterStartupFailure({
error: startupAttemptError,
spawnedBy: params.spawnedBy,
}))
) {
if (startupClientForAbandonedRequestCleanup === startupClient) {
startupClientForAbandonedRequestCleanup = undefined;
}
await closeCodexStartupClientBestEffort(startupClient);
}
}
}
};
for (
let attempt = 1;
attempt <= CODEX_APP_SERVER_STARTUP_CONNECTION_CLOSE_MAX_ATTEMPTS;
attempt += 1
) {
try {
return await startupAttempt();
} catch (error) {
const selectionChanged = isCodexAppServerStartSelectionChangedError(error);
if (
startupAbandoned ||
params.signal.aborted ||
(!selectionChanged && !isCodexAppServerConnectionClosedError(error))
) {
throw error;
}
const failedClient = attemptedClient;
const refreshedSharedClient = selectionChanged
? retireSharedCodexAppServerClientIfCurrent(failedClient)
: clearSharedCodexAppServerClientIfCurrent(failedClient);
if (startupClientForAbandonedRequestCleanup === failedClient) {
startupClientForAbandonedRequestCleanup = undefined;
}
if (attempt >= CODEX_APP_SERVER_STARTUP_CONNECTION_CLOSE_MAX_ATTEMPTS) {
embeddedAgentLog.warn(
selectionChanged
? "codex app-server executable selection kept changing during startup; retries exhausted"
: "codex app-server connection closed during startup; retries exhausted",
{
attempt,
maxAttempts: CODEX_APP_SERVER_STARTUP_CONNECTION_CLOSE_MAX_ATTEMPTS,
refreshedSharedClient,
error: formatErrorMessage(error),
},
);
throw error;
}
embeddedAgentLog.warn(
selectionChanged
? "codex app-server executable selection changed during startup; restarting app-server and retrying"
: "codex app-server connection closed during startup; restarting app-server and retrying",
{
attempt,
nextAttempt: attempt + 1,
maxAttempts: CODEX_APP_SERVER_STARTUP_CONNECTION_CLOSE_MAX_ATTEMPTS,
refreshedSharedClient,
error: formatErrorMessage(error),
},
);
}
}
throw new Error("codex app-server startup retry loop exited unexpectedly");
},
});
startupClientForAbandonedRequestCleanup = undefined;
if (!releaseSharedClientLease) {
throw new Error("codex app-server startup succeeded without a shared client lease");
}
return {
...startupResult,
pluginAppServer,
releaseSharedClientLease,
};
} catch (error) {
if (params.signal.aborted || shouldClearSharedClientAfterStartupAbandon(error)) {
releaseSharedClientLease?.();
releaseSharedClientLease = undefined;
await closeCodexStartupClientBestEffort(startupClientForAbandonedRequestCleanup);
startupClientForAbandonedRequestCleanup = undefined;
} else if (
!isCodexAppServerStartSelectionChangedError(error) &&
(shouldClearSharedClientAfterStartupRace(error) ||
shouldClearSharedClientAfterStartupFailure({
error,
spawnedBy: params.spawnedBy,
}))
) {
releaseSharedClientLease?.();
releaseSharedClientLease = undefined;
await closeCodexStartupClientBestEffort(startupClientForAbandonedRequestCleanup);
startupClientForAbandonedRequestCleanup = undefined;
}
throw error;
} finally {
params.signal.removeEventListener("abort", abandonStartupAcquire);
}
}
function shouldClearSharedClientAfterStartupAbandon(error: unknown): boolean {
return isCodexAppServerStartupError(error);
}
function shouldClearSharedClientAfterStartupRace(error: unknown): boolean {
return (
shouldClearSharedClientAfterStartupAbandon(error) || isCodexAppServerRequestTimeoutError(error)
);
}
function shouldClearSharedClientAfterStartupFailure(params: {
error: unknown;
spawnedBy: EmbeddedRunAttemptParams["spawnedBy"];
}): boolean {
return isCodexAppServerBrokenPipeError(params.error) || !params.spawnedBy;
}