refactor(codex): split app server run attempt

This commit is contained in:
Peter Steinberger
2026-07-13 12:57:36 -07:00
parent 4319ddbe8c
commit 323a9fbe29
19 changed files with 4470 additions and 3715 deletions
@@ -0,0 +1,219 @@
import { setActiveEmbeddedRun } from "openclaw/plugin-sdk/agent-harness-runtime";
import {
interruptCodexTurnBestEffort,
retireCodexAppServerClientAfterTimedOutTurn,
} from "./attempt-client-cleanup.js";
import { isTerminalTurnStatus } from "./attempt-notifications.js";
import { createCodexSteeringQueue, type CodexSteeringQueueOptions } from "./attempt-steering.js";
import { CodexAppServerEventProjector } from "./event-projector.js";
import type { CodexTurnStartResponse, JsonObject } from "./protocol.js";
import { readRecentCodexRateLimits } from "./rate-limit-cache.js";
import type { CodexAttemptLifecycleController } from "./run-attempt-lifecycle-controller.js";
import type { CodexAttemptNotificationController } from "./run-attempt-notification-controller.js";
import type { CodexAttemptResources } from "./run-attempt-resources.js";
import type { CodexAttemptTurnState } from "./run-attempt-turn-state.js";
import {
createCodexAppServerUserMessagePersistenceNotifier,
mirrorPromptAtTurnStartBestEffort,
} from "./transcript-mirror.js";
import { createCodexUserInputBridge } from "./user-input-bridge.js";
export async function activateCodexAttemptTurn(
resources: CodexAttemptResources,
turnRuntime: CodexAttemptTurnState,
lifecycle: CodexAttemptLifecycleController,
notifications: CodexAttemptNotificationController,
turn: CodexTurnStartResponse,
) {
const {
prompt,
state: resourceState,
projectorRef,
trajectoryRecorder,
pendingNativePreToolUseFailures,
} = resources;
const { context, turnState } = prompt;
const { runtime, attemptTools } = context;
const { connection } = runtime;
const {
params,
runAbortController,
terminalState,
abortExplicitly,
abortFromUpstream,
bindingStore,
bindingIdentity,
sessionAgentId,
sandboxSessionKey,
effectiveCwd,
} = connection;
const { dynamicToolParams, computerContextEpoch } = attemptTools;
const { state, userInputBridgeRef, steeringQueueRef, turnWatches } = turnRuntime;
const { emitExecutionPhaseOnce, emitLifecycleStart, maybeAnnounceFastModeAutoOff } = lifecycle;
const { enqueueNotification } = notifications;
const activeTurnId = turn.turn.id;
const streamState = { eventEmitted: false, needsTerminalSnapshot: false };
emitExecutionPhaseOnce("turn_accepted", { phase: "turn_accepted" });
userInputBridgeRef.current = createCodexUserInputBridge({
paramsForRun: params,
threadId: resourceState.thread.threadId,
turnId: activeTurnId,
signal: runAbortController.signal,
});
trajectoryRecorder?.recordEvent("prompt.submitted", {
threadId: resourceState.thread.threadId,
turnId: activeTurnId,
prompt: turnState.codexTurnPromptText,
imagesCount: params.images?.length ?? 0,
});
projectorRef.current = new CodexAppServerEventProjector(
{
...dynamicToolParams,
onAgentEvent: (event) => {
if (event.stream === "assistant" && typeof event.data.delta === "string") {
streamState.eventEmitted = true;
streamState.needsTerminalSnapshot ||= event.data.replaceable === true;
}
return dynamicToolParams.onAgentEvent?.(event);
},
},
resourceState.thread.threadId,
activeTurnId,
{
nativePostToolUseRelayEnabled:
resourceState.nativeHookRelay?.allowedEvents.includes("post_tool_use") === true &&
resourceState.nativeHookRelay.shouldRelayEvent("post_tool_use"),
readRecentRateLimits: () => readRecentCodexRateLimits(resourceState.client),
runAbortSignal: runAbortController.signal,
trajectoryRecorder,
onNativeToolResultRecorded: maybeAnnounceFastModeAutoOff,
onContextCompacted: () => {
computerContextEpoch.value += 1;
delete computerContextEpoch.frameToolCallId;
delete computerContextEpoch.frameImageIdentity;
},
},
);
if (isTerminalTurnStatus(turn.turn.status)) {
state.terminalTurnNotificationQueued = true;
}
emitLifecycleStart();
const activeProjector = projectorRef.current;
turnWatches.armTerminalIdleWatch();
turnWatches.touchActivity("turn:start", { arm: true });
turnWatches.armAttemptIdleWatch();
turnWatches.touchActivity("turn:start", { attemptProgress: true });
for (const failure of pendingNativePreToolUseFailures.splice(0)) {
activeProjector.recordNativeToolPreToolUseFailure(failure);
}
// The route buffers early events. Publish full turn context, then release in wire order.
if (resourceState.turnRoute) {
try {
await resourceState.turnRoute.bindTurn(activeTurnId);
} catch (error) {
if (!state.terminalTurnNotificationQueued) {
throw error;
}
await resourceState.turnRoute.drain();
if (!state.completed) {
turnWatches.clearAllTimers();
throw error;
}
}
}
if (!state.completed && isTerminalTurnStatus(turn.turn.status)) {
await enqueueNotification(
{
method: "turn/completed",
params: {
threadId: resourceState.thread.threadId,
turnId: activeTurnId,
turn: turn.turn as unknown as JsonObject,
},
},
{ threadId: resourceState.thread.threadId, turnId: activeTurnId },
);
}
const activeSteeringQueue = createCodexSteeringQueue({
client: resourceState.client,
threadId: resourceState.thread.threadId,
turnId: activeTurnId,
answerPendingUserInput: (text) =>
userInputBridgeRef.current?.handleQueuedMessage(text) ?? false,
signal: runAbortController.signal,
});
steeringQueueRef.current = activeSteeringQueue;
const handle = {
kind: "embedded" as const,
runId: params.runId,
queueMessage: async (text: string, optionsLocal?: CodexSteeringQueueOptions) =>
activeSteeringQueue.queue(text, optionsLocal),
isStreaming: () => !state.completed && !runAbortController.signal.aborted,
isStopped: () => state.completed || state.timedOut || runAbortController.signal.aborted,
isAbortable: () =>
!terminalState.terminalOutcomeFrozen || terminalState.sharedAbortAllowedAfterTerminalOutcome,
isCompacting: () => projectorRef.current?.isCompacting() ?? false,
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
cancel: () => abortExplicitly("cancelled"),
abort: () => abortExplicitly("aborted"),
};
params.replyOperation?.attachBackend(handle);
setActiveEmbeddedRun(params.sessionId, handle, params.sessionKey, params.sessionFile);
const freezeRunTerminalOutcome = () => {
if (terminalState.terminalOutcomeFrozen) {
return;
}
terminalState.terminalOutcomeFrozen = true;
params.abortSignal?.removeEventListener("abort", abortFromUpstream);
};
const notifyUserMessagePersisted = createCodexAppServerUserMessagePersistenceNotifier(params);
void mirrorPromptAtTurnStartBestEffort({
params,
agentId: sessionAgentId,
notifyUserMessagePersisted,
sessionKey: sandboxSessionKey,
cwd: effectiveCwd,
threadId: resourceState.thread.threadId,
turnId: activeTurnId,
});
const abortListener = () => {
if (state.timedOut) {
void (async () => {
// Supervised sessions stay native; clearing scope would silently move the next attempt.
if (resourceState.thread.connectionScope !== "supervision") {
await bindingStore.mutate(bindingIdentity, {
kind: "clear",
threadId: resourceState.thread.threadId,
});
}
await retireCodexAppServerClientAfterTimedOutTurn(resourceState.client, {
threadId: resourceState.thread.threadId,
turnId: activeTurnId,
reason: String(runAbortController.signal.reason ?? "timeout"),
suspectPhysicalClient: state.turnWatchTimeoutKind === "terminal",
});
})().finally(() => state.resolveCompletion?.());
return;
}
interruptCodexTurnBestEffort(resourceState.client, {
threadId: resourceState.thread.threadId,
turnId: activeTurnId,
});
state.resolveCompletion?.();
};
runAbortController.signal.addEventListener("abort", abortListener, { once: true });
if (runAbortController.signal.aborted) {
abortListener();
}
return {
activeTurnId,
activeProjector,
streamState,
handle,
freezeRunTerminalOutcome,
notifyUserMessagePersisted,
abortListener,
};
}
export type CodexAttemptActiveTurn = Awaited<ReturnType<typeof activateCodexAttemptTurn>>;
@@ -0,0 +1,105 @@
import {
clearActiveEmbeddedRun,
embeddedAgentLog,
runAgentCleanupStep,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import {
CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS,
unsubscribeCodexThreadBestEffort,
} from "./attempt-client-cleanup.js";
import { scheduleCodexNativeHookRelayUnregister } from "./native-hook-relay.js";
import type { CodexAttemptActiveTurn } from "./run-attempt-active-turn.js";
import type { CodexAttemptLifecycleController } from "./run-attempt-lifecycle-controller.js";
import type { CodexAttemptResources } from "./run-attempt-resources.js";
import type { prepareCodexAttemptTurnRequest } from "./run-attempt-turn-request.js";
import type { CodexAttemptTurnState } from "./run-attempt-turn-state.js";
export async function cleanupCodexAttempt(
resources: CodexAttemptResources,
turnRuntime: CodexAttemptTurnState,
lifecycle: CodexAttemptLifecycleController,
requestRuntime: Awaited<ReturnType<typeof prepareCodexAttemptTurnRequest>>,
activeTurn: CodexAttemptActiveTurn,
) {
const {
prompt,
state: resourceState,
trajectoryRecorder,
releaseCurrentRoute,
releaseSharedClientLeaseAndRetireOneShotClient,
releaseSandboxExecEnvironment,
} = resources;
const { connection } = prompt.context.runtime;
const { params, options, runAbortController } = connection;
const { state, steeringQueueRef, userInputBridgeRef, turnWatches } = turnRuntime;
const {
maybeEmitFastModeAutoResetBestEffort,
emitLifecycleTerminal,
buildLifecycleTerminalMeta,
} = lifecycle;
const { codexModelCallDiagnostics } = requestRuntime;
const { activeTurnId, abortListener, handle, freezeRunTerminalOutcome } = activeTurn;
if (params.isFinalFallbackAttempt !== false) {
await maybeEmitFastModeAutoResetBestEffort();
}
codexModelCallDiagnostics.emitError(
"codex app-server run completed without model-call terminal event",
);
emitLifecycleTerminal({
phase: "error",
error: "codex app-server run completed without lifecycle terminal event",
...buildLifecycleTerminalMeta({
aborted: runAbortController.signal.aborted && !state.clientClosedAbort,
timedOut: state.timedOut,
}),
});
if (trajectoryRecorder && !resourceState.trajectoryEndRecorded) {
trajectoryRecorder.recordEvent("session.ended", {
status:
state.timedOut || (runAbortController.signal.aborted && !state.clientClosedAbort)
? "interrupted"
: "cleanup",
threadId: resourceState.thread.threadId,
turnId: activeTurnId,
timedOut: state.timedOut,
aborted: runAbortController.signal.aborted && !state.clientClosedAbort,
});
}
await runAgentCleanupStep({
runId: params.runId,
sessionId: params.sessionId,
step: "codex-trajectory-flush",
log: embeddedAgentLog,
cleanup: async () => trajectoryRecorder?.flush(),
});
if (!state.timedOut && !runAbortController.signal.aborted) {
await steeringQueueRef.current?.flushPending();
}
if (!state.timedOut) {
await unsubscribeCodexThreadBestEffort(resourceState.client, {
threadId: resourceState.thread.threadId,
timeoutMs: CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS,
});
}
userInputBridgeRef.current?.cancelPending();
turnWatches.clearAllTimers();
releaseCurrentRoute();
await releaseSharedClientLeaseAndRetireOneShotClient();
if (resourceState.nativeHookRelay) {
if (state.shouldDelayNativeHookRelayUnregister) {
// Native hook subprocesses can finish shortly after turn completion.
scheduleCodexNativeHookRelayUnregister({
relay: resourceState.nativeHookRelay,
hookTimeoutSec: options.nativeHookRelay?.hookTimeoutSec,
});
} else {
resourceState.nativeHookRelay.unregister();
}
}
await releaseSandboxExecEnvironment();
runAbortController.signal.removeEventListener("abort", abortListener);
steeringQueueRef.current?.cancel();
freezeRunTerminalOutcome();
params.replyOperation?.detachBackend(handle);
clearActiveEmbeddedRun(params.sessionId, handle, params.sessionKey, params.sessionFile);
}
@@ -0,0 +1,386 @@
import {
embeddedAgentLog,
getBeforeToolCallPolicyDiagnosticState,
isActiveHarnessContextEngine,
resolveSandboxContext,
resolveSessionAgentIds,
resolveUserPath,
type FastModeAutoProgressState,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { resolveAgentDir } from "openclaw/plugin-sdk/agent-runtime";
import {
createDiagnosticTraceContextFromActiveScope,
freezeDiagnosticTraceContext,
resolveDiagnosticModelContentCapturePolicy,
} from "openclaw/plugin-sdk/diagnostic-runtime";
import { loadExecApprovals } from "openclaw/plugin-sdk/exec-approvals-runtime";
import {
resolveCodexAppServerForModelProvider,
resolveCodexAppServerForOpenClawToolPolicy,
} from "./app-server-policy.js";
import {
resolveCodexAppServerAuthProfileId,
resolveCodexAppServerAuthProfileIdForAgent,
resolveCodexAppServerPreparedAuthHandoff,
} from "./auth-bridge.js";
import { resolveCodexBindingAppServerConnection } from "./binding-connection.js";
import {
isCodexAppServerApprovalPolicyAllowedByRequirements,
readCodexPluginConfig,
resolveCodexComputerUseConfig,
resolveCodexModelBackedReviewerPolicyContext,
resolveOpenClawExecPolicyForCodexAppServer,
} from "./config.js";
import { createCodexDynamicToolBuildStageTracker } from "./dynamic-tool-build.js";
import { resolveCodexNativeHookRelayEvents } from "./native-hook-relay.js";
import { isCodexAppServerProfilerEnabled } from "./profiler-flag.js";
import { ensureCodexWorkspaceDirOnce } from "./run-attempt-lifecycle.js";
import type { CodexRunAttemptInput } from "./run-attempt-types.js";
import {
reclaimCurrentCodexSessionGeneration,
sessionBindingIdentity,
type CodexAppServerThreadBinding,
} from "./session-binding.js";
import { getLeasedSharedCodexAppServerClient } from "./shared-client.js";
import { rotateOversizedCodexAppServerStartupBinding } from "./startup-binding.js";
export async function prepareCodexAttemptConnection({ params, options }: CodexRunAttemptInput) {
const attemptStartedAt = Date.now();
const profilerEnabled = isCodexAppServerProfilerEnabled(params.config);
const codexModelCallTrace = freezeDiagnosticTraceContext(
createDiagnosticTraceContextFromActiveScope(),
);
const codexModelContentCapture = resolveDiagnosticModelContentCapturePolicy(params.config);
const codexModelCallId = `${params.runId}:codex-model:1`;
const fastModeAutoStartedAtMs =
typeof params.fastModeStartedAtMs === "number" && Number.isFinite(params.fastModeStartedAtMs)
? params.fastModeStartedAtMs
: undefined;
const fastModeAutoProgressState: FastModeAutoProgressState = params.fastModeAutoProgressState ?? {
offAnnounced: false,
resetAnnounced: false,
};
const preDynamicStartupStages = createCodexDynamicToolBuildStageTracker({
enabled: profilerEnabled,
});
const attemptClientFactory = options.clientFactory ?? getLeasedSharedCodexAppServerClient;
const runtimeArtifactRequest =
params.captureRuntimeArtifact || params.expectedRuntimeArtifact
? params.expectedRuntimeArtifact
? { expected: params.expectedRuntimeArtifact }
: {}
: undefined;
const pluginConfig = readCodexPluginConfig(options.pluginConfig);
const computerUseConfig = resolveCodexComputerUseConfig({ pluginConfig });
const { sessionAgentId } = resolveSessionAgentIds({
sessionKey: params.sessionKey,
config: params.config,
agentId: params.agentId,
});
const beforeToolCallPolicy = getBeforeToolCallPolicyDiagnosticState();
preDynamicStartupStages.mark("config");
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
await ensureCodexWorkspaceDirOnce(resolvedWorkspace);
preDynamicStartupStages.mark("workspace");
const sandboxSessionKey =
params.sandboxSessionKey?.trim() || params.sessionKey?.trim() || params.sessionId;
const contextSessionKey = params.sessionKey?.trim() || sandboxSessionKey;
const sandbox = await resolveSandboxContext({
config: params.config,
sessionKey: sandboxSessionKey,
workspaceDir: resolvedWorkspace,
});
preDynamicStartupStages.mark("sandbox");
const execPolicy = resolveOpenClawExecPolicyForCodexAppServer({
execOverrides: params.execOverrides,
approvals: loadExecApprovals(),
config: params.config,
agentId: sessionAgentId,
});
const agentDir = params.agentDir ?? resolveAgentDir(params.config ?? {}, sessionAgentId);
const bindingIdentity = sessionBindingIdentity({
sessionId: params.sessionId,
sessionKey: params.sessionKey,
agentId: params.agentId,
config: params.config,
});
const bindingStore = options.bindingStore;
preDynamicStartupStages.mark("session-agent");
let activeContextEngine = isActiveHarnessContextEngine(params.contextEngine)
? params.contextEngine
: undefined;
const isInactiveThreadBootstrapBinding = (binding: CodexAppServerThreadBinding | undefined) =>
!activeContextEngine && binding?.contextEngine?.projection?.mode === "thread_bootstrap";
let startupBinding = await bindingStore.read(bindingIdentity);
if (!startupBinding && bindingIdentity.kind === "session" && bindingIdentity.sessionKey) {
const reclaimed = await reclaimCurrentCodexSessionGeneration({
bindingStore,
identity: bindingIdentity,
config: params.config,
});
if (!reclaimed) {
throw new Error(
`Codex session generation is no longer current: ${bindingIdentity.sessionId}`,
);
}
startupBinding = await bindingStore.read(bindingIdentity);
}
preDynamicStartupStages.mark("read-binding");
const usesSupervisionConnection = startupBinding?.connectionScope === "supervision";
if (usesSupervisionConnection) {
activeContextEngine = undefined;
}
if (usesSupervisionConnection && pluginConfig.supervision?.enabled !== true) {
throw new Error(
"Codex supervision is disabled; refusing to open a native user-home supervised session",
);
}
const resolveRuntimeOptionsForBinding = (selection: { modelProvider?: string; model?: string }) =>
resolveCodexBindingAppServerConnection({
binding: startupBinding,
pluginConfig,
execPolicy,
modelProvider: selection.modelProvider,
model: selection.model,
config: params.config,
agentDir,
openClawSandboxActive: sandbox?.enabled === true,
}).appServer;
const initialStartupBindingHadInactiveThreadBootstrap =
isInactiveThreadBootstrapBinding(startupBinding);
const preparedAuthRoute = usesSupervisionConnection
? undefined
: params.runtimePlan?.auth.modelRoute;
const startupAuthProfileCandidate = usesSupervisionConnection
? undefined
: preparedAuthRoute
? params.runtimePlan?.auth.forwardedAuthProfileId
: (params.runtimePlan?.auth.forwardedAuthProfileId ??
params.authProfileId ??
startupBinding?.authProfileId);
const resolvedStartupAuthProfileId = usesSupervisionConnection
? undefined
: preparedAuthRoute
? startupAuthProfileCandidate
: params.authProfileStore
? resolveCodexAppServerAuthProfileId({
authProfileId: startupAuthProfileCandidate,
store: params.authProfileStore,
config: params.config,
})
: resolveCodexAppServerAuthProfileIdForAgent({
authProfileId: startupAuthProfileCandidate,
agentDir,
config: params.config,
});
const authHandoff = usesSupervisionConnection
? { authProfileId: undefined, nativeAuthProfile: true, preparedAuth: undefined }
: await resolveCodexAppServerPreparedAuthHandoff({
authRequirement: preparedAuthRoute?.authRequirement,
resolvedApiKey: params.resolvedApiKey,
authProfileId: resolvedStartupAuthProfileId,
authProfileStore: params.authProfileStore,
agentDir,
config: params.config,
subscriptionProfileRequiredError:
"Prepared Codex subscription route requires a forwarded OpenAI OAuth or token profile.",
subscriptionProfileUnusableError: "Prepared Codex subscription auth profile is unusable.",
});
const {
authProfileId: startupAuthProfileId,
nativeAuthProfile,
preparedAuth: startupPreparedAuth,
} = authHandoff;
const startupClientAuthProfileId =
usesSupervisionConnection || startupPreparedAuth?.kind === "api-key"
? null
: startupAuthProfileId;
const resolveReviewerPolicyContext = (binding: CodexAppServerThreadBinding | undefined) => {
const nativeModelOwned = binding?.preserveNativeModel === true;
return resolveCodexModelBackedReviewerPolicyContext({
provider: nativeModelOwned ? "codex" : params.provider,
model: nativeModelOwned ? binding.model : params.modelId,
bindingModelProvider: binding?.modelProvider,
bindingModel: binding?.model,
nativeAuthProfile,
});
};
let reviewerPolicyContext = resolveReviewerPolicyContext(startupBinding);
preDynamicStartupStages.mark("auth-profile");
let configuredAppServer = resolveRuntimeOptionsForBinding({
modelProvider: reviewerPolicyContext.modelProvider,
model: reviewerPolicyContext.model,
});
const effectiveWorkspace = sandbox?.enabled
? sandbox.workspaceAccess === "rw"
? resolvedWorkspace
: sandbox.workspaceDir
: resolvedWorkspace;
const requestedCwd = params.cwd ? resolveUserPath(params.cwd) : undefined;
if (sandbox?.enabled && requestedCwd && requestedCwd !== resolvedWorkspace) {
throw new Error(
"cwd override is not supported for sandboxed Codex app-server runs; omit cwd or use the agent workspace as cwd",
);
}
const effectiveCwd = sandbox?.enabled ? effectiveWorkspace : (requestedCwd ?? effectiveWorkspace);
await ensureCodexWorkspaceDirOnce(effectiveWorkspace);
preDynamicStartupStages.mark("effective-workspace");
const resolvePolicyAppServer = () =>
resolveCodexAppServerForOpenClawToolPolicy({
appServer: configuredAppServer,
pluginConfig,
env: process.env,
shouldPromote:
beforeToolCallPolicy.hasBeforeToolCallHook ||
beforeToolCallPolicy.trustedToolPolicies.length > 0,
execPolicy,
canUseUntrustedApprovalPolicy:
configuredAppServer.start.transport !== "stdio" ||
isCodexAppServerApprovalPolicyAllowedByRequirements("untrusted"),
});
let policyAppServer = resolvePolicyAppServer();
let appServer = resolveCodexAppServerForModelProvider({
appServer: policyAppServer,
provider: reviewerPolicyContext.modelProvider,
model: reviewerPolicyContext.model,
config: params.config,
env: process.env,
agentDir,
});
if (configuredAppServer.approvalPolicy === "never" && appServer.approvalPolicy === "untrusted") {
embeddedAgentLog.info("codex app-server approval policy promoted for OpenClaw tool policy", {
from: "never",
to: "untrusted",
beforeToolCallHook: beforeToolCallPolicy.hasBeforeToolCallHook,
trustedToolPolicies: beforeToolCallPolicy.trustedToolPolicies,
});
}
preDynamicStartupStages.mark("app-server-policy");
preDynamicStartupStages.mark("native-hook-relay");
const terminalState = {
explicitCancellationObserved: false,
explicitCancellationReason: undefined as unknown,
terminalOutcomeFrozen: false,
sharedAbortAllowedAfterTerminalOutcome: false,
};
const runAbortController = new AbortController();
let attemptAbortNotified = false;
const notifyAttemptAbort = () => {
if (attemptAbortNotified) {
return;
}
attemptAbortNotified = true;
params.onAttemptAbort?.();
};
const abortExplicitly = (reason: unknown) => {
if (terminalState.terminalOutcomeFrozen) {
if (terminalState.sharedAbortAllowedAfterTerminalOutcome) {
notifyAttemptAbort();
}
return;
}
notifyAttemptAbort();
terminalState.explicitCancellationObserved = true;
terminalState.explicitCancellationReason ??= reason;
runAbortController.abort(reason);
};
const abortFromUpstream = () => {
abortExplicitly(params.abortSignal?.reason ?? "upstream_abort");
};
if (params.abortSignal?.aborted) {
abortFromUpstream();
} else {
params.abortSignal?.addEventListener("abort", abortFromUpstream, { once: true });
}
startupBinding = await rotateOversizedCodexAppServerStartupBinding({
binding: startupBinding,
bindingStore,
identity: bindingIdentity,
sessionFile: params.sessionFile,
agentDir,
codexHome: appServer.start.env?.CODEX_HOME,
config: params.config,
contextEngineActive: Boolean(activeContextEngine),
});
const initialInactiveThreadBootstrapBindingForcedFreshStart =
initialStartupBindingHadInactiveThreadBootstrap && !startupBinding?.threadId;
preDynamicStartupStages.mark("rotate-binding");
reviewerPolicyContext = resolveReviewerPolicyContext(startupBinding);
configuredAppServer = resolveRuntimeOptionsForBinding({
modelProvider: reviewerPolicyContext.modelProvider,
model: reviewerPolicyContext.model,
});
policyAppServer = resolvePolicyAppServer();
appServer = resolveCodexAppServerForModelProvider({
appServer: policyAppServer,
provider: reviewerPolicyContext.modelProvider,
model: reviewerPolicyContext.model,
config: params.config,
env: process.env,
agentDir,
});
const nativeHookRelayEvents = resolveCodexNativeHookRelayEvents({
configuredEvents: options.nativeHookRelay?.events,
appServer,
});
const mutable = { startupBinding, pluginAppServer: appServer };
const resolveRuntimeOptionsForCurrentBinding = (selection: {
modelProvider?: string;
model?: string;
}) =>
resolveCodexBindingAppServerConnection({
binding: mutable.startupBinding,
pluginConfig,
execPolicy,
modelProvider: selection.modelProvider,
model: selection.model,
config: params.config,
agentDir,
openClawSandboxActive: sandbox?.enabled === true,
}).appServer;
return {
params,
options,
attemptStartedAt,
profilerEnabled,
codexModelCallTrace,
codexModelContentCapture,
codexModelCallId,
fastModeAutoStartedAtMs,
fastModeAutoProgressState,
preDynamicStartupStages,
attemptClientFactory,
runtimeArtifactRequest,
pluginConfig,
computerUseConfig,
sessionAgentId,
resolvedWorkspace,
sandboxSessionKey,
contextSessionKey,
sandbox,
agentDir,
bindingIdentity,
bindingStore,
activeContextEngine,
isInactiveThreadBootstrapBinding,
usesSupervisionConnection,
startupAuthProfileId,
startupPreparedAuth,
startupClientAuthProfileId,
effectiveWorkspace,
effectiveCwd,
appServer,
nativeHookRelayEvents,
runAbortController,
terminalState,
abortExplicitly,
abortFromUpstream,
resolveReviewerPolicyContext,
resolveRuntimeOptionsForCurrentBinding,
mutable,
initialStartupBindingHadInactiveThreadBootstrap,
initialInactiveThreadBootstrapBindingForcedFreshStart,
};
}
export type CodexAttemptConnection = Awaited<ReturnType<typeof prepareCodexAttemptConnection>>;
@@ -0,0 +1,188 @@
import {
bootstrapHarnessContextEngine,
buildHarnessContextEngineRuntimeContext,
CODEX_APP_SERVER_CONTEXT_ENGINE_HOST,
embeddedAgentLog,
getAgentHarnessHookRunner,
resolveContextEngineOwnerPluginId,
runHarnessContextEngineMaintenance,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import {
buildCodexOpenClawPromptContext,
buildCodexWorkspaceBootstrapContext,
getCodexWorkspaceMemoryToolNames,
readMirroredSessionHistoryMessages,
renderCodexSkillsCollaborationInstructions,
} from "./attempt-context.js";
import {
resolveCodexContextEngineProjectionMaxChars,
resolveCodexContextEngineProjectionReserveTokens,
type CodexProjectedContextRange,
} from "./context-engine-projection.js";
import type { CodexAttemptRuntime } from "./run-attempt-runtime.js";
import { joinPresentSections } from "./run-attempt-state.js";
import type { CodexAttemptTools } from "./run-attempt-tool-setup.js";
import {
buildDeveloperInstructions,
type CodexContextEngineThreadBootstrapProjection,
} from "./thread-lifecycle.js";
export async function prepareCodexAttemptContext(
runtime: CodexAttemptRuntime,
attemptTools: CodexAttemptTools,
) {
const {
connection,
runtimeParams,
activeSessionId,
activeSessionFile,
buildActiveRunAttemptParams,
effectiveContextWindowInfo,
effectiveContextTokenBudget,
effectiveRuntimeProviderId,
effectiveRuntimeModelId,
hookChannelId,
} = runtime;
const {
params,
sessionAgentId,
contextSessionKey,
activeContextEngine,
initialStartupBindingHadInactiveThreadBootstrap,
sandboxSessionKey,
effectiveWorkspace,
effectiveCwd,
agentDir,
usesSupervisionConnection,
resolvedWorkspace,
initialInactiveThreadBootstrapBindingForcedFreshStart,
} = connection;
const { toolBridge } = attemptTools;
const activeTranscriptTarget = {
agentId: sessionAgentId,
sessionFile: activeSessionFile,
sessionId: activeSessionId,
sessionKey: contextSessionKey,
};
const historyState = {
messages:
!activeContextEngine && initialStartupBindingHadInactiveThreadBootstrap
? []
: ((await readMirroredSessionHistoryMessages(activeTranscriptTarget)) ?? []),
};
const hadSessionTranscriptState = historyState.messages.length > 0;
const hookContextWindowFields = {
...(effectiveContextWindowInfo?.tokens
? { contextTokenBudget: effectiveContextWindowInfo.tokens }
: effectiveContextTokenBudget
? { contextTokenBudget: effectiveContextTokenBudget }
: {}),
...(effectiveContextWindowInfo?.source
? { contextWindowSource: effectiveContextWindowInfo.source }
: {}),
...(effectiveContextWindowInfo?.referenceTokens
? { contextWindowReferenceTokens: effectiveContextWindowInfo.referenceTokens }
: {}),
};
const hookContext = {
runId: params.runId,
agentId: sessionAgentId,
sessionKey: sandboxSessionKey,
sessionId: params.sessionId,
workspaceDir: params.workspaceDir,
messageProvider: params.messageProvider ?? undefined,
trigger: params.trigger,
channelId: hookChannelId,
...hookContextWindowFields,
};
const hookRunner = getAgentHarnessHookRunner();
const activeContextEnginePluginId = activeContextEngine
? resolveContextEngineOwnerPluginId(activeContextEngine)
: undefined;
const buildActiveContextEngineRuntimeContext = () =>
buildHarnessContextEngineRuntimeContext({
attempt: buildActiveRunAttemptParams(),
workspaceDir: effectiveWorkspace,
cwd: effectiveCwd,
agentDir,
activeAgentId: sessionAgentId,
contextEnginePluginId: activeContextEnginePluginId,
tokenBudget: effectiveContextTokenBudget,
});
if (activeContextEngine) {
await bootstrapHarnessContextEngine({
hadSessionFile: hadSessionTranscriptState,
contextEngine: activeContextEngine,
sessionId: activeSessionId,
sessionKey: contextSessionKey,
sessionFile: activeSessionFile,
sessionTarget: params.sessionTarget,
runtimeContext: buildActiveContextEngineRuntimeContext(),
contextEngineHostSupport: CODEX_APP_SERVER_CONTEXT_ENGINE_HOST,
providerId: effectiveRuntimeProviderId,
requestedModelId: usesSupervisionConnection ? undefined : params.requestedModelId,
modelId: effectiveRuntimeModelId,
fallbackReason: usesSupervisionConnection ? undefined : params.fallbackReason,
degradedReason: usesSupervisionConnection ? undefined : params.degradedReason,
runMaintenance: runHarnessContextEngineMaintenance,
config: params.config,
warn: (message) => embeddedAgentLog.warn(message),
});
historyState.messages =
(await readMirroredSessionHistoryMessages(activeTranscriptTarget)) ?? historyState.messages;
}
const memoryToolNames = getCodexWorkspaceMemoryToolNames(toolBridge.availableSpecs);
const workspaceBootstrapContext = await buildCodexWorkspaceBootstrapContext({
params: runtimeParams,
resolvedWorkspace,
effectiveWorkspace,
sessionKey: contextSessionKey,
sessionAgentId,
memoryToolNames,
});
const baseDeveloperInstructions = joinPresentSections(
buildDeveloperInstructions(runtimeParams, { dynamicTools: toolBridge.availableSpecs }),
workspaceBootstrapContext.developerInstructions,
);
const openClawPromptContext = buildCodexOpenClawPromptContext({
params: runtimeParams,
workspacePromptContext: workspaceBootstrapContext.promptContext,
});
const skillsCollaborationInstructions = renderCodexSkillsCollaborationInstructions({
attempt: runtimeParams,
skillsPrompt: params.skillsSnapshot?.prompt,
});
const promptState = {
promptText: params.prompt,
promptContextRange: undefined as CodexProjectedContextRange | undefined,
developerInstructions: baseDeveloperInstructions,
prePromptMessageCount: historyState.messages.length,
contextEngineProjection: undefined as CodexContextEngineThreadBootstrapProjection | undefined,
precomputedStaleBindingContinuityProjectionApplied: false,
staleBindingContinuityForcedFreshStart: false,
inactiveThreadBootstrapBindingForcedFreshStart:
initialInactiveThreadBootstrapBindingForcedFreshStart,
};
const codexContextProjectionMaxChars = resolveCodexContextEngineProjectionMaxChars({
contextTokenBudget: effectiveContextTokenBudget,
reserveTokens: resolveCodexContextEngineProjectionReserveTokens({ config: params.config }),
});
return {
runtime,
attemptTools,
activeTranscriptTarget,
historyState,
hookContext,
hookContextWindowFields,
hookRunner,
buildActiveContextEngineRuntimeContext,
workspaceBootstrapContext,
baseDeveloperInstructions,
openClawPromptContext,
skillsCollaborationInstructions,
promptState,
codexContextProjectionMaxChars,
};
}
export type CodexAttemptContext = Awaited<ReturnType<typeof prepareCodexAttemptContext>>;
@@ -0,0 +1,468 @@
import {
buildHarnessContextEngineRuntimeContextFromUsage,
CODEX_APP_SERVER_CONTEXT_ENGINE_HOST,
embeddedAgentLog,
finalizeHarnessContextEngineTurn,
formatErrorMessage,
resolveContextEngineOwnerPluginId,
runAgentHarnessLlmOutputHook,
runHarnessContextEngineMaintenance,
type EmbeddedRunAttemptResult,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { readMirroredSessionHistoryMessages } from "./attempt-context.js";
import { classifyCodexModelCallFailureKind } from "./attempt-diagnostics.js";
import {
buildCodexAppServerPromptTimeoutOutcome,
collectTerminalAssistantText,
isInvalidCodexImagePayloadError,
resolveCodexAppServerReplayBlockedReason,
} from "./attempt-results.js";
import type { CodexAttemptActiveTurn } from "./run-attempt-active-turn.js";
import type { CodexAttemptLifecycleController } from "./run-attempt-lifecycle-controller.js";
import {
emitCodexAppServerEvent,
runCodexAgentEndHook,
shouldKeepCodexSharedAbortOpen,
} from "./run-attempt-lifecycle.js";
import type { CodexAttemptNotificationController } from "./run-attempt-notification-controller.js";
import type { CodexAttemptResources } from "./run-attempt-resources.js";
import {
buildCodexAppServerTimeoutDiagnostics,
clearCodexBindingAfterInvalidImagePayload,
markCodexAppServerBindingCoveredThroughTurn,
shouldUseFreshCodexThreadAfterContextEngineOverflow,
} from "./run-attempt-state.js";
import type { prepareCodexAttemptTurnRequest } from "./run-attempt-turn-request.js";
import type { CodexAttemptTurnState } from "./run-attempt-turn-state.js";
import { normalizeCodexTrajectoryError, recordCodexTrajectoryCompletion } from "./trajectory.js";
import { codexTranscriptMirrorRuntime } from "./transcript-mirror.js";
import { refreshCodexUsageLimitPromptError } from "./usage-limit-error.js";
export async function finalizeCodexAttempt(
resources: CodexAttemptResources,
turnRuntime: CodexAttemptTurnState,
lifecycle: CodexAttemptLifecycleController,
notifications: CodexAttemptNotificationController,
requestRuntime: Awaited<ReturnType<typeof prepareCodexAttemptTurnRequest>>,
activeTurn: CodexAttemptActiveTurn,
): Promise<EmbeddedRunAttemptResult> {
const { prompt, state: resourceState, trajectoryRecorder, markTrajectoryEndRecorded } = resources;
const { context, systemPromptReport } = prompt;
const { runtime, attemptTools, activeTranscriptTarget, historyState, hookContext } = context;
const { hookContextWindowFields, hookRunner, promptState } = context;
const { connection, preparedAuthBinding, activeSessionId, activeSessionFile } = runtime;
const {
buildActiveRunAttemptParams,
effectiveContextTokenBudget,
effectiveRuntimeProviderId,
effectiveRuntimeModelId,
} = runtime;
const {
params,
terminalState,
runAbortController,
activeContextEngine,
bindingStore,
bindingIdentity,
appServer,
usesSupervisionConnection,
sessionAgentId,
contextSessionKey,
effectiveCwd,
effectiveWorkspace,
agentDir,
attemptStartedAt,
} = connection;
const { toolBridge, toolState } = attemptTools;
const {
state,
completion,
pendingOpenClawDynamicToolCompletionIds,
activeTurnItemIds,
activeCompletionBlockerItemIds,
activeFinalizationHookRunIds,
turnWatches,
} = turnRuntime;
const { emitLifecycleTerminal, buildLifecycleTerminalMeta } = lifecycle;
const { drainNotificationQueue } = notifications;
const { codexModelCallDiagnostics } = requestRuntime;
const {
activeTurnId,
activeProjector,
streamState,
freezeRunTerminalOutcome,
notifyUserMessagePersisted,
} = activeTurn;
await completion;
// Include projection work already queued when timeout completion wins.
await drainNotificationQueue();
const hasQuiescentCompletedAssistant =
activeProjector.hasCompletedTerminalAssistantText() &&
state.activeAppServerTurnRequests === 0 &&
activeTurnItemIds.size === 0 &&
activeCompletionBlockerItemIds.size === 0 &&
pendingOpenClawDynamicToolCompletionIds.size === 0 &&
activeFinalizationHookRunIds.size === 0 &&
state.unsettledFinalizationHookCount === 0 &&
state.rejectedFinalizationHookAssistant === undefined;
const hasRecoverableCompletedAssistant =
!turnWatches.isCompletionIdleWatchPinnedByTerminalError() &&
turnWatches.isAssistantCompletionIdleWatchArmed() &&
hasQuiescentCompletedAssistant;
const recoveredTurnWatchTimeout =
state.turnCompletionIdleTimedOut &&
!terminalState.explicitCancellationObserved &&
!state.terminalTurnNotificationQueued &&
hasRecoverableCompletedAssistant &&
activeProjector.recoverCompletedTerminalAssistantAfterTurnWatchTimeout();
if (recoveredTurnWatchTimeout) {
embeddedAgentLog.warn(
"codex app-server recovered completed assistant output after missing turn completion",
{
threadId: resourceState.thread.threadId,
turnId: activeTurnId,
timeoutKind: state.turnWatchTimeoutKind,
idleMs: state.turnWatchTimeoutIdleMs,
timeoutMs: state.turnWatchTimeoutMs,
},
);
trajectoryRecorder?.recordEvent("turn.watch_timeout_recovered", {
threadId: resourceState.thread.threadId,
turnId: activeTurnId,
timeoutKind: state.turnWatchTimeoutKind,
idleMs: state.turnWatchTimeoutIdleMs,
timeoutMs: state.turnWatchTimeoutMs,
});
}
const result = activeProjector.buildResult(toolBridge.telemetry, {
yieldDetected: toolState.yieldDetected,
});
const effectiveTimedOut = state.timedOut && !recoveredTurnWatchTimeout;
const effectiveTurnCompletionIdleTimedOut =
state.turnCompletionIdleTimedOut && !recoveredTurnWatchTimeout;
const isFinalAborted = () =>
result.aborted ||
terminalState.explicitCancellationObserved ||
(runAbortController.signal.aborted && !state.clientClosedAbort && !recoveredTurnWatchTimeout);
const clientClosedPromptErrorForFinal =
state.clientClosedPromptError && hasRecoverableCompletedAssistant
? undefined
: state.clientClosedPromptError;
let finalPromptError =
clientClosedPromptErrorForFinal ??
(effectiveTurnCompletionIdleTimedOut
? state.turnCompletionIdleTimeoutMessage
: effectiveTimedOut
? "codex app-server attempt timed out"
: result.promptError);
const finalPromptErrorMessage =
typeof finalPromptError === "string"
? finalPromptError
: finalPromptError
? formatErrorMessage(finalPromptError)
: undefined;
if (isInvalidCodexImagePayloadError(finalPromptErrorMessage)) {
await clearCodexBindingAfterInvalidImagePayload(bindingStore, bindingIdentity, {
phase: "turn_completed",
threadId: resourceState.thread.threadId,
turnId: activeTurnId,
error: finalPromptErrorMessage,
});
}
if (
resourceState.thread.connectionScope !== "supervision" &&
shouldUseFreshCodexThreadAfterContextEngineOverflow({
error: finalPromptError,
contextEngineActive: Boolean(activeContextEngine),
thread: resourceState.thread,
})
) {
embeddedAgentLog.warn(
"codex app-server context-engine turn overflowed after resume; clearing thread binding for recovery",
{
threadId: resourceState.thread.threadId,
turnId: activeTurnId,
error: finalPromptErrorMessage,
},
);
await bindingStore.mutate(bindingIdentity, {
kind: "clear",
threadId: resourceState.thread.threadId,
});
}
const refreshedUsageLimitPromptError = await refreshCodexUsageLimitPromptError({
client: resourceState.client,
message: finalPromptErrorMessage,
timeoutMs: appServer.requestTimeoutMs,
signal: runAbortController.signal,
});
if (refreshedUsageLimitPromptError) {
finalPromptError = refreshedUsageLimitPromptError;
}
const finalPromptErrorSource =
effectiveTimedOut || clientClosedPromptErrorForFinal ? "prompt" : result.promptErrorSource;
const codexAppServerFailureKind = clientClosedPromptErrorForFinal
? "client_closed_before_turn_completed"
: effectiveTurnCompletionIdleTimedOut
? "turn_completion_idle_timeout"
: undefined;
const replayBlockedReason = codexAppServerFailureKind
? resolveCodexAppServerReplayBlockedReason(result)
: undefined;
const promptTimeoutOutcome = buildCodexAppServerPromptTimeoutOutcome({
result,
turnCompletionIdleTimedOut: effectiveTurnCompletionIdleTimedOut,
turnWatchTimeoutKind: state.turnWatchTimeoutKind,
});
const failureDiagnostics =
codexAppServerFailureKind === "turn_completion_idle_timeout" &&
state.turnWatchTimeoutKind === "completion"
? buildCodexAppServerTimeoutDiagnostics({
idleMs: state.turnWatchTimeoutIdleMs,
timeoutMs: state.turnWatchTimeoutMs,
lastActivityReason: state.turnWatchTimeoutLastActivityReason,
details: state.turnWatchTimeoutDetails,
})
: undefined;
const codexAppServerFailure = codexAppServerFailureKind
? ({
kind: codexAppServerFailureKind,
...(codexAppServerFailureKind === "turn_completion_idle_timeout" &&
state.turnWatchTimeoutKind
? { turnWatchTimeoutKind: state.turnWatchTimeoutKind }
: {}),
transport: appServer.start.transport,
threadId: resourceState.thread.threadId,
turnId: activeTurnId,
replaySafe: replayBlockedReason === undefined,
...(replayBlockedReason ? { replayBlockedReason } : {}),
...(failureDiagnostics ? { diagnostics: failureDiagnostics } : {}),
} satisfies NonNullable<EmbeddedRunAttemptResult["codexAppServerFailure"]>)
: undefined;
const finalAborted = isFinalAborted();
const completedTurnStatus = activeProjector.getCompletedTurnStatus();
const completedWithoutTerminalNotification =
state.completed &&
!state.terminalTurnNotificationQueued &&
!state.timedOut &&
clientClosedPromptErrorForFinal === undefined;
const attemptSucceeded =
!finalAborted &&
!effectiveTimedOut &&
(finalPromptError === null || finalPromptError === undefined) &&
result.agentHarnessResultClassification === undefined &&
(completedTurnStatus === "completed" ||
recoveredTurnWatchTimeout ||
completedWithoutTerminalNotification);
terminalState.sharedAbortAllowedAfterTerminalOutcome = shouldKeepCodexSharedAbortOpen({
trigger: params.trigger,
result,
attemptSucceeded,
explicitCancellationObserved: terminalState.explicitCancellationObserved,
});
// Every terminal observer must see the same immutable outcome.
freezeRunTerminalOutcome();
const modelCallFailureKind =
classifyCodexModelCallFailureKind({
error: finalPromptError,
timedOut: effectiveTimedOut,
turnCompletionIdleTimedOut: effectiveTurnCompletionIdleTimedOut,
runAborted: finalAborted,
abortReason: terminalState.explicitCancellationReason ?? runAbortController.signal.reason,
clientClosedAbort: state.clientClosedAbort,
formatError: formatErrorMessage,
}) ?? (finalAborted ? "aborted" : undefined);
if (modelCallFailureKind) {
codexModelCallDiagnostics.emitError(
finalPromptError ?? "codex app-server attempt interrupted",
{
failureKind: modelCallFailureKind,
},
);
} else if (finalPromptError) {
codexModelCallDiagnostics.emitError(finalPromptError);
} else {
codexModelCallDiagnostics.emitCompleted(result);
}
const assistantTranscriptOwned = await codexTranscriptMirrorRuntime.mirrorBestEffort({
params,
agentId: sessionAgentId,
notifyUserMessagePersisted,
result,
sessionKey: contextSessionKey,
cwd: effectiveCwd,
threadId: resourceState.thread.threadId,
turnId: activeTurnId,
});
if (activeContextEngine) {
const contextEnginePluginId = resolveContextEngineOwnerPluginId(activeContextEngine);
const isHeartbeat =
params.bootstrapContextRunKind === "heartbeat" ||
params.bootstrapContextRunKind === "commitment-only";
const finalMessages =
(await readMirroredSessionHistoryMessages(activeTranscriptTarget)) ??
historyState.messages.concat(result.messagesSnapshot);
await finalizeHarnessContextEngineTurn({
contextEngine: activeContextEngine,
promptError: Boolean(finalPromptError),
aborted: finalAborted,
yieldAborted: Boolean(result.yieldDetected),
sessionIdUsed: activeSessionId,
sessionKey: contextSessionKey,
sessionFile: activeSessionFile,
sessionTarget: params.sessionTarget,
messagesSnapshot: finalMessages,
prePromptMessageCount: promptState.prePromptMessageCount,
tokenBudget: effectiveContextTokenBudget,
runtimeContext: buildHarnessContextEngineRuntimeContextFromUsage({
attempt: buildActiveRunAttemptParams(),
workspaceDir: effectiveWorkspace,
cwd: effectiveCwd,
agentDir,
activeAgentId: sessionAgentId,
contextEnginePluginId,
tokenBudget: effectiveContextTokenBudget,
lastCallUsage: result.attemptUsage,
promptCache: result.promptCache,
}),
contextEngineHostSupport: CODEX_APP_SERVER_CONTEXT_ENGINE_HOST,
providerId: usesSupervisionConnection
? (resourceState.thread.modelProvider ?? effectiveRuntimeProviderId)
: params.provider,
requestedModelId: usesSupervisionConnection ? undefined : params.requestedModelId,
modelId: usesSupervisionConnection
? (resourceState.thread.model ?? effectiveRuntimeModelId)
: params.modelId,
fallbackReason: usesSupervisionConnection ? undefined : params.fallbackReason,
degradedReason: usesSupervisionConnection ? undefined : params.degradedReason,
runMaintenance: runHarnessContextEngineMaintenance,
config: params.config,
warn: (message) => embeddedAgentLog.warn(message),
isHeartbeat,
});
}
runAgentHarnessLlmOutputHook({
event: {
runId: params.runId,
sessionId: params.sessionId,
provider: usesSupervisionConnection
? (resourceState.thread.modelProvider ?? effectiveRuntimeProviderId)
: params.provider,
model: usesSupervisionConnection
? (resourceState.thread.model ?? effectiveRuntimeModelId)
: params.modelId,
...hookContextWindowFields,
resolvedRef: usesSupervisionConnection
? `${resourceState.thread.modelProvider ?? effectiveRuntimeProviderId}/${resourceState.thread.model ?? effectiveRuntimeModelId}`
: (params.runtimePlan?.observability.resolvedRef ?? `${params.provider}/${params.modelId}`),
...(!usesSupervisionConnection && params.runtimePlan?.observability.harnessId
? { harnessId: params.runtimePlan.observability.harnessId }
: {}),
assistantTexts: result.assistantTexts,
...(result.lastAssistant ? { lastAssistant: result.lastAssistant } : {}),
...(result.attemptUsage ? { usage: result.attemptUsage } : {}),
},
ctx: hookContext,
hookRunner,
});
await runCodexAgentEndHook(params, {
event: {
messages: result.messagesSnapshot,
success: !finalAborted && !finalPromptError,
...(finalPromptError ? { error: formatErrorMessage(finalPromptError) } : {}),
durationMs: Date.now() - attemptStartedAt,
},
ctx: hookContext,
hookRunner,
});
state.shouldDelayNativeHookRelayUnregister =
completedTurnStatus === "completed" &&
!effectiveTimedOut &&
!runAbortController.signal.aborted &&
!finalAborted &&
!finalPromptError;
if (state.shouldDelayNativeHookRelayUnregister) {
try {
await markCodexAppServerBindingCoveredThroughTurn({
bindingStore,
identity: bindingIdentity,
threadId: resourceState.thread.threadId,
});
} catch (error) {
if (resourceState.thread.connectionScope === "supervision") {
throw error;
}
const cleared = await bindingStore.mutate(bindingIdentity, {
kind: "clear",
threadId: resourceState.thread.threadId,
});
if (!cleared) {
throw error;
}
embeddedAgentLog.warn(
"codex app-server binding coverage update failed after completed turn; cleared stale binding",
{ threadId: resourceState.thread.threadId, turnId: activeTurnId, error },
);
}
}
recordCodexTrajectoryCompletion(trajectoryRecorder, {
attempt: params,
result,
threadId: resourceState.thread.threadId,
turnId: activeTurnId,
timedOut: effectiveTimedOut,
yieldDetected: toolState.yieldDetected,
});
trajectoryRecorder?.recordEvent("session.ended", {
status: finalPromptError
? "error"
: finalAborted || effectiveTimedOut
? "interrupted"
: "success",
threadId: resourceState.thread.threadId,
turnId: activeTurnId,
timedOut: effectiveTimedOut,
yieldDetected: toolState.yieldDetected,
promptError: normalizeCodexTrajectoryError(finalPromptError),
});
markTrajectoryEndRecorded();
const terminalAssistantText = collectTerminalAssistantText(result);
if (
terminalAssistantText &&
(!streamState.eventEmitted || streamState.needsTerminalSnapshot) &&
!finalAborted &&
!finalPromptError
) {
void emitCodexAppServerEvent(params, {
stream: "assistant",
data: { text: terminalAssistantText },
});
}
emitLifecycleTerminal(
finalPromptError
? {
phase: "error",
error: formatErrorMessage(finalPromptError),
...buildLifecycleTerminalMeta({ aborted: finalAborted, timedOut: effectiveTimedOut }),
}
: {
phase: "end",
...buildLifecycleTerminalMeta({ aborted: finalAborted, timedOut: effectiveTimedOut }),
},
);
return {
...result,
timedOut: effectiveTimedOut,
aborted: finalAborted,
promptError: finalPromptError,
promptErrorSource: finalPromptErrorSource,
...(codexAppServerFailure ? { codexAppServerFailure } : {}),
...(promptTimeoutOutcome ? { promptTimeoutOutcome } : {}),
...(assistantTranscriptOwned ? { assistantTranscriptOwned: true } : {}),
...(resourceState.runtimeArtifact ? { runtimeArtifact: resourceState.runtimeArtifact } : {}),
...(!finalAborted && !effectiveTimedOut && !finalPromptError && preparedAuthBinding
? { authBindingFingerprint: preparedAuthBinding.fingerprint }
: {}),
systemPromptReport,
};
}
@@ -0,0 +1,262 @@
import {
embeddedAgentLog,
FAST_MODE_AUTO_PROGRESS_KIND,
formatErrorMessage,
formatFastModeAutoProgressText,
resolveAgentRunAbortLifecycleFields,
resolveFastModeForElapsed,
type EmbeddedRunAttemptParams,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import {
CODEX_APP_SERVER_INTERRUPT_TIMEOUT_MS,
interruptCodexTurnBestEffort,
} from "./attempt-client-cleanup.js";
import { reportCodexExecutionNotification } from "./attempt-notification-state.js";
import {
resolveTerminalDynamicToolBatchAction,
shouldReleaseTurnAfterTerminalDynamicTool,
} from "./dynamic-tool-execution.js";
import type {
CodexDynamicToolCallParams,
CodexDynamicToolCallResponse,
CodexServerNotification,
} from "./protocol.js";
import { emitCodexAppServerEvent } from "./run-attempt-lifecycle.js";
import type { CodexAttemptResources } from "./run-attempt-resources.js";
import type { CodexAttemptTurnState } from "./run-attempt-turn-state.js";
export function createCodexAttemptLifecycleController(
resources: CodexAttemptResources,
turnRuntime: CodexAttemptTurnState,
) {
const { prompt, state: resourceState, trajectoryRecorder } = resources;
const { connection } = prompt.context.runtime;
const {
params,
attemptStartedAt,
runAbortController,
fastModeAutoStartedAtMs,
fastModeAutoProgressState,
} = connection;
const { state, activeTurnItemIds, pendingOpenClawDynamicToolCompletionIds, turnWatches } =
turnRuntime;
const releaseTurnAfterTerminalDynamicTool = (value: {
call: CodexDynamicToolCallParams;
response: CodexDynamicToolCallResponse;
durationMs: number;
}) => {
if (
!shouldReleaseTurnAfterTerminalDynamicTool({
completed: state.completed,
aborted: runAbortController.signal.aborted,
responseSuccess: value.response.success,
currentTurnHadNonTerminalDynamicToolResult:
state.currentTurnHadNonTerminalDynamicToolResult,
activeAppServerTurnRequests: state.activeAppServerTurnRequests,
activeTurnItemIdsCount: activeTurnItemIds.size,
pendingOpenClawDynamicToolCompletionIdsCount: pendingOpenClawDynamicToolCompletionIds.size,
})
) {
return;
}
state.pendingTerminalDynamicToolRelease = undefined;
trajectoryRecorder?.recordEvent("turn.dynamic_tool_terminal_release", {
threadId: value.call.threadId,
turnId: value.call.turnId,
toolCallId: value.call.callId,
name: value.call.tool,
durationMs: value.durationMs,
});
embeddedAgentLog.info("codex app-server turn released after terminal dynamic tool result", {
threadId: value.call.threadId,
turnId: value.call.turnId,
toolCallId: value.call.callId,
tool: value.call.tool,
durationMs: value.durationMs,
});
interruptCodexTurnBestEffort(resourceState.client, {
threadId: value.call.threadId,
turnId: value.call.turnId,
timeoutMs: CODEX_APP_SERVER_INTERRUPT_TIMEOUT_MS,
});
state.completed = true;
turnWatches.clearCompletionIdleTimer();
turnWatches.clearAssistantCompletionIdleTimer();
turnWatches.clearTerminalIdleTimer();
state.resolveCompletion?.();
};
const scheduleTerminalDynamicToolReleaseCheck = () => {
if (
state.terminalDynamicToolReleaseCheckScheduled ||
(!state.pendingTerminalDynamicToolRelease &&
!state.currentTurnHadNonTerminalDynamicToolResult)
) {
return;
}
// The JSON-RPC response must flush before the terminal tool interrupts its turn.
state.terminalDynamicToolReleaseCheckScheduled = true;
const immediate = setImmediate(() => {
state.terminalDynamicToolReleaseCheckScheduled = false;
const action = resolveTerminalDynamicToolBatchAction({
activeAppServerTurnRequests: state.activeAppServerTurnRequests,
activeTurnItemIdsCount: activeTurnItemIds.size,
pendingOpenClawDynamicToolCompletionIdsCount: pendingOpenClawDynamicToolCompletionIds.size,
currentTurnHadNonTerminalDynamicToolResult:
state.currentTurnHadNonTerminalDynamicToolResult,
hasPendingTerminalDynamicToolRelease: state.pendingTerminalDynamicToolRelease !== undefined,
});
if (action === "release-pending-terminal" && state.pendingTerminalDynamicToolRelease) {
releaseTurnAfterTerminalDynamicTool(state.pendingTerminalDynamicToolRelease);
} else if (action === "clear-nonterminal-batch") {
state.pendingTerminalDynamicToolRelease = undefined;
state.currentTurnHadNonTerminalDynamicToolResult = false;
}
});
immediate.unref?.();
};
const scheduleTurnReleaseAfterTerminalDynamicTool = (value: {
call: CodexDynamicToolCallParams;
response: CodexDynamicToolCallResponse;
durationMs: number;
}) => {
state.pendingTerminalDynamicToolRelease = value;
scheduleTerminalDynamicToolReleaseCheck();
};
const emitLifecycleStart = () => {
void emitCodexAppServerEvent(params, {
stream: "lifecycle",
data: { phase: "start", startedAt: attemptStartedAt },
});
state.lifecycleStarted = true;
};
const emitLifecycleTerminal = (data: Record<string, unknown> & { phase: "end" | "error" }) => {
if (!state.lifecycleStarted || state.lifecycleTerminalEmitted) {
return;
}
void emitCodexAppServerEvent(params, {
stream: "lifecycle",
data: {
startedAt: attemptStartedAt,
endedAt: Date.now(),
...data,
...((params.deferTerminalLifecycle ?? params.deferTerminalLifecycleEnd)
? { phase: "finishing" }
: {}),
},
});
state.lifecycleTerminalEmitted = true;
};
const buildLifecycleTerminalMeta = (input: { aborted: boolean; timedOut: boolean }) => {
const abortFields = input.aborted
? resolveAgentRunAbortLifecycleFields(runAbortController.signal)
: undefined;
if (input.timedOut || abortFields?.stopReason === "timeout") {
return {
aborted: true,
status: "timed_out",
stopReason: "timeout",
timeoutPhase: "provider",
providerStarted: true,
} as const;
}
return input.aborted
? ({ aborted: true, status: "cancelled", stopReason: "stop" } as const)
: undefined;
};
const executionPhaseKeys = new Set<string>();
const emitExecutionPhaseOnce = (
key: string,
info: Parameters<NonNullable<EmbeddedRunAttemptParams["onExecutionPhase"]>>[0],
) => {
if (executionPhaseKeys.has(key)) {
return;
}
executionPhaseKeys.add(key);
params.onExecutionPhase?.({
provider: params.provider,
model: params.modelId,
backend: "codex-app-server",
...info,
});
};
const reportExecutionNotification = (notification: CodexServerNotification) => {
reportCodexExecutionNotification({ notification, emitExecutionPhaseOnce });
};
const emitFastModeAutoProgress = async (payload: {
enabled: boolean;
elapsedSeconds: number;
fastAutoOnSeconds?: number;
}) => {
const summary = formatFastModeAutoProgressText(payload);
await emitCodexAppServerEvent(params, {
stream: "item",
data: { kind: "status", title: "Fast", phase: "update", summary },
});
try {
await params.onToolResult?.({
text: summary,
channelData: { openclawProgressKind: FAST_MODE_AUTO_PROGRESS_KIND },
});
} catch (error) {
embeddedAgentLog.debug("codex app-server fast mode auto progress delivery failed", { error });
}
};
const maybeAnnounceFastModeAutoOff = async () => {
if (
params.fastModeAuto !== true ||
fastModeAutoStartedAtMs === undefined ||
fastModeAutoProgressState.offAnnounced
) {
return;
}
const next = resolveFastModeForElapsed({
mode: "auto",
startedAtMs: fastModeAutoStartedAtMs,
fastAutoOnSeconds: params.fastModeAutoOnSeconds,
});
if (next.enabled) {
return;
}
fastModeAutoProgressState.offAnnounced = true;
await emitFastModeAutoProgress(next);
};
const maybeEmitFastModeAutoReset = async () => {
if (
params.fastModeAuto !== true ||
!fastModeAutoProgressState.offAnnounced ||
fastModeAutoProgressState.resetAnnounced
) {
return;
}
fastModeAutoProgressState.resetAnnounced = true;
await emitFastModeAutoProgress({
enabled: true,
elapsedSeconds: 0,
fastAutoOnSeconds: params.fastModeAutoOnSeconds,
});
};
const maybeEmitFastModeAutoResetBestEffort = async () => {
try {
await maybeEmitFastModeAutoReset();
} catch (error) {
embeddedAgentLog.warn(
`codex app-server fast mode auto reset progress failed: ${formatErrorMessage(error)}`,
);
}
};
return {
scheduleTerminalDynamicToolReleaseCheck,
scheduleTurnReleaseAfterTerminalDynamicTool,
emitLifecycleStart,
emitLifecycleTerminal,
buildLifecycleTerminalMeta,
emitExecutionPhaseOnce,
reportExecutionNotification,
maybeAnnounceFastModeAutoOff,
maybeEmitFastModeAutoResetBestEffort,
};
}
export type CodexAttemptLifecycleController = ReturnType<
typeof createCodexAttemptLifecycleController
>;
@@ -0,0 +1,266 @@
import { embeddedAgentLog } from "openclaw/plugin-sdk/agent-harness-runtime";
import {
applyCodexTurnNotificationState,
isTerminalCodexTurnNotificationForTurn,
} from "./attempt-notification-state.js";
import {
describeNotificationActivity,
isAssistantCompletionReleaseNotification,
isRawFunctionToolOutputCompletionNotification,
readCodexNotificationItem,
readRawResponseToolCallId,
} from "./attempt-notifications.js";
import type { CodexServerNotification } from "./protocol.js";
import type { CodexAttemptLifecycleController } from "./run-attempt-lifecycle-controller.js";
import type { CodexAttemptResources } from "./run-attempt-resources.js";
import {
readCodexFinalizationHookNotification,
waitForCodexNotificationDispatchTurn,
} from "./run-attempt-state.js";
import type { CodexAttemptTurnState } from "./run-attempt-turn-state.js";
import { CODEX_APP_SERVER_NATIVE_TURN_WAIT_TIMEOUT_MS } from "./turn-router.js";
import type { CodexThreadRouteScope } from "./turn-router.js";
export function createCodexAttemptNotificationController(
resources: CodexAttemptResources,
turnRuntime: CodexAttemptTurnState,
lifecycle: CodexAttemptLifecycleController,
) {
const { prompt, state: resourceState, projectorRef, registerNativeSubagentMonitor } = resources;
const { context, turnState } = prompt;
const { attemptTools, runtime } = context;
const { connection } = runtime;
const { appServer, runAbortController } = connection;
const { allocateCodexToolOutcomeOrdinal } = attemptTools;
const {
state,
turnIdRef,
userInputBridgeRef,
steeringQueueRef,
turnWatches,
activeTurnItemIds,
activeCompletionBlockerItemIds,
activeFinalizationHookRunIds,
finalizationHookBatchStatuses,
pendingOpenClawDynamicToolCompletionIds,
postToolRawAssistantCompletionIdleTimeoutMs,
} = turnRuntime;
const {
scheduleTerminalDynamicToolReleaseCheck,
reportExecutionNotification,
maybeAnnounceFastModeAutoOff,
} = lifecycle;
const isTerminalTurnNotificationForTurn = (
notification: CodexServerNotification,
notificationTurnId: string,
) =>
isTerminalCodexTurnNotificationForTurn({
notification,
threadId: resourceState.thread.threadId,
turnId: notificationTurnId,
currentPromptTexts: [turnState.codexTurnPromptText],
});
const handleNotification = async (notification: CodexServerNotification) => {
const projector = projectorRef.current;
const turnId = turnIdRef.current;
const steeringQueue = steeringQueueRef.current;
userInputBridgeRef.current?.handleNotification(notification);
if (!projector || !turnId) {
if (notification.method === "error") {
state.latestStartupErrorNotification = notification;
}
return;
}
const notificationState = applyCodexTurnNotificationState({
notification,
threadId: resourceState.thread.threadId,
turnId,
currentPromptTexts: [turnState.codexTurnPromptText],
turnWatches,
activeTurnItemIds,
activeCompletionBlockerItemIds,
activeAppServerTurnRequests: state.activeAppServerTurnRequests,
pendingOpenClawDynamicToolCompletionIds,
turnCrossedToolHandoff: state.turnCrossedToolHandoff,
postToolRawAssistantCompletionIdleTimeoutMs,
onScheduleTerminalDynamicToolReleaseCheck: scheduleTerminalDynamicToolReleaseCheck,
onReportExecutionNotification: reportExecutionNotification,
});
state.turnCrossedToolHandoff = notificationState.turnCrossedToolHandoff;
const hookNotification = readCodexFinalizationHookNotification(
notification,
resourceState.thread.threadId,
turnId,
);
if (hookNotification?.phase === "started") {
if (activeFinalizationHookRunIds.size === 0) {
finalizationHookBatchStatuses.clear();
}
activeFinalizationHookRunIds.add(hookNotification.runId);
turnWatches.disarmAssistantCompletionIdleWatch();
}
if (notificationState.isTurnTerminal) {
state.terminalTurnNotificationQueued = true;
}
try {
await waitForCodexNotificationDispatchTurn();
await projector.handleNotification(notification);
const canRelease =
isAssistantCompletionReleaseNotification(notification, state.turnCrossedToolHandoff) ||
(notificationState.isCurrentTurnNotification &&
state.turnCrossedToolHandoff &&
notification.method === "rawResponseItem/completed" &&
projector.canReleaseLatestTerminalAssistantAfterToolHandoff());
if (notificationState.isCurrentTurnNotification && canRelease) {
const itemId = projector.getLatestTerminalAssistantCandidate()?.itemId;
if (
state.rejectedFinalizationHookAssistant &&
itemId &&
itemId !== state.rejectedFinalizationHookAssistant.itemId
) {
state.rejectedFinalizationHookAssistant = undefined;
} else if (state.rejectedFinalizationHookAssistant) {
turnWatches.disarmAssistantCompletionIdleWatch();
} else if (
activeFinalizationHookRunIds.size === 0 &&
!state.terminalTurnNotificationQueued &&
state.activeAppServerTurnRequests === 0 &&
activeTurnItemIds.size === 0 &&
activeCompletionBlockerItemIds.size === 0 &&
pendingOpenClawDynamicToolCompletionIds.size === 0 &&
projector.hasLatestTerminalAssistantCandidateText()
) {
turnWatches.armAssistantCompletionIdleWatch(describeNotificationActivity(notification));
}
}
if (
notificationState.isCurrentTurnNotification &&
activeTurnItemIds.size === 0 &&
isRawFunctionToolOutputCompletionNotification(notification)
) {
await maybeAnnounceFastModeAutoOff();
}
} catch (error) {
embeddedAgentLog.debug("codex app-server projector notification threw", {
method: notification.method,
error,
});
} finally {
if (hookNotification?.phase === "completed") {
state.unsettledFinalizationHookCount = Math.max(
0,
state.unsettledFinalizationHookCount - 1,
);
activeFinalizationHookRunIds.delete(hookNotification.runId);
finalizationHookBatchStatuses.set(hookNotification.runId, hookNotification.status);
if (activeFinalizationHookRunIds.size === 0) {
const statuses = new Set(finalizationHookBatchStatuses.values());
if (statuses.has("blocked") && !statuses.has("stopped")) {
const itemId = projector.getLatestTerminalAssistantCandidate()?.itemId;
state.rejectedFinalizationHookAssistant = itemId ? { itemId } : {};
turnWatches.disarmAssistantCompletionIdleWatch();
} else {
state.rejectedFinalizationHookAssistant = undefined;
}
}
if (
activeFinalizationHookRunIds.size === 0 &&
state.rejectedFinalizationHookAssistant === undefined &&
!state.terminalTurnNotificationQueued &&
state.activeAppServerTurnRequests === 0 &&
activeTurnItemIds.size === 0 &&
activeCompletionBlockerItemIds.size === 0 &&
pendingOpenClawDynamicToolCompletionIds.size === 0 &&
projector.hasLatestTerminalAssistantCandidateText()
) {
turnWatches.armAssistantCompletionIdleWatch({
lastNotificationMethod: notification.method,
hookRunId: hookNotification.runId,
hookStatus: hookNotification.status,
});
}
}
if (notificationState.isTurnTerminal) {
if (notificationState.isTurnAbortMarker) {
projector.markAborted();
}
if (!state.timedOut && !runAbortController.signal.aborted) {
await steeringQueue?.flushPending();
}
state.completed = true;
turnWatches.clearCompletionIdleTimer();
turnWatches.clearAssistantCompletionIdleTimer();
turnWatches.clearTerminalIdleTimer();
state.resolveCompletion?.();
}
}
};
const waitForActiveNativeTurnCompletion = async () => {
const route = resourceState.turnRoute;
if (!route) {
return false;
}
return await route.waitForTurnCompletion({
timeoutMs: Math.min(appServer.requestTimeoutMs, CODEX_APP_SERVER_NATIVE_TURN_WAIT_TIMEOUT_MS),
signal: runAbortController.signal,
});
};
const noteNotificationReceived = (
notification: CodexServerNotification,
scope: CodexThreadRouteScope,
receivedAtMs: number,
) => {
const projector = projectorRef.current;
const turnId = turnIdRef.current;
if (!projector || !turnId) {
return;
}
if (isTerminalTurnNotificationForTurn(notification, turnId)) {
state.terminalTurnNotificationQueued = true;
}
if (scope.turnId === turnId) {
const modelToolCallId = readRawResponseToolCallId(notification);
if (modelToolCallId) {
allocateCodexToolOutcomeOrdinal?.(modelToolCallId);
}
const nativeItem = readCodexNotificationItem(notification.params);
if (nativeItem?.type === "webSearch") {
projector.recordNativeToolOutcome(nativeItem);
}
}
const hookNotification = readCodexFinalizationHookNotification(
notification,
resourceState.thread.threadId,
turnId,
);
if (hookNotification?.phase === "started") {
state.unsettledFinalizationHookCount += 1;
turnWatches.disarmAssistantCompletionIdleWatch();
}
turnWatches.noteNotificationReceived(notification.method, { receivedAtMs });
};
const enqueueNotification = async (
notification: CodexServerNotification,
scope: CodexThreadRouteScope,
) => {
embeddedAgentLog.trace("codex app-server raw notification received", {
method: notification.method,
...scope,
});
await handleNotification(notification);
};
const drainNotificationQueue = async () => {
await resourceState.turnRoute?.drain();
};
registerNativeSubagentMonitor(resourceState.thread.threadId);
return {
waitForActiveNativeTurnCompletion,
noteNotificationReceived,
enqueueNotification,
drainNotificationQueue,
};
}
export type CodexAttemptNotificationController = ReturnType<
typeof createCodexAttemptNotificationController
>;
@@ -0,0 +1,480 @@
import {
assembleHarnessContextEngine,
CODEX_APP_SERVER_CONTEXT_ENGINE_HOST,
embeddedAgentLog,
formatErrorMessage,
resolveAgentHarnessBeforePromptBuildResult,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import {
buildCodexSystemPromptReport,
prependCodexOpenClawPromptContext,
readContextEngineThreadBootstrapProjection,
resolveCodexDeliveryHintPreservedInputRange,
resolveContextEngineBootstrapProjectionDecision,
} from "./attempt-context.js";
import {
fitCodexProjectedContextForTurnStart,
projectContextEngineAssemblyForCodex,
type CodexProjectedContextRange,
} from "./context-engine-projection.js";
import { flattenCodexDynamicToolFunctions } from "./protocol.js";
import type { CodexAttemptContext } from "./run-attempt-context.js";
import { estimateCodexAppServerProjectedTurnTokens } from "./run-attempt-lifecycle.js";
import {
isNonEmptyString,
joinPresentSections,
prependCurrentInboundContext,
} from "./run-attempt-state.js";
import { rotateOversizedCodexAppServerStartupBinding } from "./startup-binding.js";
import {
buildContextEngineBinding,
buildTurnCollaborationMode,
codexDynamicToolsFingerprint,
codexLegacyDynamicToolsFingerprint,
} from "./thread-lifecycle.js";
export async function prepareCodexAttemptPrompt(context: CodexAttemptContext) {
const {
runtime,
attemptTools,
historyState,
hookContext,
workspaceBootstrapContext,
baseDeveloperInstructions,
openClawPromptContext,
skillsCollaborationInstructions,
promptState,
codexContextProjectionMaxChars,
} = context;
const {
connection,
buildActiveRunAttemptParams,
effectiveContextTokenBudget,
effectiveRuntimeModelId,
effectiveRuntimeProviderId,
} = runtime;
const {
params,
activeContextEngine,
usesSupervisionConnection,
mutable,
isInactiveThreadBootstrapBinding,
bindingStore,
bindingIdentity,
agentDir,
appServer,
contextSessionKey,
effectiveWorkspace,
} = connection;
const { toolBridge } = attemptTools;
const applyFreshThreadContinuityProjection = () => {
const projection = projectContextEngineAssemblyForCodex({
assembledMessages: historyState.messages,
originalHistoryMessages: historyState.messages,
prompt: params.prompt,
maxRenderedContextChars: codexContextProjectionMaxChars,
});
promptState.promptText = projection.promptText;
promptState.promptContextRange = projection.promptContextRange;
promptState.prePromptMessageCount = projection.prePromptMessageCount;
};
const applyActiveContextEngineProjection = async (
decisionStartupBinding: typeof mutable.startupBinding,
) => {
if (!activeContextEngine) {
return;
}
const assembled = await assembleHarnessContextEngine({
contextEngine: activeContextEngine,
sessionId: runtime.activeSessionId,
sessionKey: contextSessionKey,
messages: historyState.messages,
tokenBudget: effectiveContextTokenBudget,
availableTools: new Set(
flattenCodexDynamicToolFunctions(toolBridge.availableSpecs)
.map((tool) => tool.name)
.filter(isNonEmptyString),
),
citationsMode: params.config?.memory?.citations,
modelId: effectiveRuntimeModelId,
contextEngineHostSupport: CODEX_APP_SERVER_CONTEXT_ENGINE_HOST,
providerId: effectiveRuntimeProviderId,
requestedModelId: usesSupervisionConnection ? undefined : params.requestedModelId,
fallbackReason: usesSupervisionConnection ? undefined : params.fallbackReason,
degradedReason: usesSupervisionConnection ? undefined : params.degradedReason,
prompt: params.prompt,
});
if (!assembled) {
throw new Error("context engine assemble returned no result");
}
promptState.contextEngineProjection = readContextEngineThreadBootstrapProjection(
assembled.contextProjection,
);
const projection = projectContextEngineAssemblyForCodex({
assembledMessages: assembled.messages,
originalHistoryMessages: historyState.messages,
prompt: params.prompt,
systemPromptAddition: assembled.systemPromptAddition,
maxRenderedContextChars: codexContextProjectionMaxChars,
toolPayloadMode: promptState.contextEngineProjection ? "preserve" : "elide",
});
const projectionDecision = promptState.contextEngineProjection
? resolveContextEngineBootstrapProjectionDecision({
startupBinding: decisionStartupBinding,
expectedBinding: buildContextEngineBinding(
buildActiveRunAttemptParams(),
promptState.contextEngineProjection,
),
projection: promptState.contextEngineProjection,
dynamicToolsFingerprint: codexDynamicToolsFingerprint(toolBridge.specs),
legacyDynamicToolsFingerprint: codexLegacyDynamicToolsFingerprint(toolBridge.specs),
})
: { project: true, reason: "per-turn-projection" };
const decisionBinding = decisionStartupBinding;
embeddedAgentLog.info("codex app-server context-engine projection decision", {
sessionId: params.sessionId,
sessionKey: contextSessionKey,
engineId: activeContextEngine.info.id,
mode:
promptState.contextEngineProjection?.mode ??
assembled.contextProjection?.mode ??
"per_turn",
epoch: promptState.contextEngineProjection?.epoch,
fingerprint: promptState.contextEngineProjection?.fingerprint,
previousThreadId: decisionBinding?.threadId,
previousEpoch: decisionBinding?.contextEngine?.projection?.epoch,
previousFingerprint: decisionBinding?.contextEngine?.projection?.fingerprint,
projected: projectionDecision.project,
reason: projectionDecision.reason,
assembledMessages: assembled.messages.length,
originalHistoryMessages: historyState.messages.length,
projectedPromptChars: projection.promptText.length,
developerInstructionAdditionChars: projection.developerInstructionAddition?.length ?? 0,
});
promptState.promptText = projectionDecision.project ? projection.promptText : params.prompt;
promptState.promptContextRange = projectionDecision.project
? projection.promptContextRange
: undefined;
promptState.developerInstructions = joinPresentSections(
baseDeveloperInstructions,
projection.developerInstructionAddition,
);
promptState.prePromptMessageCount = projection.prePromptMessageCount;
};
if (activeContextEngine) {
try {
await applyActiveContextEngineProjection(
runtime.nativeToolSurfaceEnabled ? mutable.startupBinding : undefined,
);
} catch (assembleErr) {
embeddedAgentLog.warn("context engine assemble failed; using Codex baseline prompt", {
error: formatErrorMessage(assembleErr),
});
}
}
const codexModelInputHistoryMessages: typeof historyState.messages = [];
const buildPromptFromCurrentInputs = () =>
resolveAgentHarnessBeforePromptBuildResult({
prompt: prependCurrentInboundContext(promptState.promptText, params.currentInboundContext),
developerInstructions: promptState.developerInstructions,
messages: codexModelInputHistoryMessages,
ctx: hookContext,
bootstrapContextRunKind: params.bootstrapContextRunKind,
...("beforeAgentStartResult" in params
? { beforeAgentStartResult: params.beforeAgentStartResult }
: {}),
});
const resolveShiftedPromptInputRange = (
prompt: string,
promptInputRange: { start: number; end: number } | undefined,
turnPromptText: string,
): CodexProjectedContextRange | undefined => {
if (
!promptInputRange ||
promptInputRange.start < 0 ||
promptInputRange.end < promptInputRange.start ||
promptInputRange.end > prompt.length ||
!turnPromptText.endsWith(prompt)
) {
return undefined;
}
const turnPromptOffset = turnPromptText.length - prompt.length;
return {
start: turnPromptOffset + promptInputRange.start,
end: turnPromptOffset + promptInputRange.end,
};
};
const resolveShiftedPromptContextRange = (
prompt: string,
promptInputRange: { start: number; end: number } | undefined,
turnPromptText: string,
) => {
const promptTextInputOffset = promptInputRange
? promptInputRange.end - promptState.promptText.length
: undefined;
if (
!promptState.promptContextRange ||
!promptInputRange ||
promptTextInputOffset === undefined ||
promptInputRange.start < 0 ||
promptInputRange.end < promptInputRange.start ||
promptInputRange.end > prompt.length ||
promptTextInputOffset < promptInputRange.start ||
prompt.slice(promptTextInputOffset, promptInputRange.end) !== promptState.promptText ||
!turnPromptText.endsWith(prompt)
) {
return undefined;
}
const promptTextOffset = prompt.endsWith(promptState.promptText)
? prompt.length - promptState.promptText.length
: promptTextInputOffset;
if (promptTextOffset < 0) {
return undefined;
}
const turnPromptOffset = turnPromptText.length - prompt.length + promptTextOffset;
const contextRange = {
start: turnPromptOffset + promptState.promptContextRange.start,
end: turnPromptOffset + promptState.promptContextRange.end,
};
return {
contextRange,
requestRange: {
start: contextRange.end,
end: turnPromptOffset + promptState.promptText.length,
},
};
};
const decorateCodexTurnPromptText = (promptBuildResult: {
prompt: string;
promptInputRange?: { start: number; end: number };
}) => {
const turnPromptText = prependCodexOpenClawPromptContext(
promptBuildResult.prompt,
openClawPromptContext,
{
preservePromptWithoutContext:
params.bootstrapContextMode === "lightweight" &&
params.bootstrapContextRunKind === "cron",
},
);
const projectedRanges = resolveShiftedPromptContextRange(
promptBuildResult.prompt,
promptBuildResult.promptInputRange,
turnPromptText,
);
const preservedRange =
resolveShiftedPromptInputRange(
promptBuildResult.prompt,
promptBuildResult.promptInputRange,
turnPromptText,
) ??
resolveCodexDeliveryHintPreservedInputRange({
prompt: promptBuildResult.prompt,
promptInputRange: promptBuildResult.promptInputRange,
decoratedPrompt: turnPromptText,
});
return fitCodexProjectedContextForTurnStart({
promptText: turnPromptText,
contextRange: projectedRanges?.contextRange,
requestRange: projectedRanges?.requestRange,
preservedRange,
});
};
const firstPromptBuild = await buildPromptFromCurrentInputs();
const turnState = {
promptBuild: firstPromptBuild,
codexTurnPromptText: decorateCodexTurnPromptText(firstPromptBuild),
};
const buildRenderedCodexDeveloperInstructions = () =>
joinPresentSections(
turnState.promptBuild.developerInstructions,
buildTurnCollaborationMode(params, {
turnScopedDeveloperInstructions: workspaceBootstrapContext.turnScopedDeveloperInstructions,
skillsCollaborationInstructions,
memoryCollaborationInstructions: workspaceBootstrapContext.memoryCollaborationInstructions,
heartbeatCollaborationInstructions:
workspaceBootstrapContext.heartbeatCollaborationInstructions,
}).settings.developer_instructions ?? undefined,
);
const rebuildCodexPromptBuildFromCurrentProjection = async () => {
turnState.promptBuild = await buildPromptFromCurrentInputs();
turnState.codexTurnPromptText = decorateCodexTurnPromptText(turnState.promptBuild);
};
const rebuildCodexTurnPromptTextFromCurrentProjection = async () => {
const nextPromptBuild = await buildPromptFromCurrentInputs();
turnState.promptBuild = {
...turnState.promptBuild,
prompt: nextPromptBuild.prompt,
promptInputRange: nextPromptBuild.promptInputRange,
};
turnState.codexTurnPromptText = decorateCodexTurnPromptText(nextPromptBuild);
};
const selectNewerVisibleHistoryAfterBinding = (
binding: NonNullable<typeof mutable.startupBinding>,
) => {
const cutoff = Date.parse(binding.historyCoveredThrough ?? "");
return historyState.messages.filter((message) => {
if (message.role !== "user" && message.role !== "assistant") {
return false;
}
const record = message as unknown as Record<string, unknown>;
const meta = record["__openclaw"];
const mirrorIdentity =
meta && typeof meta === "object" && !Array.isArray(meta)
? (meta as Record<string, unknown>).mirrorIdentity
: undefined;
const mirrorOrigin =
meta && typeof meta === "object" && !Array.isArray(meta)
? (meta as Record<string, unknown>).mirrorOrigin
: undefined;
const timestamp =
typeof message.timestamp === "number"
? message.timestamp
: typeof message.timestamp === "string"
? Date.parse(message.timestamp)
: Number.NaN;
return (
!(
typeof record.idempotencyKey === "string" &&
record.idempotencyKey.startsWith("codex-app-server:")
) &&
mirrorOrigin !== "codex-app-server" &&
!(typeof mirrorIdentity === "string" && mirrorIdentity.startsWith("codex-app-server:")) &&
Number.isFinite(timestamp) &&
timestamp > (Number.isFinite(cutoff) ? cutoff : 0)
);
});
};
const applyResumeStaleBindingContinuityProjection = (
binding: NonNullable<typeof mutable.startupBinding>,
) => {
const newerVisibleMessages = selectNewerVisibleHistoryAfterBinding(binding);
if (newerVisibleMessages.length === 0) {
return false;
}
const projection = projectContextEngineAssemblyForCodex({
assembledMessages: newerVisibleMessages,
originalHistoryMessages: historyState.messages,
prompt: params.prompt,
maxRenderedContextChars: codexContextProjectionMaxChars,
});
promptState.promptText = projection.promptText;
promptState.promptContextRange = projection.promptContextRange;
promptState.prePromptMessageCount = projection.prePromptMessageCount;
return true;
};
const precomputeNoContextEngineStaleBindingProjection = () => {
promptState.precomputedStaleBindingContinuityProjectionApplied = false;
promptState.staleBindingContinuityForcedFreshStart = false;
const binding = mutable.startupBinding;
if (activeContextEngine || !binding?.threadId || binding.pendingSupervisionBranch) {
return false;
}
if (isInactiveThreadBootstrapBinding(binding)) {
promptState.inactiveThreadBootstrapBindingForcedFreshStart = true;
return false;
}
const projected = applyResumeStaleBindingContinuityProjection(binding);
promptState.precomputedStaleBindingContinuityProjectionApplied = projected;
return projected;
};
const applyNoContextEngineContinuityProjection = (
action: "started" | "resumed" | "forked",
binding?: NonNullable<typeof mutable.startupBinding>,
) => {
if (activeContextEngine || !historyState.messages.some((message) => message.role === "user")) {
return false;
}
if (action === "resumed" && promptState.precomputedStaleBindingContinuityProjectionApplied) {
return true;
}
if (action === "started" && promptState.staleBindingContinuityForcedFreshStart) {
return true;
}
if (action === "started" && promptState.inactiveThreadBootstrapBindingForcedFreshStart) {
return false;
}
if (action === "resumed" && binding) {
return applyResumeStaleBindingContinuityProjection(binding);
}
if (action === "started") {
applyFreshThreadContinuityProjection();
return true;
}
return false;
};
if (precomputeNoContextEngineStaleBindingProjection()) {
await rebuildCodexPromptBuildFromCurrentProjection();
}
const rotateStartupBindingForProjectedTurn = async () => {
const binding = mutable.startupBinding;
if (!binding?.threadId) {
return;
}
const previousThreadId = binding.threadId;
const hadInactiveThreadBootstrapBinding = isInactiveThreadBootstrapBinding(binding);
mutable.startupBinding = await rotateOversizedCodexAppServerStartupBinding({
binding,
bindingStore,
identity: bindingIdentity,
sessionFile: params.sessionFile,
agentDir,
codexHome: appServer.start.env?.CODEX_HOME,
config: params.config,
contextEngineActive: Boolean(activeContextEngine),
projectedTurnTokens: estimateCodexAppServerProjectedTurnTokens({
prompt: turnState.codexTurnPromptText,
developerInstructions: buildRenderedCodexDeveloperInstructions(),
}),
});
if (mutable.startupBinding?.threadId) {
return;
}
promptState.inactiveThreadBootstrapBindingForcedFreshStart = hadInactiveThreadBootstrapBinding;
promptState.staleBindingContinuityForcedFreshStart =
promptState.precomputedStaleBindingContinuityProjectionApplied &&
!promptState.inactiveThreadBootstrapBindingForcedFreshStart;
if (promptState.staleBindingContinuityForcedFreshStart) {
applyFreshThreadContinuityProjection();
}
if (activeContextEngine) {
promptState.contextEngineProjection = undefined;
try {
await applyActiveContextEngineProjection(undefined);
} catch (assembleErr) {
embeddedAgentLog.warn("context engine assemble failed; using Codex baseline prompt", {
error: formatErrorMessage(assembleErr),
});
}
}
await rebuildCodexPromptBuildFromCurrentProjection();
embeddedAgentLog.info("codex app-server rebuilt turn prompt after native thread rotation", {
sessionId: params.sessionId,
sessionKey: contextSessionKey,
previousThreadId,
promptChars: turnState.codexTurnPromptText.length,
developerInstructionChars: buildRenderedCodexDeveloperInstructions()?.length ?? 0,
});
};
await rotateStartupBindingForProjectedTurn();
const systemPromptReport = buildCodexSystemPromptReport({
attempt: params,
sessionKey: contextSessionKey,
workspaceDir: effectiveWorkspace,
developerInstructions: buildRenderedCodexDeveloperInstructions(),
workspaceBootstrapContext,
skillsPrompt: skillsCollaborationInstructions ? (params.skillsSnapshot?.prompt ?? "") : "",
tools: toolBridge.availableSpecs,
});
return {
context,
codexModelInputHistoryMessages,
turnState,
buildRenderedCodexDeveloperInstructions,
rebuildCodexTurnPromptTextFromCurrentProjection,
applyNoContextEngineContinuityProjection,
systemPromptReport,
};
}
export type CodexAttemptPrompt = Awaited<ReturnType<typeof prepareCodexAttemptPrompt>>;
@@ -0,0 +1,251 @@
import {
embeddedAgentLog,
type AgentHarnessRuntimeArtifactBinding,
type NativeHookRelayRegistrationHandle,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { resolveCodexStartupTimeoutMs } from "./attempt-timeouts.js";
import type { CodexAppServerClient } from "./client.js";
import { resolveCodexToolAbortTerminalReason } from "./dynamic-tool-execution.js";
import { CodexAppServerEventProjector } from "./event-projector.js";
import {
buildCodexNativeHookRelayDisabledConfig,
buildCodexNativeHookRelayConfig,
CODEX_NATIVE_HOOK_RELAY_TTL_GRACE_MS,
createCodexNativeHookRelay,
emitCodexNativePreToolUseFailureDiagnostic,
type CodexNativePreToolUseFailure,
} from "./native-hook-relay.js";
import { codexNativeSubagentMonitorRuntime } from "./native-subagent-monitor.js";
import type { CodexSandboxPolicy, CodexTurnEnvironmentParams } from "./protocol.js";
import type { CodexAttemptPrompt } from "./run-attempt-prompt.js";
import { releaseCodexSandboxExecServerEnvironment } from "./sandbox-exec-server.js";
import type { CodexAppServerThreadBinding } from "./session-binding.js";
import {
retainSharedCodexAppServerClientIfCurrent,
retireSharedCodexAppServerClientIfCurrent,
} from "./shared-client.js";
import type { CodexAppServerThreadLifecycleBinding } from "./thread-lifecycle.js";
import { createCodexTrajectoryRecorder, type CodexHostTrajectoryRecorder } from "./trajectory.js";
import type { CodexAppServerTurnRouter, CodexThreadRouteReservation } from "./turn-router.js";
export function prepareCodexAttemptResources(prompt: CodexAttemptPrompt) {
const { context, turnState, buildRenderedCodexDeveloperInstructions } = prompt;
const { runtime, attemptTools } = context;
const { connection, hookChannelId } = runtime;
const {
params,
effectiveCwd,
sessionAgentId,
sandboxSessionKey,
runAbortController,
sandbox,
options,
nativeHookRelayEvents,
} = connection;
const { toolBridge } = attemptTools;
const hostTrajectoryRecorder = (
params as typeof params & { trajectoryRecorder?: CodexHostTrajectoryRecorder | null }
).trajectoryRecorder;
const trajectoryRecorder = createCodexTrajectoryRecorder({
attempt: params,
cwd: effectiveCwd,
developerInstructions: buildRenderedCodexDeveloperInstructions(),
prompt: turnState.codexTurnPromptText,
trajectoryRecorder: hostTrajectoryRecorder,
trajectorySessionFile: params.trajectorySessionFile,
tools: toolBridge.availableSpecs,
warn: (message, fields) => embeddedAgentLog.warn(message, fields),
});
const state = {
client: undefined as unknown as CodexAppServerClient,
thread: undefined as unknown as CodexAppServerThreadLifecycleBinding,
runtimeArtifact: undefined as AgentHarnessRuntimeArtifactBinding | undefined,
turnRouter: undefined as unknown as CodexAppServerTurnRouter,
turnRoute: undefined as CodexThreadRouteReservation | undefined,
routeActivated: false,
detachRouteAbort: (() => undefined) as () => void,
trajectoryEndRecorded: false,
nativeHookRelay: undefined as NativeHookRelayRegistrationHandle | undefined,
nativeSubagentMonitor: undefined as
| ReturnType<typeof codexNativeSubagentMonitorRuntime.register>
| undefined,
nativePreToolUseFailureFallbackActive: false,
nativePreToolUseFailureFallbackTerminalReason: undefined as
| CodexNativePreToolUseFailure["disposition"]
| undefined,
releaseSharedClientLease: undefined as (() => void) | undefined,
sharedCodexClientRetiredForOneShotCleanup: false,
sandboxExecEnvironmentAcquired: false,
codexEnvironmentSelection: undefined as CodexTurnEnvironmentParams[] | undefined,
codexExecutionCwd: effectiveCwd,
codexSandboxPolicy: undefined as CodexSandboxPolicy | undefined,
restartContextEngineCodexThread: undefined as
| (() => Promise<CodexAppServerThreadLifecycleBinding>)
| undefined,
};
const pendingNativePreToolUseFailures: CodexNativePreToolUseFailure[] = [];
const projectorRef: { current?: CodexAppServerEventProjector } = {};
const emitNativePreToolUseFailure = (failure: CodexNativePreToolUseFailure) => {
emitCodexNativePreToolUseFailureDiagnostic({
agentId: sessionAgentId,
sessionId: params.sessionId,
sessionKey: sandboxSessionKey,
runId: params.runId,
signal: runAbortController.signal,
failure,
...(state.nativePreToolUseFailureFallbackActive
? {
terminalReason:
state.nativePreToolUseFailureFallbackTerminalReason ?? failure.disposition,
}
: {}),
});
};
const flushPendingNativePreToolUseFailures = () => {
for (const failure of pendingNativePreToolUseFailures.splice(0)) {
emitNativePreToolUseFailure(failure);
}
};
const activateNativePreToolUseFailureFallback = () => {
if (!state.nativePreToolUseFailureFallbackActive) {
state.nativePreToolUseFailureFallbackTerminalReason = runAbortController.signal.aborted
? resolveCodexToolAbortTerminalReason(runAbortController.signal)
: undefined;
state.nativePreToolUseFailureFallbackActive = true;
}
flushPendingNativePreToolUseFailures();
};
const releaseSharedClientLeaseOnce = () => {
const release = state.releaseSharedClientLease;
if (!release) {
return;
}
state.releaseSharedClientLease = undefined;
release();
};
const retireSharedCodexClientForOneShotCleanup = async () => {
if (
params.cleanupBundleMcpOnRunEnd !== true ||
state.sharedCodexClientRetiredForOneShotCleanup
) {
return;
}
state.sharedCodexClientRetiredForOneShotCleanup = true;
const retired = retireSharedCodexAppServerClientIfCurrent(state.client);
embeddedAgentLog.info("codex app-server one-shot cleanup retired shared client", {
runId: params.runId,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
activeLeases: retired?.activeLeases ?? null,
closed: retired?.closed ?? false,
matchedSharedClient: Boolean(retired),
});
if (retired?.closed) {
await state.client.closeAndWait({ exitTimeoutMs: 2_000, forceKillDelayMs: 250 });
}
};
const releaseSharedClientLeaseAndRetireOneShotClient = async () => {
releaseSharedClientLeaseOnce();
await retireSharedCodexClientForOneShotCleanup();
};
const releaseSandboxExecEnvironment = async () => {
if (state.sandboxExecEnvironmentAcquired) {
state.sandboxExecEnvironmentAcquired = false;
await releaseCodexSandboxExecServerEnvironment(sandbox);
}
};
const unregisterNativeSubagentMonitor = () => {
state.nativeSubagentMonitor?.unregister();
state.nativeSubagentMonitor = undefined;
};
const registerNativeSubagentMonitor = (parentThreadId: string) => {
unregisterNativeSubagentMonitor();
state.nativeSubagentMonitor = codexNativeSubagentMonitorRuntime.register({
client: state.client,
parentThreadId,
requesterSessionKey: params.sessionKey,
taskRuntimeScope: params.agentHarnessTaskRuntimeScope,
agentId: sessionAgentId,
retainClient: () => retainSharedCodexAppServerClientIfCurrent(state.client),
});
};
const releaseCurrentRoute = () => {
state.detachRouteAbort();
state.detachRouteAbort = () => undefined;
state.turnRoute?.release();
state.turnRoute = undefined;
state.routeActivated = false;
unregisterNativeSubagentMonitor();
};
const startupTimeoutMs = resolveCodexStartupTimeoutMs({
timeoutMs: params.timeoutMs,
timeoutFloorMs: options.startupTimeoutFloorMs,
});
const buildNativeHookRelayFinalConfigPatch = (
decision: { action: "resume"; binding: CodexAppServerThreadBinding } | { action: "start" },
) => {
state.nativeHookRelay?.unregister();
state.nativeHookRelay = createCodexNativeHookRelay({
options: options.nativeHookRelay,
generation:
decision.action === "resume" ? decision.binding.nativeHookRelayGeneration : undefined,
generationMismatchGraceMs:
decision.action === "resume" && !decision.binding.nativeHookRelayGeneration
? CODEX_NATIVE_HOOK_RELAY_TTL_GRACE_MS
: undefined,
events: nativeHookRelayEvents,
agentId: sessionAgentId,
sessionId: params.sessionId,
sessionKey: sandboxSessionKey,
config: params.config,
runId: params.runId,
channelId: hookChannelId,
attemptTimeoutMs: params.timeoutMs,
startupTimeoutMs,
turnStartTimeoutMs: params.timeoutMs,
signal: runAbortController.signal,
onPreToolUseFailure: (failure) => {
const projector = projectorRef.current;
if (projector) {
projector.recordNativeToolPreToolUseFailure(failure);
} else if (state.nativePreToolUseFailureFallbackActive) {
emitNativePreToolUseFailure(failure);
} else {
pendingNativePreToolUseFailures.push(failure);
}
},
});
return {
configPatch: state.nativeHookRelay
? buildCodexNativeHookRelayConfig({
relay: state.nativeHookRelay,
events: nativeHookRelayEvents,
hookTimeoutSec: options.nativeHookRelay?.hookTimeoutSec,
})
: options.nativeHookRelay?.enabled === false
? buildCodexNativeHookRelayDisabledConfig()
: undefined,
nativeHookRelayGeneration: state.nativeHookRelay?.generation,
};
};
return {
prompt,
trajectoryRecorder,
state,
projectorRef,
pendingNativePreToolUseFailures,
markTrajectoryEndRecorded: () => {
state.trajectoryEndRecorded = true;
},
activateNativePreToolUseFailureFallback,
releaseSharedClientLeaseOnce,
releaseSharedClientLeaseAndRetireOneShotClient,
releaseSandboxExecEnvironment,
registerNativeSubagentMonitor,
releaseCurrentRoute,
startupTimeoutMs,
buildNativeHookRelayFinalConfigPatch,
};
}
export type CodexAttemptResources = ReturnType<typeof prepareCodexAttemptResources>;
@@ -0,0 +1,104 @@
import { embeddedAgentLog, formatErrorMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
import type { CodexAttemptNotificationController } from "./run-attempt-notification-controller.js";
import type { CodexAttemptResources } from "./run-attempt-resources.js";
import type { createCodexAttemptServerRequestController } from "./run-attempt-server-requests.js";
import type { CodexAttemptTurnState } from "./run-attempt-turn-state.js";
import type { CodexThreadRouteReservation } from "./turn-router.js";
export async function prepareCodexAttemptRoute(
resources: CodexAttemptResources,
turnRuntime: CodexAttemptTurnState,
notifications: CodexAttemptNotificationController,
handleServerRequest: ReturnType<
typeof createCodexAttemptServerRequestController
>["handleServerRequest"],
) {
const {
prompt,
state: resourceState,
trajectoryRecorder,
releaseCurrentRoute,
registerNativeSubagentMonitor,
activateNativePreToolUseFailureFallback,
releaseSandboxExecEnvironment,
releaseSharedClientLeaseOnce,
} = resources;
const { connection } = prompt.context.runtime;
const { params, runAbortController, abortFromUpstream } = connection;
const { state, turnIdRef, turnWatches } = turnRuntime;
const { noteNotificationReceived, enqueueNotification } = notifications;
const attachRouteAbort = (route: CodexThreadRouteReservation) => {
const onAbort = () => {
if (
state.completed ||
state.terminalTurnNotificationQueued ||
runAbortController.signal.aborted
) {
return;
}
const reasonText = formatErrorMessage(route.signal.reason);
const closedClient = reasonText.includes("turn router closed");
state.clientClosedPromptError = closedClient
? "codex app-server client closed before turn completed"
: `codex app-server turn route closed before turn completed: ${reasonText}`;
state.clientClosedAbort = closedClient;
const activeTurnId = turnIdRef.current;
if (activeTurnId) {
trajectoryRecorder?.recordEvent("turn.client_closed", {
threadId: resourceState.thread.threadId,
turnId: activeTurnId,
});
}
embeddedAgentLog.warn(state.clientClosedPromptError, {
threadId: resourceState.thread.threadId,
turnId: activeTurnId,
});
runAbortController.abort(closedClient ? "client_closed" : "turn_route_closed");
state.completed = true;
turnWatches.clearAllTimers();
state.resolveCompletion?.();
};
route.signal.addEventListener("abort", onAbort, { once: true });
if (route.signal.aborted) {
onAbort();
}
return () => route.signal.removeEventListener("abort", onAbort);
};
const ensureCurrentThreadRoute = async () => {
if (resourceState.turnRoute?.threadId !== resourceState.thread.threadId) {
releaseCurrentRoute();
resourceState.turnRoute = resourceState.turnRouter.reserveThread({
threadId: resourceState.thread.threadId,
releaseOn: runAbortController.signal,
});
}
if (!resourceState.turnRoute) {
throw new Error("codex app-server turn route was not reserved");
}
if (!resourceState.routeActivated) {
if (!resourceState.nativeSubagentMonitor) {
registerNativeSubagentMonitor(resourceState.thread.threadId);
}
resourceState.detachRouteAbort = attachRouteAbort(resourceState.turnRoute);
await resourceState.turnRoute.activate({
onNotificationReceived: noteNotificationReceived,
onNotification: enqueueNotification,
onRequest: handleServerRequest,
});
resourceState.routeActivated = true;
}
return resourceState.turnRoute;
};
try {
await ensureCurrentThreadRoute();
} catch (error) {
activateNativePreToolUseFailureFallback();
releaseCurrentRoute();
resourceState.nativeHookRelay?.unregister();
await releaseSandboxExecEnvironment();
releaseSharedClientLeaseOnce();
params.abortSignal?.removeEventListener("abort", abortFromUpstream);
throw error;
}
return { ensureCurrentThreadRoute };
}
@@ -0,0 +1,211 @@
import {
assertContextEngineHostSupport,
CODEX_APP_SERVER_CONTEXT_ENGINE_HOST,
embeddedAgentLog,
loadCodexBundleMcpThreadConfig,
supportsModelTools,
type EmbeddedRunAttemptParams,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { prepareCodexAppServerAuthBinding } from "./auth-binding.js";
import {
resolveCodexAppServerAuthAccountCacheKey,
resolveCodexAppServerFallbackApiKeyCacheKey,
resolveCodexAppServerPreparedApiKeyCacheKey,
} from "./auth-bridge.js";
import { isCodexSandboxExecServerEnabled } from "./config.js";
import {
resolveCodexAppServerHookChannelId,
shouldEnableCodexAppServerNativeToolSurface,
} from "./dynamic-tool-build.js";
import { resolveCodexProviderWebSearchSupport } from "./provider-capabilities.js";
import type { CodexAttemptConnection } from "./run-attempt-connection.js";
import { resolveCodexAppServerThreadModelSelection } from "./thread-lifecycle.js";
import { resolveCodexWebSearchPlan } from "./web-search.js";
export async function prepareCodexAttemptRuntime(connection: CodexAttemptConnection) {
const {
params,
pluginConfig,
usesSupervisionConnection,
appServer,
startupAuthProfileId,
startupPreparedAuth,
startupClientAuthProfileId,
agentDir,
preDynamicStartupStages,
effectiveWorkspace,
contextSessionKey,
sandboxSessionKey,
sessionAgentId,
sandbox,
attemptClientFactory,
runAbortController,
activeContextEngine,
mutable,
} = connection;
const preparedAuthBinding =
!usesSupervisionConnection && appServer.start.homeScope !== "user" && startupAuthProfileId
? await prepareCodexAppServerAuthBinding({
authProfileId: startupAuthProfileId,
authProfileStore: params.authProfileStore,
agentDir,
config: params.config,
})
: undefined;
const attemptAuthProfileStore = preparedAuthBinding?.authProfileStore ?? params.authProfileStore;
const effectiveContextWindowInfo = usesSupervisionConnection
? undefined
: params.contextWindowInfo;
const effectiveContextTokenBudget = usesSupervisionConnection
? undefined
: params.contextTokenBudget;
const effectiveRuntimeProviderId = usesSupervisionConnection
? (mutable.startupBinding?.modelProvider ?? "codex")
: params.provider;
const effectiveRuntimeModelId = usesSupervisionConnection
? (mutable.startupBinding?.model ?? "codex-native")
: params.modelId;
const {
authProfileId: _outerAuthProfileId,
contextWindowInfo: _outerContextWindowInfo,
contextTokenBudget: _outerContextTokenBudget,
model: _outerModel,
modelId: _outerModelId,
provider: _outerProvider,
runtimePlan: _outerRuntimePlan,
requestedModelId: _outerRequestedModelId,
fallbackReason: _outerFallbackReason,
degradedReason: _outerDegradedReason,
thinkLevel: _outerThinkLevel,
fastMode: _outerFastMode,
...paramsWithoutOuterNativeOwnership
} = params;
const supervisedRuntimeModel = {
id: effectiveRuntimeModelId,
name: effectiveRuntimeModelId,
provider: effectiveRuntimeProviderId,
api: "openai-chatgpt-responses",
reasoning: true,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: undefined,
maxTokens: undefined,
} as unknown as EmbeddedRunAttemptParams["model"];
const runtimeParams: EmbeddedRunAttemptParams = usesSupervisionConnection
? {
...paramsWithoutOuterNativeOwnership,
provider: "codex",
modelId: effectiveRuntimeModelId,
model: supervisedRuntimeModel,
thinkLevel: _outerThinkLevel,
sessionKey: contextSessionKey,
}
: {
...params,
authProfileStore: attemptAuthProfileStore,
sessionKey: contextSessionKey,
...(startupAuthProfileId ? { authProfileId: startupAuthProfileId } : {}),
};
const activeSessionId = params.sessionId;
const activeSessionFile = params.sessionFile;
const buildActiveRunAttemptParams = (): EmbeddedRunAttemptParams => ({
...runtimeParams,
sessionId: activeSessionId,
sessionFile: activeSessionFile,
});
const startupAuthAccountCacheKey = usesSupervisionConnection
? undefined
: startupPreparedAuth?.kind === "api-key"
? resolveCodexAppServerPreparedApiKeyCacheKey(startupPreparedAuth.apiKey)
: startupPreparedAuth?.kind === "profile"
? startupPreparedAuth.snapshot?.secretFreeCacheKey
: await resolveCodexAppServerAuthAccountCacheKey({
authProfileId: startupAuthProfileId,
authProfileStore: attemptAuthProfileStore,
agentDir,
config: params.config,
});
const startupEnvApiKeyCacheKey = usesSupervisionConnection
? undefined
: startupPreparedAuth || startupAuthProfileId
? undefined
: resolveCodexAppServerFallbackApiKeyCacheKey({ startOptions: appServer.start });
preDynamicStartupStages.mark("auth-cache");
const bundleMcpThreadConfig = await loadCodexBundleMcpThreadConfig({
workspaceDir: effectiveWorkspace,
cfg: params.config,
toolsEnabled: usesSupervisionConnection || supportsModelTools(params.model),
disableTools: params.disableTools,
toolsAllow: params.toolsAllow,
});
preDynamicStartupStages.mark("bundle-mcp");
const sandboxExecServerEnabled = isCodexSandboxExecServerEnabled(pluginConfig);
const nativeToolSurfaceEnabled = shouldEnableCodexAppServerNativeToolSurface(
runtimeParams,
sandbox,
{ agentId: sessionAgentId, runtimeSessionKey: sandboxSessionKey, sandboxExecServerEnabled },
);
preDynamicStartupStages.mark("native-tool-surface");
const nativeProviderWebSearchSupport =
resolveCodexWebSearchPlan({
config: params.config,
disableTools: params.disableTools,
nativeToolSurfaceEnabled,
}).kind === "native-hosted"
? await resolveCodexProviderWebSearchSupport({
clientFactory: attemptClientFactory,
appServer,
authProfileId: startupClientAuthProfileId,
preparedAuth: startupPreparedAuth,
agentDir,
config: params.config,
modelProviderOverride: usesSupervisionConnection
? mutable.startupBinding?.modelProvider
: resolveCodexAppServerThreadModelSelection({
provider: params.provider,
model: params.modelId,
binding: mutable.startupBinding,
authProfileId: startupAuthProfileId,
authProfileStore: attemptAuthProfileStore,
agentDir,
config: params.config,
}).modelProvider,
signal: runAbortController.signal,
})
: "unsupported";
preDynamicStartupStages.mark("provider-capabilities");
for (const diagnostic of bundleMcpThreadConfig.diagnostics) {
embeddedAgentLog.warn(`bundle-mcp: ${diagnostic.pluginId}: ${diagnostic.message}`);
}
if (activeContextEngine) {
assertContextEngineHostSupport({
contextEngine: activeContextEngine,
operation: "agent-run",
host: CODEX_APP_SERVER_CONTEXT_ENGINE_HOST,
});
}
const hookChannelId = resolveCodexAppServerHookChannelId(params, sandboxSessionKey);
preDynamicStartupStages.mark("context-engine-support");
return {
connection,
preparedAuthBinding,
runtimeParams,
activeSessionId,
activeSessionFile,
buildActiveRunAttemptParams,
attemptAuthProfileStore,
effectiveContextWindowInfo,
effectiveContextTokenBudget,
effectiveRuntimeProviderId,
effectiveRuntimeModelId,
startupAuthAccountCacheKey,
startupEnvApiKeyCacheKey,
bundleMcpThreadConfig,
sandboxExecServerEnabled,
nativeToolSurfaceEnabled,
nativeProviderWebSearchSupport,
hookChannelId,
};
}
export type CodexAttemptRuntime = Awaited<ReturnType<typeof prepareCodexAttemptRuntime>>;
@@ -0,0 +1,360 @@
import { onInternalDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
import { isCodexAppServerApprovalRequest } from "./client.js";
import { shouldAutoApproveCodexAppServerApprovals } from "./config.js";
import {
emitDynamicToolErrorDiagnostic,
emitDynamicToolStartedDiagnostic,
emitDynamicToolTerminalDiagnostic,
} from "./dynamic-tool-diagnostics.js";
import {
handleDynamicToolCallWithTimeout,
hasPendingDynamicToolTerminalDiagnostic,
isDynamicToolTerminalDiagnosticEvent,
isMatchingDynamicToolTerminalDiagnostic,
resolveDynamicToolCallTimeoutMs,
shouldBlockTerminalReleaseForNonTerminalDynamicToolResult,
toCodexDynamicToolProgressResponse,
toCodexDynamicToolProtocolResponse,
} from "./dynamic-tool-execution.js";
import { handleCodexAppServerElicitationRequest } from "./elicitation-bridge.js";
import { shouldEmitTranscriptToolProgress } from "./event-projector.js";
import { readCodexDynamicToolCallParams } from "./protocol-validators.js";
import type { JsonValue } from "./protocol.js";
import type { CodexAttemptLifecycleController } from "./run-attempt-lifecycle-controller.js";
import { emitCodexAppServerEvent } from "./run-attempt-lifecycle.js";
import type { CodexAttemptResources } from "./run-attempt-resources.js";
import { handleApprovalRequest, toTranscriptToolResult } from "./run-attempt-tools.js";
import type { CodexAttemptTurnState } from "./run-attempt-turn-state.js";
import {
inferCodexDynamicToolMeta,
resolveCodexToolProgressDetailMode,
sanitizeCodexToolArguments,
} from "./tool-progress-normalization.js";
import type { CodexAppServerServerRequest, CodexThreadRouteScope } from "./turn-router.js";
export function createCodexAttemptServerRequestController(
resources: CodexAttemptResources,
turnRuntime: CodexAttemptTurnState,
lifecycle: CodexAttemptLifecycleController,
) {
const { prompt, state: resourceState, projectorRef, trajectoryRecorder } = resources;
const { context } = prompt;
const { runtime, attemptTools } = context;
const { connection } = runtime;
const { params, computerUseConfig, runAbortController, appServer, sessionAgentId } = connection;
const {
toolBridge,
toolOutcomeOrdinals,
suppressedDynamicToolOutcomeOrdinals,
allocateCodexToolOutcomeOrdinal,
} = attemptTools;
const {
state,
turnIdRef,
userInputBridgeRef,
openClawDynamicToolExecutions,
pendingOpenClawDynamicToolCompletionIds,
postToolRawAssistantCompletionIdleTimeoutMs,
turnWatches,
} = turnRuntime;
const {
emitExecutionPhaseOnce,
scheduleTurnReleaseAfterTerminalDynamicTool,
scheduleTerminalDynamicToolReleaseCheck,
} = lifecycle;
const handleServerRequest = async (
request: CodexAppServerServerRequest,
scope: CodexThreadRouteScope,
) => {
const turnId = turnIdRef.current;
const projector = projectorRef.current;
let armCompletionWatchOnResponse = false;
let requestCountsAsTurnActivity = false;
const markCurrentTurnRequestProgress = () => {
state.activeAppServerTurnRequests += 1;
turnWatches.clearCompletionIdleTimer();
turnWatches.disarmAssistantCompletionIdleWatch();
requestCountsAsTurnActivity = true;
turnWatches.touchActivity(`request:${request.method}:start`, { attemptProgress: true });
};
try {
if (!turnId) {
return undefined;
}
if (request.method === "mcpServer/elicitation/request") {
if (!scope.turnId || scope.turnId === turnId) {
armCompletionWatchOnResponse = true;
markCurrentTurnRequestProgress();
}
return await handleCodexAppServerElicitationRequest({
requestParams: request.params,
paramsForRun: params,
threadId: resourceState.thread.threadId,
turnId,
pluginAppPolicyContext: resourceState.thread.pluginAppPolicyContext,
...(computerUseConfig.enabled
? { computerUseMcpServerName: computerUseConfig.mcpServerName }
: {}),
signal: runAbortController.signal,
});
}
if (request.method === "item/tool/requestUserInput") {
if (scope.turnId === turnId) {
armCompletionWatchOnResponse = true;
markCurrentTurnRequestProgress();
}
return userInputBridgeRef.current?.handleRequest({
id: request.id,
params: request.params,
});
}
if (request.method !== "item/tool/call") {
if (isCodexAppServerApprovalRequest(request.method)) {
if (scope.turnId === turnId) {
armCompletionWatchOnResponse = true;
markCurrentTurnRequestProgress();
}
return handleApprovalRequest({
method: request.method,
params: request.params,
paramsForRun: params,
threadId: resourceState.thread.threadId,
turnId,
nativeHookRelay: resourceState.nativeHookRelay,
autoApprove: shouldAutoApproveCodexAppServerApprovals(appServer),
signal: runAbortController.signal,
onNativeToolFailureDisposition: (itemId, disposition) =>
projector?.recordNativeToolApprovalFailure(itemId, disposition),
});
}
return undefined;
}
const call = readCodexDynamicToolCallParams(request.params);
if (!call || call.threadId !== resourceState.thread.threadId || call.turnId !== turnId) {
return undefined;
}
const replayedExecution = openClawDynamicToolExecutions.get(call);
if (replayedExecution) {
armCompletionWatchOnResponse = true;
markCurrentTurnRequestProgress();
state.turnCrossedToolHandoff = true;
return toCodexDynamicToolProtocolResponse(await replayedExecution) as JsonValue;
}
const toolCallOrdinal = allocateCodexToolOutcomeOrdinal?.(call.callId);
armCompletionWatchOnResponse = true;
markCurrentTurnRequestProgress();
state.turnCrossedToolHandoff = true;
pendingOpenClawDynamicToolCompletionIds.add(call.callId);
trajectoryRecorder?.recordEvent("tool.call", {
threadId: call.threadId,
turnId: call.turnId,
toolCallId: call.callId,
name: call.tool,
arguments: call.arguments,
});
projector?.recordDynamicToolCall({
callId: call.callId,
tool: call.tool,
arguments: call.arguments,
});
emitExecutionPhaseOnce(`tool:${call.callId}`, {
phase: "tool_execution_started",
tool: call.tool,
toolCallId: call.callId,
});
emitDynamicToolStartedDiagnostic({
call,
agentId: sessionAgentId,
runId: params.runId,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
});
const toolMeta = inferCodexDynamicToolMeta(
call,
resolveCodexToolProgressDetailMode(params.toolProgressDetail),
);
const toolArgs = sanitizeCodexToolArguments(call.arguments);
const shouldEmitDynamicToolProgress = shouldEmitTranscriptToolProgress(call.tool, toolArgs);
if (shouldEmitDynamicToolProgress) {
void emitCodexAppServerEvent(params, {
stream: "tool",
data: {
phase: "start",
name: call.tool,
toolCallId: call.callId,
...(toolMeta ? { meta: toolMeta } : {}),
...(toolArgs ? { args: toolArgs } : {}),
},
});
}
const dynamicToolTimeoutMs = resolveDynamicToolCallTimeoutMs({ call, config: params.config });
const toolStartedAt = Date.now();
let terminalDiagnosticObserved = false;
const unsubscribeToolDiagnosticObserver = onInternalDiagnosticEvent((event) => {
if (
isDynamicToolTerminalDiagnosticEvent(event) &&
isMatchingDynamicToolTerminalDiagnostic({
event,
call,
runId: params.runId,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
})
) {
terminalDiagnosticObserved = true;
}
});
try {
const { execution } = openClawDynamicToolExecutions.claim(call, () =>
handleDynamicToolCallWithTimeout({
call,
toolBridge,
signal: runAbortController.signal,
timeoutMs: dynamicToolTimeoutMs,
toolCallOrdinal,
onAgentToolResult: params.onAgentToolResult,
onFallbackSelected: () => {
if (toolCallOrdinal !== undefined) {
suppressedDynamicToolOutcomeOrdinals.add(toolCallOrdinal);
}
},
onTimeout: () => {
trajectoryRecorder?.recordEvent("tool.timeout", {
threadId: call.threadId,
turnId: call.turnId,
toolCallId: call.callId,
name: call.tool,
timeoutMs: dynamicToolTimeoutMs,
});
},
}),
);
const response = await execution;
const protocolResponse = toCodexDynamicToolProtocolResponse(response);
if (!protocolResponse.success && toolCallOrdinal !== undefined) {
suppressedDynamicToolOutcomeOrdinals.add(toolCallOrdinal);
params.onToolOutcome?.({
toolName: call.tool,
argsHash: "",
resultHash: "",
toolCallOrdinal,
terminalPresentation: undefined,
presentationOnly: true,
});
}
const toolDurationMs = Math.max(0, Date.now() - toolStartedAt);
trajectoryRecorder?.recordEvent("tool.result", {
threadId: call.threadId,
turnId: call.turnId,
toolCallId: call.callId,
name: call.tool,
success: protocolResponse.success,
contentItems: protocolResponse.contentItems,
});
projector?.recordDynamicToolResult({
callId: call.callId,
tool: call.tool,
asyncStarted: response.asyncStarted === true,
success: protocolResponse.success,
terminalType:
response.diagnosticTerminalType ?? (protocolResponse.success ? "completed" : "error"),
sideEffectEvidence: response.sideEffectEvidence === true,
contentItems: protocolResponse.contentItems,
});
if (shouldEmitDynamicToolProgress) {
const progressResponse = toCodexDynamicToolProgressResponse(response, protocolResponse);
void emitCodexAppServerEvent(params, {
stream: "tool",
data: {
phase: "result",
name: call.tool,
toolCallId: call.callId,
...(toolMeta ? { meta: toolMeta } : {}),
isError: !protocolResponse.success,
result: toTranscriptToolResult(progressResponse),
},
});
}
if (
!terminalDiagnosticObserved &&
!hasPendingDynamicToolTerminalDiagnostic({
call,
runId: params.runId,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
})
) {
emitDynamicToolTerminalDiagnostic({
response,
call,
agentId: sessionAgentId,
runId: params.runId,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
durationMs: toolDurationMs,
});
}
pendingOpenClawDynamicToolCompletionIds.delete(call.callId);
if (response.terminate === true) {
scheduleTurnReleaseAfterTerminalDynamicTool({
call,
response,
durationMs: toolDurationMs,
});
} else if (!shouldBlockTerminalReleaseForNonTerminalDynamicToolResult(response)) {
scheduleTerminalDynamicToolReleaseCheck();
} else {
state.currentTurnHadNonTerminalDynamicToolResult = true;
state.pendingTerminalDynamicToolRelease = undefined;
}
return protocolResponse as JsonValue;
} catch (error) {
pendingOpenClawDynamicToolCompletionIds.delete(call.callId);
if (
!terminalDiagnosticObserved &&
!hasPendingDynamicToolTerminalDiagnostic({
call,
runId: params.runId,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
})
) {
emitDynamicToolErrorDiagnostic({
call,
agentId: sessionAgentId,
runId: params.runId,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
durationMs: Math.max(0, Date.now() - toolStartedAt),
});
}
throw error;
} finally {
toolOutcomeOrdinals.delete(call.callId);
unsubscribeToolDiagnosticObserver();
}
} finally {
if (requestCountsAsTurnActivity) {
state.activeAppServerTurnRequests = Math.max(0, state.activeAppServerTurnRequests - 1);
const postToolContinuationTimeoutMs =
request.method === "item/tool/call" && state.turnCrossedToolHandoff
? postToolRawAssistantCompletionIdleTimeoutMs
: undefined;
turnWatches.touchActivity(`request:${request.method}:response`, {
arm: armCompletionWatchOnResponse,
attemptProgress: true,
...(postToolContinuationTimeoutMs !== undefined
? { attemptTimeoutMs: postToolContinuationTimeoutMs }
: {}),
});
if (armCompletionWatchOnResponse && postToolContinuationTimeoutMs !== undefined) {
turnWatches.armCompletionIdleWatch({ timeoutMs: postToolContinuationTimeoutMs });
}
scheduleTerminalDynamicToolReleaseCheck();
} else {
turnWatches.scheduleProgressWatches();
}
}
};
return { handleServerRequest };
}
@@ -0,0 +1,186 @@
import { embeddedAgentLog } from "openclaw/plugin-sdk/agent-harness-runtime";
import { resolveCodexAppServerForModelProvider } from "./app-server-policy.js";
import { startCodexAttemptThread } from "./attempt-startup.js";
import { flattenCodexDynamicToolFunctions } from "./protocol.js";
import {
emitCodexAppServerEvent,
withCodexAppServerFastModeServiceTier,
} from "./run-attempt-lifecycle.js";
import type { CodexAttemptResources } from "./run-attempt-resources.js";
import { recordCodexTrajectoryContext } from "./trajectory.js";
export async function startCodexAttemptRuntime(resources: CodexAttemptResources) {
const {
prompt,
state,
trajectoryRecorder,
activateNativePreToolUseFailureFallback,
releaseSandboxExecEnvironment,
releaseCurrentRoute,
startupTimeoutMs,
buildNativeHookRelayFinalConfigPatch,
} = resources;
const {
context,
turnState,
buildRenderedCodexDeveloperInstructions,
rebuildCodexTurnPromptTextFromCurrentProjection,
applyNoContextEngineContinuityProjection,
} = prompt;
const { runtime, attemptTools, promptState } = context;
const {
connection,
runtimeParams,
preparedAuthBinding,
buildActiveRunAttemptParams,
startupAuthAccountCacheKey,
startupEnvApiKeyCacheKey,
bundleMcpThreadConfig,
nativeToolSurfaceEnabled,
nativeProviderWebSearchSupport,
sandboxExecServerEnabled,
} = runtime;
const { toolBridge, toolState } = attemptTools;
const {
params,
attemptClientFactory,
bindingStore,
appServer,
pluginConfig,
computerUseConfig,
startupClientAuthProfileId,
runtimeArtifactRequest,
startupPreparedAuth,
agentDir,
sessionAgentId,
effectiveWorkspace,
effectiveCwd,
sandbox,
runAbortController,
usesSupervisionConnection,
resolveReviewerPolicyContext,
resolveRuntimeOptionsForCurrentBinding,
startupAuthProfileId,
abortFromUpstream,
} = connection;
let pluginAppServer = withCodexAppServerFastModeServiceTier(appServer, runtimeParams);
try {
void emitCodexAppServerEvent(params, {
stream: "codex_app_server.lifecycle",
data: { phase: "startup" },
});
const startupResult = await startCodexAttemptThread({
attemptClientFactory,
bindingStore,
appServer: pluginAppServer,
pluginConfig,
computerUseConfig,
startupAuthProfileId: startupClientAuthProfileId,
startupAuthBindingFingerprint: preparedAuthBinding?.fingerprint,
...(runtimeArtifactRequest ? { runtimeArtifactRequest } : {}),
startupPreparedAuth,
startupAuthAccountCacheKey,
startupEnvApiKeyCacheKey,
agentDir,
config: params.config,
buildAttemptParams: buildActiveRunAttemptParams,
sessionAgentId,
effectiveWorkspace,
effectiveCwd,
dynamicTools: toolBridge.specs,
persistentWebSearchAllowed: toolState.persistentWebSearchAllowed,
webSearchAllowed: toolState.webSearchAllowed,
developerInstructions: turnState.promptBuild.developerInstructions,
buildFinalConfigPatch: buildNativeHookRelayFinalConfigPatch,
bundleMcpThreadConfig,
nativeToolSurfaceEnabled,
nativeProviderWebSearchSupport,
sandboxExecServerEnabled,
sandbox,
contextEngineProjection: promptState.contextEngineProjection,
startupTimeoutMs,
signal: runAbortController.signal,
onStartupTimeout: () => runAbortController.abort("codex_startup_timeout"),
spawnedBy: params.spawnedBy,
});
state.client = startupResult.client;
state.thread = startupResult.thread;
state.runtimeArtifact = startupResult.runtimeArtifact;
state.turnRouter = startupResult.turnRouter;
state.turnRoute = startupResult.turnRoute;
pluginAppServer = startupResult.pluginAppServer;
if (
usesSupervisionConnection &&
(state.thread.connectionScope !== "supervision" ||
state.thread.supervisionSourceThreadId !==
connection.mutable.startupBinding?.supervisionSourceThreadId)
) {
throw new Error("Codex supervised thread lost its private connection ownership");
}
if (state.thread.lifecycle.action === "started" || state.thread.lifecycle.action === "forked") {
const activePolicy = resolveReviewerPolicyContext(state.thread);
const activeConfig = resolveRuntimeOptionsForCurrentBinding({
modelProvider: activePolicy.modelProvider,
model: activePolicy.model,
});
const activeAppServer = resolveCodexAppServerForModelProvider({
appServer: activeConfig,
provider: activePolicy.modelProvider,
model: activePolicy.model,
config: params.config,
env: process.env,
agentDir,
});
const previousReviewer = pluginAppServer.approvalsReviewer;
pluginAppServer = {
...pluginAppServer,
approvalsReviewer: activeAppServer.approvalsReviewer,
};
if (pluginAppServer.approvalsReviewer !== previousReviewer) {
embeddedAgentLog.info(
"codex app-server approval reviewer updated from active thread model provider",
{
from: previousReviewer,
to: pluginAppServer.approvalsReviewer,
modelProvider: activePolicy.modelProvider,
},
);
}
}
state.sandboxExecEnvironmentAcquired = Boolean(startupResult.sandboxEnvironment);
state.codexEnvironmentSelection = startupResult.environmentSelection;
state.codexExecutionCwd = startupResult.executionCwd;
state.codexSandboxPolicy = startupResult.sandboxPolicy;
state.releaseSharedClientLease = startupResult.releaseSharedClientLease;
state.restartContextEngineCodexThread = startupResult.restartContextEngineCodexThread;
void emitCodexAppServerEvent(params, {
stream: "codex_app_server.lifecycle",
data: { phase: "thread_ready", threadId: state.thread.threadId },
});
} catch (error) {
activateNativePreToolUseFailureFallback();
releaseCurrentRoute();
state.nativeHookRelay?.unregister();
await releaseSandboxExecEnvironment();
params.abortSignal?.removeEventListener("abort", abortFromUpstream);
throw error;
}
if (applyNoContextEngineContinuityProjection(state.thread.lifecycle.action, state.thread)) {
await rebuildCodexTurnPromptTextFromCurrentProjection();
}
trajectoryRecorder?.recordEvent("session.started", {
sessionFile: params.sessionFile,
threadId: state.thread.threadId,
authProfileId: startupAuthProfileId,
workspaceDir: effectiveWorkspace,
toolCount: flattenCodexDynamicToolFunctions(toolBridge.specs).length,
});
recordCodexTrajectoryContext(trajectoryRecorder, {
attempt: params,
cwd: effectiveCwd,
developerInstructions: buildRenderedCodexDeveloperInstructions(),
prompt: turnState.codexTurnPromptText,
tools: toolBridge.availableSpecs,
});
connection.mutable.pluginAppServer = pluginAppServer;
}
@@ -0,0 +1,187 @@
import {
embeddedAgentLog,
isHostScopedAgentToolActive,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import {
buildDynamicTools,
formatCodexDynamicToolBuildStageSummary,
resolveCodexMessageToolProvider,
shouldWarnCodexDynamicToolBuildStageSummary,
} from "./dynamic-tool-build.js";
import { resolveCodexDynamicToolsLoadingForRuntime } from "./dynamic-tool-profile.js";
import { createCodexDynamicToolBridge } from "./dynamic-tools.js";
import { emitCodexAppServerEvent } from "./run-attempt-lifecycle.js";
import type { CodexAttemptRuntime } from "./run-attempt-runtime.js";
import { resolveCodexDynamicToolDirectNames } from "./run-attempt-tools.js";
export async function prepareCodexAttemptTools(runtime: CodexAttemptRuntime) {
const {
connection,
bundleMcpThreadConfig,
runtimeParams,
effectiveRuntimeModelId,
nativeToolSurfaceEnabled,
nativeProviderWebSearchSupport,
hookChannelId,
} = runtime;
const {
params,
preDynamicStartupStages,
mutable,
startupAuthProfileId,
resolvedWorkspace,
effectiveWorkspace,
effectiveCwd,
sandboxSessionKey,
sandbox,
runAbortController,
sessionAgentId,
pluginConfig,
profilerEnabled,
} = connection;
const preDynamicSummary = preDynamicStartupStages.snapshot();
if (shouldWarnCodexDynamicToolBuildStageSummary(preDynamicSummary)) {
embeddedAgentLog.warn(
`codex app-server pre-dynamic startup timings runId=${params.runId} sessionId=${params.sessionId} totalMs=${preDynamicSummary.totalMs} stages=${formatCodexDynamicToolBuildStageSummary(preDynamicSummary)}`,
{
runId: params.runId,
sessionId: params.sessionId,
totalMs: preDynamicSummary.totalMs,
stages: preDynamicSummary.stages,
hasStartupBinding: Boolean(mutable.startupBinding?.threadId),
startupAuthProfileId: startupAuthProfileId ?? null,
bundleMcpDiagnosticCount: bundleMcpThreadConfig.diagnostics.length,
nativeToolSurfaceEnabled,
},
);
}
const toolState = {
yieldDetected: false,
persistentWebSearchAllowed: undefined as boolean | undefined,
webSearchAllowed: false,
};
const toolOutcomeOrdinals = new Map<string, number>();
const suppressedDynamicToolOutcomeOrdinals = new Set<number>();
const onCodexToolOutcome = params.onToolOutcome
? (observation: Parameters<NonNullable<typeof params.onToolOutcome>>[0]) => {
if (
observation.toolCallOrdinal !== undefined &&
suppressedDynamicToolOutcomeOrdinals.has(observation.toolCallOrdinal)
) {
return;
}
params.onToolOutcome?.(observation);
}
: undefined;
const baseAllocateToolOutcomeOrdinal = params.allocateToolOutcomeOrdinal;
const allocateCodexToolOutcomeOrdinal = baseAllocateToolOutcomeOrdinal
? (toolCallId?: string): number => {
const reservedOrdinal = toolCallId ? toolOutcomeOrdinals.get(toolCallId) : undefined;
if (reservedOrdinal !== undefined) {
return reservedOrdinal;
}
const ordinal = baseAllocateToolOutcomeOrdinal(toolCallId);
if (toolCallId) {
toolOutcomeOrdinals.set(toolCallId, ordinal);
}
return ordinal;
}
: undefined;
const dynamicToolParams =
allocateCodexToolOutcomeOrdinal || onCodexToolOutcome
? {
...runtimeParams,
...(allocateCodexToolOutcomeOrdinal
? { allocateToolOutcomeOrdinal: allocateCodexToolOutcomeOrdinal }
: {}),
...(onCodexToolOutcome ? { onToolOutcome: onCodexToolOutcome } : {}),
}
: runtimeParams;
const computerContextEpoch: {
value: number;
frameToolCallId?: string;
frameImageIdentity?: string;
} = { value: 0 };
const commonToolParams = {
params: dynamicToolParams,
resolvedWorkspace,
effectiveWorkspace,
effectiveCwd,
sandboxSessionKey,
sandbox,
nativeToolSurfaceEnabled,
nativeProviderWebSearchSupport,
runAbortController,
sessionAgentId,
pluginConfig,
profilerEnabled,
onYieldDetected: () => {
toolState.yieldDetected = true;
},
onCodexAppServerEvent: (event: Parameters<typeof emitCodexAppServerEvent>[1]) => {
void emitCodexAppServerEvent(params, event);
},
computerContextEpoch,
};
const tools = await buildDynamicTools({
...commonToolParams,
onPersistentWebSearchPolicyResolved: (allowed) => {
toolState.persistentWebSearchAllowed = allowed;
},
onWebSearchPolicyResolved: (allowed) => {
toolState.webSearchAllowed = allowed;
},
});
const registeredTools = await buildDynamicTools({
...commonToolParams,
forceHeartbeatTool: true,
ignoreDisableMessageTool: true,
ignoreRuntimePlan: true,
});
const toolBridge = createCodexDynamicToolBridge({
tools,
registeredTools,
signal: runAbortController.signal,
computerContextEpoch,
loading: resolveCodexDynamicToolsLoadingForRuntime(pluginConfig, effectiveRuntimeModelId, {
connectionClass: connection.appServer.connectionClass,
}),
directToolNames: resolveCodexDynamicToolDirectNames(
params,
isHostScopedAgentToolActive("crestodian"),
),
hookContext: {
agentId: sessionAgentId,
config: params.config,
workspaceDir: effectiveWorkspace,
sessionId: params.sessionId,
sessionKey: sandboxSessionKey,
runId: params.runId,
channelId: hookChannelId,
currentChannelProvider: resolveCodexMessageToolProvider(params),
currentChannelId: params.currentChannelId,
currentMessagingTarget: params.currentMessagingTarget,
currentMessageId: params.currentMessageId,
currentThreadId: params.currentThreadTs,
replyToMode: params.replyToMode,
hasRepliedRef: params.hasRepliedRef,
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
onToolOutcome: onCodexToolOutcome,
allocateToolOutcomeOrdinal: allocateCodexToolOutcomeOrdinal,
},
});
return {
tools,
registeredTools,
dynamicToolParams,
computerContextEpoch,
toolBridge,
toolState,
toolOutcomeOrdinals,
suppressedDynamicToolOutcomeOrdinals,
onCodexToolOutcome,
allocateCodexToolOutcomeOrdinal,
};
}
export type CodexAttemptTools = Awaited<ReturnType<typeof prepareCodexAttemptTools>>;
@@ -0,0 +1,198 @@
import { embeddedAgentLog, formatErrorMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
import {
CODEX_APP_SERVER_INTERRUPT_TIMEOUT_MS,
interruptCodexTurnBestEffort,
} from "./attempt-client-cleanup.js";
import {
createCodexModelCallDiagnosticEmitter,
utf8JsonByteLength,
} from "./attempt-diagnostics.js";
import { assertCodexTurnStartResponse } from "./protocol-validators.js";
import type { CodexTurnStartResponse } from "./protocol.js";
import { readCodexRateLimitsRevision } from "./rate-limit-cache.js";
import {
emitCodexAppServerEvent,
withCodexAppServerFastModeServiceTier,
} from "./run-attempt-lifecycle.js";
import type { CodexAttemptResources } from "./run-attempt-resources.js";
import type { CodexAttemptTurnState } from "./run-attempt-turn-state.js";
import { buildTurnStartParams } from "./thread-lifecycle.js";
import { buildCodexUserPromptMessage } from "./transcript-mirror.js";
export async function prepareCodexAttemptTurnRequest(
resources: CodexAttemptResources,
turnRuntime: CodexAttemptTurnState,
ensureCurrentThreadRoute: () => Promise<unknown>,
waitForActiveNativeTurnCompletion: () => Promise<boolean>,
) {
const { prompt, state: resourceState, releaseCurrentRoute } = resources;
const { context, turnState, buildRenderedCodexDeveloperInstructions } = prompt;
const { runtime, attemptTools, hookContextWindowFields, workspaceBootstrapContext } = context;
const { connection, runtimeParams, effectiveRuntimeProviderId, effectiveRuntimeModelId } =
runtime;
const { tools } = attemptTools;
const {
params,
usesSupervisionConnection,
codexModelCallId,
codexModelCallTrace,
codexModelContentCapture,
appServer,
runAbortController,
} = connection;
const { state } = turnRuntime;
const buildCodexModelInputMessages = () => [
...prompt.codexModelInputHistoryMessages,
buildCodexUserPromptMessage({ ...runtimeParams, prompt: turnState.codexTurnPromptText }),
];
const codexModelCallDiagnostics = createCodexModelCallDiagnosticEmitter({
baseFields: {
runId: params.runId,
callId: codexModelCallId,
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
sessionId: params.sessionId,
provider: usesSupervisionConnection
? (resourceState.thread.modelProvider ?? effectiveRuntimeProviderId)
: params.provider,
model: usesSupervisionConnection
? (resourceState.thread.model ?? effectiveRuntimeModelId)
: params.modelId,
api: usesSupervisionConnection ? runtimeParams.model.api : params.model.api,
transport: appServer.start.transport,
...hookContextWindowFields,
trace: codexModelCallTrace,
},
capture: codexModelContentCapture,
tools,
buildInputMessages: buildCodexModelInputMessages,
buildSystemPrompt: buildRenderedCodexDeveloperInstructions,
onErrorDiagnostic: (error) => {
embeddedAgentLog.debug("codex app-server model call diagnostic ended with error", {
error: formatErrorMessage(error),
});
},
});
const throwIfTurnStartAcceptedAfterAbort = () => {
if (!runAbortController.signal.aborted) {
return;
}
const reason = runAbortController.signal.reason;
if (reason instanceof Error) {
throw reason;
}
const error = new Error(
typeof reason === "string" && reason.length > 0
? reason
: "codex app-server turn start aborted before acceptance",
);
error.name = "AbortError";
throw error;
};
const startCodexTurn = async (): Promise<CodexTurnStartResponse> => {
const activeTurnRoute = (await ensureCurrentThreadRoute()) as {
armTurn(): void;
cancelTurn(): Promise<void>;
};
const turnAppServer = withCodexAppServerFastModeServiceTier(
connection.mutable.pluginAppServer,
runtimeParams,
);
connection.mutable.pluginAppServer = turnAppServer;
const turnStartParams = buildTurnStartParams(runtimeParams, {
threadId: resourceState.thread.threadId,
cwd: resourceState.codexExecutionCwd,
appServer: turnAppServer,
promptText: turnState.codexTurnPromptText,
sandboxPolicy: resourceState.codexSandboxPolicy,
environmentSelection: resourceState.codexEnvironmentSelection,
...(usesSupervisionConnection
? {}
: { model: resourceState.thread.model, modelProvider: resourceState.thread.modelProvider }),
turnScopedDeveloperInstructions: workspaceBootstrapContext.turnScopedDeveloperInstructions,
skillsCollaborationInstructions: context.skillsCollaborationInstructions,
memoryCollaborationInstructions: workspaceBootstrapContext.memoryCollaborationInstructions,
heartbeatCollaborationInstructions:
workspaceBootstrapContext.heartbeatCollaborationInstructions,
preserveNativeTurnSettings: usesSupervisionConnection,
});
codexModelCallDiagnostics.setRequestPayloadBytes(utf8JsonByteLength(turnStartParams));
state.latestStartupErrorNotification = undefined;
state.rateLimitsRevisionBeforeLastTurnStart = readCodexRateLimitsRevision(resourceState.client);
activeTurnRoute.armTurn();
void emitCodexAppServerEvent(params, {
stream: "codex_app_server.lifecycle",
data: {
phase: "turn_starting",
threadId: resourceState.thread.threadId,
model: turnStartParams.model,
effort: turnStartParams.effort,
collaborationEffort: turnStartParams.collaborationMode?.settings.reasoning_effort,
},
});
let acceptedTurnId: string | undefined;
try {
const startedTurn = assertCodexTurnStartResponse(
await resourceState.client.request("turn/start", turnStartParams, {
timeoutMs: params.timeoutMs,
signal: runAbortController.signal,
}),
);
acceptedTurnId = startedTurn.turn.id;
throwIfTurnStartAcceptedAfterAbort();
return startedTurn;
} catch (error) {
if (acceptedTurnId) {
interruptCodexTurnBestEffort(resourceState.client, {
threadId: resourceState.thread.threadId,
turnId: acceptedTurnId,
timeoutMs: CODEX_APP_SERVER_INTERRUPT_TIMEOUT_MS,
});
releaseCurrentRoute();
} else {
await activeTurnRoute.cancelTurn();
}
throw error;
}
};
if (
resourceState.thread.lifecycle.action === "resumed" &&
(resourceState.thread.lifecycle.activeTurnIds?.length ?? 0) > 0
) {
embeddedAgentLog.info(
"codex app-server resumed thread has active native turn; waiting before turn/start",
{ threadId: resourceState.thread.threadId },
);
void emitCodexAppServerEvent(params, {
stream: "codex_app_server.lifecycle",
data: {
phase: "turn_start_waiting_for_native_turn",
threadId: resourceState.thread.threadId,
},
});
const nativeTurnCompleted = await waitForActiveNativeTurnCompletion();
if (nativeTurnCompleted) {
await resourceState.turnRoute?.drain();
} else if (!runAbortController.signal.aborted) {
embeddedAgentLog.warn(
"codex app-server active native turn did not complete before turn/start wait timed out",
{ threadId: resourceState.thread.threadId },
);
}
}
const buildLlmInputEvent = () => ({
runId: params.runId,
sessionId: params.sessionId,
provider: usesSupervisionConnection
? (resourceState.thread.modelProvider ?? effectiveRuntimeProviderId)
: params.provider,
model: usesSupervisionConnection
? (resourceState.thread.model ?? effectiveRuntimeModelId)
: params.modelId,
systemPrompt: buildRenderedCodexDeveloperInstructions(),
prompt: turnState.codexTurnPromptText,
historyMessages: prompt.codexModelInputHistoryMessages,
imagesCount: params.images?.length ?? 0,
tools,
});
return { codexModelCallDiagnostics, startCodexTurn, buildLlmInputEvent };
}
@@ -0,0 +1,298 @@
import {
embeddedAgentLog,
formatErrorMessage,
runAgentCleanupStep,
runAgentHarnessLlmInputHook,
runAgentHarnessLlmOutputHook,
type EmbeddedRunAttemptResult,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import {
CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS,
unsubscribeCodexThreadBestEffort,
} from "./attempt-client-cleanup.js";
import { classifyCodexModelCallFailureKind } from "./attempt-diagnostics.js";
import {
buildCodexTurnStartFailureResult,
isInvalidCodexImagePayloadError,
} from "./attempt-results.js";
import { isCodexContextRestartSelectionChangedError } from "./attempt-startup.js";
import type { CodexTurnStartResponse } from "./protocol.js";
import { emitCodexAppServerEvent, runCodexAgentEndHook } from "./run-attempt-lifecycle.js";
import type { CodexAttemptNotificationController } from "./run-attempt-notification-controller.js";
import type { CodexAttemptResources } from "./run-attempt-resources.js";
import {
isCodexActiveCompactTurnError,
clearCodexBindingAfterInvalidImagePayload,
shouldUseFreshCodexThreadAfterContextEngineOverflow,
} from "./run-attempt-state.js";
import type { prepareCodexAttemptTurnRequest } from "./run-attempt-turn-request.js";
import type { CodexAttemptTurnState } from "./run-attempt-turn-state.js";
import { buildCodexUserPromptMessage } from "./transcript-mirror.js";
import {
formatCodexTurnStartUsageLimitError,
markCodexAuthProfileBlockedFromRateLimits,
} from "./usage-limit-error.js";
export async function startCodexAttemptTurn(
resources: CodexAttemptResources,
turnRuntime: CodexAttemptTurnState,
notifications: CodexAttemptNotificationController,
requestRuntime: Awaited<ReturnType<typeof prepareCodexAttemptTurnRequest>>,
): Promise<{ result: EmbeddedRunAttemptResult } | { turn: CodexTurnStartResponse }> {
const {
prompt,
state: resourceState,
trajectoryRecorder,
markTrajectoryEndRecorded,
activateNativePreToolUseFailureFallback,
releaseCurrentRoute,
releaseSandboxExecEnvironment,
releaseSharedClientLeaseAndRetireOneShotClient,
} = resources;
const { context, turnState, systemPromptReport } = prompt;
const { runtime, historyState, hookContext, hookContextWindowFields, hookRunner } = context;
const { connection, runtimeParams, effectiveRuntimeProviderId, effectiveRuntimeModelId } =
runtime;
const {
params,
usesSupervisionConnection,
runAbortController,
activeContextEngine,
bindingStore,
bindingIdentity,
appServer,
attemptStartedAt,
startupAuthProfileId,
abortFromUpstream,
} = connection;
const { state, turnIdRef } = turnRuntime;
const { waitForActiveNativeTurnCompletion } = notifications;
const { codexModelCallDiagnostics, startCodexTurn, buildLlmInputEvent } = requestRuntime;
let turn: CodexTurnStartResponse | undefined;
try {
codexModelCallDiagnostics.emitStarted();
runAgentHarnessLlmInputHook({ event: buildLlmInputEvent(), ctx: hookContext, hookRunner });
turn = await startCodexTurn();
} catch (error) {
let turnStartError = error;
if (isCodexActiveCompactTurnError(turnStartError)) {
embeddedAgentLog.info(
"codex app-server turn/start blocked by active compact turn; waiting to retry",
{ threadId: resourceState.thread.threadId },
);
const compactTurnCompleted = await waitForActiveNativeTurnCompletion();
if (compactTurnCompleted && !runAbortController.signal.aborted) {
void emitCodexAppServerEvent(params, {
stream: "codex_app_server.lifecycle",
data: {
phase: "turn_start_retry_after_compact",
threadId: resourceState.thread.threadId,
},
});
try {
turn = await startCodexTurn();
} catch (retryError) {
turnStartError = retryError;
}
}
}
if (
turn === undefined &&
resourceState.thread.connectionScope !== "supervision" &&
shouldUseFreshCodexThreadAfterContextEngineOverflow({
error: turnStartError,
contextEngineActive: Boolean(activeContextEngine),
thread: resourceState.thread,
}) &&
resourceState.restartContextEngineCodexThread
) {
embeddedAgentLog.warn(
"codex app-server context-engine turn overflowed on resume; retrying with fresh thread",
{ threadId: resourceState.thread.threadId, error: formatErrorMessage(turnStartError) },
);
try {
const clearedBinding = await bindingStore.mutate(bindingIdentity, {
kind: "clear",
threadId: resourceState.thread.threadId,
});
if (!clearedBinding) {
embeddedAgentLog.warn(
"codex app-server preserved newer context-engine binding after resume overflow; skipping fresh retry",
{ threadId: resourceState.thread.threadId, error: formatErrorMessage(turnStartError) },
);
} else {
resourceState.thread = await resourceState.restartContextEngineCodexThread();
const retryBinding = await bindingStore.read(bindingIdentity);
if (
retryBinding &&
retryBinding.threadId === resourceState.thread.threadId &&
retryBinding.contextEngine?.projection
) {
await bindingStore.mutate(bindingIdentity, {
kind: "patch",
threadId: retryBinding.threadId,
patch: {
contextEngine: { ...retryBinding.contextEngine, projection: undefined },
},
});
embeddedAgentLog.info(
"codex app-server cleared stale context-engine projection after overflow retry",
{
threadId: resourceState.thread.threadId,
previousEpoch: retryBinding.contextEngine.projection.epoch,
},
);
}
void emitCodexAppServerEvent(params, {
stream: "codex_app_server.lifecycle",
data: { phase: "thread_ready_retry", threadId: resourceState.thread.threadId },
});
try {
turn = await startCodexTurn();
} catch (retryError) {
turnStartError = retryError;
}
}
} catch (retrySetupError) {
turnStartError = retrySetupError;
}
}
if (turn === undefined) {
const usageLimitError = await formatCodexTurnStartUsageLimitError({
client: resourceState.client,
error: turnStartError,
errorNotification: state.latestStartupErrorNotification,
rateLimitsRevisionBeforeTurnStart: state.rateLimitsRevisionBeforeLastTurnStart,
timeoutMs: appServer.requestTimeoutMs,
signal: runAbortController.signal,
});
const message = usageLimitError?.message ?? formatErrorMessage(turnStartError);
if (isInvalidCodexImagePayloadError(message)) {
await clearCodexBindingAfterInvalidImagePayload(bindingStore, bindingIdentity, {
phase: "turn_start",
threadId: resourceState.thread.threadId,
error: message,
});
}
void emitCodexAppServerEvent(params, {
stream: "codex_app_server.lifecycle",
data: { phase: "turn_start_failed", error: message },
});
trajectoryRecorder?.recordEvent("session.ended", {
status: "error",
threadId: resourceState.thread.threadId,
timedOut: state.timedOut,
aborted: runAbortController.signal.aborted,
promptError: message,
});
markTrajectoryEndRecorded();
runAgentHarnessLlmOutputHook({
event: {
runId: params.runId,
sessionId: params.sessionId,
provider: usesSupervisionConnection
? (resourceState.thread.modelProvider ?? effectiveRuntimeProviderId)
: params.provider,
model: usesSupervisionConnection
? (resourceState.thread.model ?? effectiveRuntimeModelId)
: params.modelId,
...hookContextWindowFields,
resolvedRef: usesSupervisionConnection
? `${resourceState.thread.modelProvider ?? effectiveRuntimeProviderId}/${resourceState.thread.model ?? effectiveRuntimeModelId}`
: (params.runtimePlan?.observability.resolvedRef ??
`${params.provider}/${params.modelId}`),
...(!usesSupervisionConnection && params.runtimePlan?.observability.harnessId
? { harnessId: params.runtimePlan.observability.harnessId }
: {}),
assistantTexts: [],
},
ctx: hookContext,
hookRunner,
});
const failureKind = classifyCodexModelCallFailureKind({
error: turnStartError,
timedOut: state.timedOut,
turnCompletionIdleTimedOut: state.turnCompletionIdleTimedOut,
runAborted: runAbortController.signal.aborted,
abortReason: runAbortController.signal.reason,
clientClosedAbort: state.clientClosedAbort,
formatError: formatErrorMessage,
});
codexModelCallDiagnostics.emitError(message, failureKind ? { failureKind } : {});
const messagesSnapshot = [
...historyState.messages,
buildCodexUserPromptMessage({ ...runtimeParams, prompt: turnState.codexTurnPromptText }),
];
await runCodexAgentEndHook(params, {
event: {
messages: messagesSnapshot,
success: false,
error: message,
durationMs: Date.now() - attemptStartedAt,
},
ctx: hookContext,
hookRunner,
});
if (!state.timedOut) {
await unsubscribeCodexThreadBestEffort(resourceState.client, {
threadId: resourceState.thread.threadId,
timeoutMs: CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS,
});
}
releaseCurrentRoute();
activateNativePreToolUseFailureFallback();
resourceState.nativeHookRelay?.unregister();
await releaseSandboxExecEnvironment();
await runAgentCleanupStep({
runId: params.runId,
sessionId: params.sessionId,
step: "codex-trajectory-flush-startup-failure",
log: embeddedAgentLog,
cleanup: async () => trajectoryRecorder?.flush(),
});
params.abortSignal?.removeEventListener("abort", abortFromUpstream);
await releaseSharedClientLeaseAndRetireOneShotClient();
if (usageLimitError) {
await markCodexAuthProfileBlockedFromRateLimits({
params,
authProfileId: startupAuthProfileId,
rateLimits: usageLimitError.rateLimitsForProfile,
});
return {
result: buildCodexTurnStartFailureResult({
params,
message: usageLimitError.message,
messagesSnapshot,
systemPromptReport,
}),
};
}
if (isCodexContextRestartSelectionChangedError(turnStartError)) {
return {
result: {
...buildCodexTurnStartFailureResult({
params,
message,
messagesSnapshot,
systemPromptReport,
}),
codexAppServerFailure: {
kind: "client_closed_before_turn_completed" as const,
transport: appServer.start.transport,
threadId: resourceState.thread.threadId,
replaySafe: true,
},
},
};
}
throw turnStartError;
}
}
if (!turn) {
activateNativePreToolUseFailureFallback();
await releaseSharedClientLeaseAndRetireOneShotClient();
throw new Error("codex app-server turn/start failed without an error");
}
turnIdRef.current = turn.turn.id;
return { turn };
}
@@ -0,0 +1,207 @@
import { emitTrustedDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
import {
CODEX_APP_SERVER_INTERRUPT_TIMEOUT_MS,
interruptCodexTurnBestEffort,
} from "./attempt-client-cleanup.js";
import { createCodexSteeringQueue } from "./attempt-steering.js";
import {
resolveCodexPostToolRawAssistantCompletionIdleTimeoutMs,
resolveCodexTurnAssistantCompletionIdleTimeoutMs,
resolveCodexTurnCompletionIdleTimeoutMs,
resolveCodexTurnTerminalIdleTimeoutMs,
} from "./attempt-timeouts.js";
import {
createCodexAttemptTurnWatchController,
type CodexAttemptTurnWatchTimeoutKind,
} from "./attempt-turn-watches.js";
import {
resolveCodexNativeHookRelayTtlMs,
CODEX_NATIVE_HOOK_RELAY_TTL_GRACE_MS,
} from "./native-hook-relay.js";
import type {
CodexServerNotification,
CodexDynamicToolCallParams,
CodexDynamicToolCallResponse,
} from "./protocol.js";
import type { CodexAttemptResources } from "./run-attempt-resources.js";
import { createCodexDynamicToolExecutionRegistry } from "./run-attempt-tools.js";
import { createCodexUserInputBridge } from "./user-input-bridge.js";
const CODEX_NATIVE_HOOK_RELAY_RENEW_INTERVAL_MS = 60_000;
export function createCodexAttemptTurnState(resources: CodexAttemptResources) {
const {
prompt,
state: resourceState,
projectorRef,
trajectoryRecorder,
startupTimeoutMs,
} = resources;
const { context } = prompt;
const { connection } = context.runtime;
const { params, options, appServer, runAbortController } = connection;
const state = {
latestStartupErrorNotification: undefined as CodexServerNotification | undefined,
rateLimitsRevisionBeforeLastTurnStart: undefined as number | undefined,
completed: false,
terminalTurnNotificationQueued: false,
timedOut: false,
turnCompletionIdleTimedOut: false,
turnWatchTimeoutKind: undefined as CodexAttemptTurnWatchTimeoutKind | undefined,
turnWatchTimeoutIdleMs: undefined as number | undefined,
turnWatchTimeoutMs: undefined as number | undefined,
turnWatchTimeoutLastActivityReason: undefined as string | undefined,
turnWatchTimeoutDetails: undefined as Record<string, unknown> | undefined,
turnCompletionIdleTimeoutMessage: undefined as string | undefined,
clientClosedPromptError: undefined as string | undefined,
clientClosedAbort: false,
shouldDelayNativeHookRelayUnregister: false,
lifecycleStarted: false,
lifecycleTerminalEmitted: false,
resolveCompletion: undefined as (() => void) | undefined,
nativeHookRelayLastRenewedAt: 0,
activeAppServerTurnRequests: 0,
unsettledFinalizationHookCount: 0,
rejectedFinalizationHookAssistant: undefined as { itemId?: string } | undefined,
turnCrossedToolHandoff: false,
pendingTerminalDynamicToolRelease: undefined as
| {
call: CodexDynamicToolCallParams;
response: CodexDynamicToolCallResponse;
durationMs: number;
}
| undefined,
terminalDynamicToolReleaseCheckScheduled: false,
currentTurnHadNonTerminalDynamicToolResult: false,
};
const completion = new Promise<void>((resolve) => {
state.resolveCompletion = resolve;
});
const turnCompletionIdleTimeoutMs = resolveCodexTurnCompletionIdleTimeoutMs(
options.turnCompletionIdleTimeoutMs ?? appServer.turnCompletionIdleTimeoutMs,
);
const turnAssistantCompletionIdleTimeoutMs = resolveCodexTurnAssistantCompletionIdleTimeoutMs(
options.turnAssistantCompletionIdleTimeoutMs,
);
const postToolRawAssistantCompletionIdleTimeoutMs =
resolveCodexPostToolRawAssistantCompletionIdleTimeoutMs(
options.postToolRawAssistantCompletionIdleTimeoutMs ??
appServer.postToolRawAssistantCompletionIdleTimeoutMs,
turnAssistantCompletionIdleTimeoutMs,
);
const turnTerminalIdleTimeoutMs = resolveCodexTurnTerminalIdleTimeoutMs(
options.turnTerminalIdleTimeoutMs,
params.runTimeoutOverrideMs,
);
const turnAttemptIdleTimeoutMs = Math.max(100, Math.floor(params.timeoutMs));
const pendingOpenClawDynamicToolCompletionIds = new Set<string>();
// One execution promise per call id prevents duplicate delivery from
// repeating non-idempotent computer input while the attempt remains active.
const openClawDynamicToolExecutions = createCodexDynamicToolExecutionRegistry();
const activeTurnItemIds = new Set<string>();
const activeCompletionBlockerItemIds = new Set<string>();
const activeFinalizationHookRunIds = new Set<string>();
const finalizationHookBatchStatuses = new Map<string, string | undefined>();
const turnIdRef: { current?: string } = {};
const userInputBridgeRef: { current?: ReturnType<typeof createCodexUserInputBridge> } = {};
const steeringQueueRef: { current?: ReturnType<typeof createCodexSteeringQueue> } = {};
const renewNativeHookRelayForTurnProgress = () => {
if (!resourceState.nativeHookRelay || options.nativeHookRelay?.ttlMs !== undefined) {
return;
}
const now = Date.now();
const renewsRecently =
now - state.nativeHookRelayLastRenewedAt < CODEX_NATIVE_HOOK_RELAY_RENEW_INTERVAL_MS;
const expiresSoon =
now >= resourceState.nativeHookRelay.expiresAtMs - CODEX_NATIVE_HOOK_RELAY_TTL_GRACE_MS;
if (renewsRecently && !expiresSoon) {
return;
}
state.nativeHookRelayLastRenewedAt = now;
resourceState.nativeHookRelay.renew(
resolveCodexNativeHookRelayTtlMs({
explicitTtlMs: undefined,
attemptTimeoutMs: turnAttemptIdleTimeoutMs,
startupTimeoutMs,
turnStartTimeoutMs: params.timeoutMs,
}),
);
};
const turnWatches = createCodexAttemptTurnWatchController({
threadId: resourceState.thread.threadId,
signal: runAbortController.signal,
getTurnId: () => turnIdRef.current,
isCompleted: () => state.completed,
isTerminalTurnNotificationQueued: () => state.terminalTurnNotificationQueued,
getActiveAppServerTurnRequests: () => state.activeAppServerTurnRequests,
getActiveTurnItemCount: () => activeTurnItemIds.size,
getActiveCompletionBlockerItemCount: () => activeCompletionBlockerItemIds.size,
getActiveFinalizationHookCount: () => state.unsettledFinalizationHookCount,
canReleaseAssistantCompletionIdle: () =>
projectorRef.current?.hasLatestTerminalAssistantCandidateText() === true,
turnCompletionIdleTimeoutMs,
turnAssistantCompletionIdleTimeoutMs,
turnAttemptIdleTimeoutMs,
turnTerminalIdleTimeoutMs,
interruptTimeoutMs: CODEX_APP_SERVER_INTERRUPT_TIMEOUT_MS,
onInterruptTurn: (input) => interruptCodexTurnBestEffort(resourceState.client, input),
onTimeout: (timeout) => {
state.timedOut = true;
state.turnCompletionIdleTimedOut = true;
state.turnWatchTimeoutKind = timeout.kind;
state.turnWatchTimeoutIdleMs = timeout.idleMs;
state.turnWatchTimeoutMs = timeout.timeoutMs;
state.turnWatchTimeoutLastActivityReason = timeout.lastActivityReason;
state.turnWatchTimeoutDetails = timeout.details;
state.turnCompletionIdleTimeoutMessage =
"codex app-server turn idle timed out waiting for turn/completed";
},
onMarkTimedOut: () => projectorRef.current?.markTimedOut(),
onAbort: (reason) => runAbortController.abort(reason),
onCompleted: () => {
state.completed = true;
},
onResolveCompletion: () => state.resolveCompletion?.(),
onRecordEvent: (name, fields) => trajectoryRecorder?.recordEvent(name, fields),
onAttemptProgress: (reason) => {
renewNativeHookRelayForTurnProgress();
params.onRunProgress?.({
reason,
provider: params.provider,
model: params.modelId,
backend: "codex-app-server",
});
},
onProgressDiagnostic: (reason) => {
emitTrustedDiagnosticEvent({
type: "run.progress",
runId: params.runId,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
reason: `codex_app_server:${reason}`,
});
},
});
return {
state,
completion,
turnCompletionIdleTimeoutMs,
turnAssistantCompletionIdleTimeoutMs,
postToolRawAssistantCompletionIdleTimeoutMs,
turnTerminalIdleTimeoutMs,
turnAttemptIdleTimeoutMs,
pendingOpenClawDynamicToolCompletionIds,
openClawDynamicToolExecutions,
activeTurnItemIds,
activeCompletionBlockerItemIds,
activeFinalizationHookRunIds,
finalizationHookBatchStatuses,
turnIdRef,
userInputBridgeRef,
steeringQueueRef,
renewNativeHookRelayForTurnProgress,
turnWatches,
};
}
export type CodexAttemptTurnState = ReturnType<typeof createCodexAttemptTurnState>;
@@ -0,0 +1,29 @@
import type {
EmbeddedRunAttemptParams,
NativeHookRelayEvent,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import type { CodexAppServerBindingStore } from "./session-binding.js";
import type { CodexAppServerClientFactory } from "./shared-client.js";
export type CodexRunAttemptOptions = {
bindingStore: CodexAppServerBindingStore;
pluginConfig?: unknown;
startupTimeoutFloorMs?: number;
nativeHookRelay?: {
enabled?: boolean;
events?: readonly NativeHookRelayEvent[];
ttlMs?: number;
gatewayTimeoutMs?: number;
hookTimeoutSec?: number;
};
turnCompletionIdleTimeoutMs?: number;
turnAssistantCompletionIdleTimeoutMs?: number;
postToolRawAssistantCompletionIdleTimeoutMs?: number;
turnTerminalIdleTimeoutMs?: number;
clientFactory?: CodexAppServerClientFactory;
};
export type CodexRunAttemptInput = {
params: EmbeddedRunAttemptParams;
options: CodexRunAttemptOptions;
};
File diff suppressed because it is too large Load Diff