@gakr-gakr/codex 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/autobot.plugin.json +374 -0
- package/doctor-contract-api.ts +68 -0
- package/harness.ts +72 -0
- package/index.ts +124 -0
- package/media-understanding-provider.ts +521 -0
- package/package.json +40 -0
- package/prompt-overlay.ts +21 -0
- package/provider-catalog.ts +83 -0
- package/provider-discovery.ts +45 -0
- package/provider.ts +243 -0
- package/src/app-server/app-inventory-cache.ts +324 -0
- package/src/app-server/approval-bridge.ts +1211 -0
- package/src/app-server/auth-bridge.ts +614 -0
- package/src/app-server/capabilities.ts +27 -0
- package/src/app-server/client-factory.ts +24 -0
- package/src/app-server/client.ts +715 -0
- package/src/app-server/compact.ts +512 -0
- package/src/app-server/computer-use.ts +683 -0
- package/src/app-server/config.ts +1038 -0
- package/src/app-server/context-engine-projection.ts +403 -0
- package/src/app-server/dynamic-tool-diagnostics.ts +73 -0
- package/src/app-server/dynamic-tool-profile.ts +70 -0
- package/src/app-server/dynamic-tools.ts +623 -0
- package/src/app-server/elicitation-bridge.ts +783 -0
- package/src/app-server/event-projector.ts +2065 -0
- package/src/app-server/image-payload-sanitizer.ts +167 -0
- package/src/app-server/local-runtime-attribution.ts +39 -0
- package/src/app-server/managed-binary.ts +193 -0
- package/src/app-server/models.ts +172 -0
- package/src/app-server/native-hook-relay.ts +150 -0
- package/src/app-server/native-subagent-task-mirror.ts +497 -0
- package/src/app-server/plugin-activation.ts +283 -0
- package/src/app-server/plugin-app-cache-key.ts +74 -0
- package/src/app-server/plugin-approval-roundtrip.ts +122 -0
- package/src/app-server/plugin-inventory.ts +357 -0
- package/src/app-server/plugin-thread-config.ts +455 -0
- package/src/app-server/protocol-generated/json/DynamicToolCallParams.json +33 -0
- package/src/app-server/protocol-generated/json/v2/ErrorNotification.json +199 -0
- package/src/app-server/protocol-generated/json/v2/GetAccountResponse.json +102 -0
- package/src/app-server/protocol-generated/json/v2/ModelListResponse.json +227 -0
- package/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json +2630 -0
- package/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json +2630 -0
- package/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json +1659 -0
- package/src/app-server/protocol-generated/json/v2/TurnStartResponse.json +1655 -0
- package/src/app-server/protocol-validators.ts +203 -0
- package/src/app-server/protocol.ts +520 -0
- package/src/app-server/rate-limit-cache.ts +48 -0
- package/src/app-server/rate-limits.ts +583 -0
- package/src/app-server/request.ts +73 -0
- package/src/app-server/run-attempt.ts +4862 -0
- package/src/app-server/session-binding.ts +398 -0
- package/src/app-server/session-history.ts +44 -0
- package/src/app-server/shared-client.ts +289 -0
- package/src/app-server/side-question.ts +1009 -0
- package/src/app-server/test-support.ts +48 -0
- package/src/app-server/thread-lifecycle.ts +959 -0
- package/src/app-server/timeout.ts +9 -0
- package/src/app-server/tool-progress-normalization.ts +77 -0
- package/src/app-server/trajectory.ts +368 -0
- package/src/app-server/transcript-mirror.ts +208 -0
- package/src/app-server/transport-stdio.ts +107 -0
- package/src/app-server/transport-websocket.ts +90 -0
- package/src/app-server/transport.ts +117 -0
- package/src/app-server/user-input-bridge.ts +316 -0
- package/src/app-server/version.ts +4 -0
- package/src/app-server/vision-tools.ts +12 -0
- package/src/command-account.ts +544 -0
- package/src/command-formatters.ts +426 -0
- package/src/command-handlers.ts +2021 -0
- package/src/command-plugins-management.ts +137 -0
- package/src/command-rpc.ts +142 -0
- package/src/commands.ts +65 -0
- package/src/conversation-binding-data.ts +124 -0
- package/src/conversation-binding.ts +561 -0
- package/src/conversation-control.ts +303 -0
- package/src/conversation-turn-collector.ts +186 -0
- package/src/conversation-turn-input.ts +106 -0
- package/src/migration/apply.ts +501 -0
- package/src/migration/helpers.ts +55 -0
- package/src/migration/plan.ts +461 -0
- package/src/migration/provider.ts +41 -0
- package/src/migration/source.ts +643 -0
- package/src/migration/targets.ts +25 -0
- package/src/node-cli-sessions.ts +711 -0
- package/test-api.ts +95 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,4862 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { Dirent } from "node:fs";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
assembleHarnessContextEngine,
|
|
7
|
+
bootstrapHarnessContextEngine,
|
|
8
|
+
buildAgentHookContextChannelFields,
|
|
9
|
+
buildHarnessContextEngineRuntimeContext,
|
|
10
|
+
buildHarnessContextEngineRuntimeContextFromUsage,
|
|
11
|
+
buildEmbeddedAttemptToolRunContext,
|
|
12
|
+
clearActiveEmbeddedRun,
|
|
13
|
+
compactContextEngineWithSafetyTimeout,
|
|
14
|
+
embeddedAgentLog,
|
|
15
|
+
emitAgentEvent as emitGlobalAgentEvent,
|
|
16
|
+
finalizeHarnessContextEngineTurn,
|
|
17
|
+
formatErrorMessage,
|
|
18
|
+
hasBeforeToolCallPolicy,
|
|
19
|
+
isActiveHarnessContextEngine,
|
|
20
|
+
isSubagentSessionKey,
|
|
21
|
+
loadCodexBundleMcpThreadConfig,
|
|
22
|
+
normalizeAgentRuntimeTools,
|
|
23
|
+
resolveAttemptSpawnWorkspaceDir,
|
|
24
|
+
resolveAgentHarnessBeforePromptBuildResult,
|
|
25
|
+
resolveCompactionTimeoutMs,
|
|
26
|
+
resolveModelAuthMode,
|
|
27
|
+
resolveContextEngineOwnerPluginId,
|
|
28
|
+
resolveSandboxContext,
|
|
29
|
+
resolveSessionAgentIds,
|
|
30
|
+
resolveUserPath,
|
|
31
|
+
runAgentHarnessAgentEndHook,
|
|
32
|
+
runAgentHarnessLlmInputHook,
|
|
33
|
+
runAgentHarnessLlmOutputHook,
|
|
34
|
+
runHarnessContextEngineMaintenance,
|
|
35
|
+
registerNativeHookRelay,
|
|
36
|
+
resolveBootstrapContextForRun,
|
|
37
|
+
setActiveEmbeddedRun,
|
|
38
|
+
supportsModelTools,
|
|
39
|
+
hasSandboxBindContainerPathAliases,
|
|
40
|
+
hasSandboxBindReadonlyHostShadows,
|
|
41
|
+
resolveWritableSandboxBindHostRoots,
|
|
42
|
+
runAgentCleanupStep,
|
|
43
|
+
type AgentMessage,
|
|
44
|
+
type EmbeddedRunAttemptParams,
|
|
45
|
+
type EmbeddedRunAttemptResult,
|
|
46
|
+
type EmbeddedContextFile,
|
|
47
|
+
type ContextEngineProjection,
|
|
48
|
+
type NativeHookRelayEvent,
|
|
49
|
+
type NativeHookRelayRegistrationHandle,
|
|
50
|
+
} from "autobot/plugin-sdk/agent-harness-runtime";
|
|
51
|
+
import { markAuthProfileBlockedUntil, resolveAgentDir } from "autobot/plugin-sdk/agent-runtime";
|
|
52
|
+
import {
|
|
53
|
+
emitTrustedDiagnosticEvent,
|
|
54
|
+
hasPendingInternalDiagnosticEvent,
|
|
55
|
+
onInternalDiagnosticEvent,
|
|
56
|
+
type DiagnosticEventPayload,
|
|
57
|
+
} from "autobot/plugin-sdk/diagnostic-runtime";
|
|
58
|
+
import { pathExists } from "autobot/plugin-sdk/security-runtime";
|
|
59
|
+
import { defaultCodexAppInventoryCache } from "./app-inventory-cache.js";
|
|
60
|
+
import { handleCodexAppServerApprovalRequest } from "./approval-bridge.js";
|
|
61
|
+
import {
|
|
62
|
+
refreshCodexAppServerAuthTokens,
|
|
63
|
+
resolveCodexAppServerAuthAccountCacheKey,
|
|
64
|
+
resolveCodexAppServerEnvApiKeyCacheKey,
|
|
65
|
+
resolveCodexAppServerHomeDir,
|
|
66
|
+
resolveCodexAppServerAuthProfileId,
|
|
67
|
+
resolveCodexAppServerAuthProfileIdForAgent,
|
|
68
|
+
} from "./auth-bridge.js";
|
|
69
|
+
import { CODEX_CONTROL_METHODS } from "./capabilities.js";
|
|
70
|
+
import {
|
|
71
|
+
defaultCodexAppServerClientFactory,
|
|
72
|
+
type CodexAppServerClientFactory,
|
|
73
|
+
} from "./client-factory.js";
|
|
74
|
+
import {
|
|
75
|
+
isCodexAppServerApprovalRequest,
|
|
76
|
+
isCodexAppServerConnectionClosedError,
|
|
77
|
+
type CodexAppServerClient,
|
|
78
|
+
} from "./client.js";
|
|
79
|
+
import { ensureCodexComputerUse } from "./computer-use.js";
|
|
80
|
+
import {
|
|
81
|
+
isCodexAppServerApprovalPolicyAllowedByRequirements,
|
|
82
|
+
readCodexPluginConfig,
|
|
83
|
+
resolveCodexPluginsPolicy,
|
|
84
|
+
resolveCodexAppServerRuntimeOptions,
|
|
85
|
+
withMcpElicitationsApprovalPolicy,
|
|
86
|
+
type CodexAppServerRuntimeOptions,
|
|
87
|
+
type CodexPluginConfig,
|
|
88
|
+
} from "./config.js";
|
|
89
|
+
import {
|
|
90
|
+
projectContextEngineAssemblyForCodex,
|
|
91
|
+
resolveCodexContextEngineProjectionMaxChars,
|
|
92
|
+
resolveCodexContextEngineProjectionReserveTokens,
|
|
93
|
+
} from "./context-engine-projection.js";
|
|
94
|
+
import {
|
|
95
|
+
emitDynamicToolErrorDiagnostic,
|
|
96
|
+
emitDynamicToolStartedDiagnostic,
|
|
97
|
+
emitDynamicToolTerminalDiagnostic,
|
|
98
|
+
} from "./dynamic-tool-diagnostics.js";
|
|
99
|
+
import {
|
|
100
|
+
filterCodexDynamicTools,
|
|
101
|
+
isForcedPrivateQaCodexRuntime,
|
|
102
|
+
normalizeCodexDynamicToolName,
|
|
103
|
+
resolveCodexDynamicToolsLoading,
|
|
104
|
+
} from "./dynamic-tool-profile.js";
|
|
105
|
+
import { createCodexDynamicToolBridge, type CodexDynamicToolBridge } from "./dynamic-tools.js";
|
|
106
|
+
import { handleCodexAppServerElicitationRequest } from "./elicitation-bridge.js";
|
|
107
|
+
import {
|
|
108
|
+
CodexAppServerEventProjector,
|
|
109
|
+
shouldEmitTranscriptToolProgress,
|
|
110
|
+
} from "./event-projector.js";
|
|
111
|
+
import {
|
|
112
|
+
buildCodexNativeHookRelayDisabledConfig,
|
|
113
|
+
buildCodexNativeHookRelayConfig,
|
|
114
|
+
CODEX_NATIVE_HOOK_RELAY_EVENTS,
|
|
115
|
+
} from "./native-hook-relay.js";
|
|
116
|
+
import { buildCodexPluginAppCacheKey } from "./plugin-app-cache-key.js";
|
|
117
|
+
import {
|
|
118
|
+
buildCodexPluginThreadConfig,
|
|
119
|
+
buildCodexPluginThreadConfigInputFingerprint,
|
|
120
|
+
mergeCodexThreadConfigs,
|
|
121
|
+
shouldBuildCodexPluginThreadConfig,
|
|
122
|
+
} from "./plugin-thread-config.js";
|
|
123
|
+
import {
|
|
124
|
+
assertCodexTurnStartResponse,
|
|
125
|
+
readCodexDynamicToolCallParams,
|
|
126
|
+
} from "./protocol-validators.js";
|
|
127
|
+
import {
|
|
128
|
+
type CodexUserInput,
|
|
129
|
+
isJsonObject,
|
|
130
|
+
type CodexSandboxPolicy,
|
|
131
|
+
type CodexServerNotification,
|
|
132
|
+
type CodexDynamicToolSpec,
|
|
133
|
+
type CodexDynamicToolCallParams,
|
|
134
|
+
type CodexDynamicToolCallResponse,
|
|
135
|
+
type CodexThreadItem,
|
|
136
|
+
type CodexTurnStartResponse,
|
|
137
|
+
type JsonObject,
|
|
138
|
+
type JsonValue,
|
|
139
|
+
} from "./protocol.js";
|
|
140
|
+
import { readRecentCodexRateLimits, rememberCodexRateLimits } from "./rate-limit-cache.js";
|
|
141
|
+
import {
|
|
142
|
+
formatCodexUsageLimitErrorMessage,
|
|
143
|
+
resolveCodexUsageLimitResetAtMs,
|
|
144
|
+
shouldRefreshCodexRateLimitsForUsageLimitMessage,
|
|
145
|
+
} from "./rate-limits.js";
|
|
146
|
+
import {
|
|
147
|
+
clearCodexAppServerBinding,
|
|
148
|
+
readCodexAppServerBinding,
|
|
149
|
+
type CodexAppServerThreadBinding,
|
|
150
|
+
} from "./session-binding.js";
|
|
151
|
+
import { readCodexMirroredSessionHistoryMessages } from "./session-history.js";
|
|
152
|
+
import { clearSharedCodexAppServerClientIfCurrent } from "./shared-client.js";
|
|
153
|
+
import {
|
|
154
|
+
areCodexDynamicToolFingerprintsCompatible,
|
|
155
|
+
buildDeveloperInstructions,
|
|
156
|
+
buildContextEngineBinding,
|
|
157
|
+
buildTurnStartParams,
|
|
158
|
+
codexDynamicToolsFingerprint,
|
|
159
|
+
isContextEngineBindingCompatible,
|
|
160
|
+
startOrResumeThread,
|
|
161
|
+
type CodexAppServerThreadLifecycleBinding,
|
|
162
|
+
type CodexContextEngineThreadBootstrapProjection,
|
|
163
|
+
} from "./thread-lifecycle.js";
|
|
164
|
+
import {
|
|
165
|
+
inferCodexDynamicToolMeta,
|
|
166
|
+
resolveCodexToolProgressDetailMode,
|
|
167
|
+
sanitizeCodexToolArguments,
|
|
168
|
+
sanitizeCodexToolResponse,
|
|
169
|
+
} from "./tool-progress-normalization.js";
|
|
170
|
+
import {
|
|
171
|
+
createCodexTrajectoryRecorder,
|
|
172
|
+
normalizeCodexTrajectoryError,
|
|
173
|
+
recordCodexTrajectoryCompletion,
|
|
174
|
+
recordCodexTrajectoryContext,
|
|
175
|
+
} from "./trajectory.js";
|
|
176
|
+
import {
|
|
177
|
+
buildCodexUserPromptMessage,
|
|
178
|
+
mirrorCodexAppServerTranscript,
|
|
179
|
+
} from "./transcript-mirror.js";
|
|
180
|
+
import { createCodexUserInputBridge } from "./user-input-bridge.js";
|
|
181
|
+
import { filterToolsForVisionInputs } from "./vision-tools.js";
|
|
182
|
+
|
|
183
|
+
const CODEX_DYNAMIC_TOOL_TIMEOUT_MS = 30_000;
|
|
184
|
+
const CODEX_DYNAMIC_TOOL_MAX_TIMEOUT_MS = 600_000;
|
|
185
|
+
const CODEX_DYNAMIC_IMAGE_GENERATION_TOOL_TIMEOUT_MS = 120_000;
|
|
186
|
+
const CODEX_DYNAMIC_IMAGE_TOOL_TIMEOUT_MS = 60_000;
|
|
187
|
+
const CODEX_APP_SERVER_STARTUP_CONNECTION_CLOSE_MAX_ATTEMPTS = 3;
|
|
188
|
+
const CODEX_APP_SERVER_STARTUP_TIMEOUT_FLOOR_MS = 100;
|
|
189
|
+
const CODEX_APP_SERVER_INTERRUPT_TIMEOUT_MS = 5_000;
|
|
190
|
+
const CODEX_USAGE_LIMIT_RATE_LIMIT_REFRESH_TIMEOUT_MS = 5_000;
|
|
191
|
+
const CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS = 60_000;
|
|
192
|
+
const CODEX_TURN_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS = 10_000;
|
|
193
|
+
const CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS = 30 * 60_000;
|
|
194
|
+
const CODEX_NATIVE_HOOK_RELAY_MIN_TTL_MS = 30 * 60_000;
|
|
195
|
+
const CODEX_NATIVE_HOOK_RELAY_TTL_GRACE_MS = 5 * 60_000;
|
|
196
|
+
const CODEX_NATIVE_HOOK_RELAY_RENEW_INTERVAL_MS = 60_000;
|
|
197
|
+
const CODEX_STEER_ALL_DEBOUNCE_MS = 500;
|
|
198
|
+
const LOG_FIELD_MAX_LENGTH = 160;
|
|
199
|
+
const CODEX_NATIVE_PROJECT_DOC_BASENAMES = new Set(["agents.md"]);
|
|
200
|
+
const CODEX_WORKSPACE_DEVELOPER_CONTEXT_BASENAMES = new Set([
|
|
201
|
+
"identity.md",
|
|
202
|
+
"soul.md",
|
|
203
|
+
"tools.md",
|
|
204
|
+
"user.md",
|
|
205
|
+
]);
|
|
206
|
+
const CODEX_HEARTBEAT_CONTEXT_BASENAME = "heartbeat.md";
|
|
207
|
+
const CODEX_NATIVE_HOOK_RELAY_EVENTS_WITH_APP_SERVER_APPROVALS =
|
|
208
|
+
CODEX_NATIVE_HOOK_RELAY_EVENTS.filter((event) => event !== "permission_request");
|
|
209
|
+
const CODEX_BOOTSTRAP_CONTEXT_ORDER = new Map<string, number>([
|
|
210
|
+
["soul.md", 10],
|
|
211
|
+
["identity.md", 20],
|
|
212
|
+
["user.md", 30],
|
|
213
|
+
["tools.md", 40],
|
|
214
|
+
["bootstrap.md", 50],
|
|
215
|
+
["memory.md", 60],
|
|
216
|
+
["heartbeat.md", 70],
|
|
217
|
+
]);
|
|
218
|
+
|
|
219
|
+
type AutoBotCodingToolsOptions = NonNullable<
|
|
220
|
+
Parameters<(typeof import("autobot/plugin-sdk/agent-harness"))["createAutoBotCodingTools"]>[0]
|
|
221
|
+
>;
|
|
222
|
+
type AutoBotCodingToolsFactory =
|
|
223
|
+
(typeof import("autobot/plugin-sdk/agent-harness"))["createAutoBotCodingTools"];
|
|
224
|
+
type AutoBotDynamicTool = ReturnType<AutoBotCodingToolsFactory>[number];
|
|
225
|
+
type CodexBootstrapContext = Awaited<ReturnType<typeof resolveBootstrapContextForRun>>;
|
|
226
|
+
type CodexBootstrapFile = CodexBootstrapContext["bootstrapFiles"][number];
|
|
227
|
+
type CodexSystemPromptReport = NonNullable<EmbeddedRunAttemptResult["systemPromptReport"]>;
|
|
228
|
+
type CodexToolReportEntry = CodexSystemPromptReport["tools"]["entries"][number];
|
|
229
|
+
type CodexWorkspaceBootstrapContext = CodexBootstrapContext & {
|
|
230
|
+
promptContextFiles?: EmbeddedContextFile[];
|
|
231
|
+
developerInstructionFiles?: EmbeddedContextFile[];
|
|
232
|
+
heartbeatReferenceFiles?: EmbeddedContextFile[];
|
|
233
|
+
promptContext?: string;
|
|
234
|
+
developerInstructions?: string;
|
|
235
|
+
heartbeatCollaborationInstructions?: string;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
let autoBotCodingToolsFactoryForTests: AutoBotCodingToolsFactory | undefined;
|
|
239
|
+
|
|
240
|
+
function emitCodexAppServerEvent(
|
|
241
|
+
params: EmbeddedRunAttemptParams,
|
|
242
|
+
event: Parameters<NonNullable<EmbeddedRunAttemptParams["onAgentEvent"]>>[0],
|
|
243
|
+
): void {
|
|
244
|
+
try {
|
|
245
|
+
emitGlobalAgentEvent({
|
|
246
|
+
runId: params.runId,
|
|
247
|
+
stream: event.stream,
|
|
248
|
+
data: event.data,
|
|
249
|
+
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
|
|
250
|
+
});
|
|
251
|
+
} catch (error) {
|
|
252
|
+
embeddedAgentLog.debug("codex app-server global agent event emit failed", { error });
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
const maybePromise = params.onAgentEvent?.(event);
|
|
256
|
+
void Promise.resolve(maybePromise).catch((error: unknown) => {
|
|
257
|
+
embeddedAgentLog.debug("codex app-server agent event handler rejected", { error });
|
|
258
|
+
});
|
|
259
|
+
} catch (error) {
|
|
260
|
+
// Event consumers are observational; they must not abort or strand the
|
|
261
|
+
// canonical app-server turn lifecycle.
|
|
262
|
+
embeddedAgentLog.debug("codex app-server agent event handler threw", { error });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function collectTerminalAssistantText(result: EmbeddedRunAttemptResult): string {
|
|
267
|
+
return result.assistantTexts.join("\n\n").trim();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
type CodexSteeringQueueOptions = {
|
|
271
|
+
debounceMs?: number;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
type DynamicToolTimeoutDetails = {
|
|
275
|
+
responseMessage: string;
|
|
276
|
+
consoleMessage: string;
|
|
277
|
+
meta: Record<string, unknown>;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
function normalizeLogField(value: unknown): string | undefined {
|
|
281
|
+
if (typeof value !== "string") {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
const normalized = value
|
|
285
|
+
.replaceAll(String.fromCharCode(27), " ")
|
|
286
|
+
.replaceAll("\r", " ")
|
|
287
|
+
.replaceAll("\n", " ")
|
|
288
|
+
.replaceAll("\t", " ")
|
|
289
|
+
.trim();
|
|
290
|
+
if (!normalized) {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
return normalized.length > LOG_FIELD_MAX_LENGTH
|
|
294
|
+
? `${normalized.slice(0, LOG_FIELD_MAX_LENGTH - 3)}...`
|
|
295
|
+
: normalized;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function readNumericTimeoutMs(value: unknown): number | undefined {
|
|
299
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
300
|
+
return Math.max(0, Math.floor(value));
|
|
301
|
+
}
|
|
302
|
+
if (typeof value === "string") {
|
|
303
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
304
|
+
if (Number.isFinite(parsed)) {
|
|
305
|
+
return Math.max(0, Math.floor(parsed));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function formatDynamicToolTimeoutDetails(params: {
|
|
312
|
+
call: CodexDynamicToolCallParams;
|
|
313
|
+
timeoutMs: number;
|
|
314
|
+
}): DynamicToolTimeoutDetails {
|
|
315
|
+
const tool = normalizeLogField(params.call.tool) ?? "unknown";
|
|
316
|
+
const baseMeta: Record<string, unknown> = {
|
|
317
|
+
tool: params.call.tool,
|
|
318
|
+
toolCallId: params.call.callId,
|
|
319
|
+
threadId: params.call.threadId,
|
|
320
|
+
turnId: params.call.turnId,
|
|
321
|
+
timeoutMs: params.timeoutMs,
|
|
322
|
+
timeoutKind: "codex_dynamic_tool_rpc",
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
if (tool !== "process" || !isJsonObject(params.call.arguments)) {
|
|
326
|
+
return {
|
|
327
|
+
responseMessage: `AutoBot dynamic tool call timed out after ${params.timeoutMs}ms while running tool ${tool}.`,
|
|
328
|
+
consoleMessage: `codex dynamic tool timeout: tool=${tool} toolTimeoutMs=${params.timeoutMs}; per-tool-call watchdog, not session idle`,
|
|
329
|
+
meta: baseMeta,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const action = normalizeLogField(params.call.arguments.action);
|
|
334
|
+
const sessionId = normalizeLogField(params.call.arguments.sessionId);
|
|
335
|
+
const requestedTimeoutMs = readNumericTimeoutMs(params.call.arguments.timeout);
|
|
336
|
+
const actionPart = action ? ` action=${action}` : "";
|
|
337
|
+
const sessionPart = sessionId ? ` sessionId=${sessionId}` : "";
|
|
338
|
+
const requestedPart =
|
|
339
|
+
requestedTimeoutMs === undefined ? "" : ` requestedWaitMs=${requestedTimeoutMs}`;
|
|
340
|
+
const retryHint =
|
|
341
|
+
action === "poll"
|
|
342
|
+
? "; repeated lines usually mean process-poll retry churn, not model progress"
|
|
343
|
+
: "";
|
|
344
|
+
const responseTarget =
|
|
345
|
+
action || sessionId
|
|
346
|
+
? ` while waiting for process${actionPart}${sessionPart}`
|
|
347
|
+
: " while waiting for the process tool";
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
responseMessage: `AutoBot dynamic tool call timed out after ${params.timeoutMs}ms${responseTarget}. This is a tool RPC timeout, not a session idle timeout.`,
|
|
351
|
+
consoleMessage: `codex process tool timeout:${actionPart}${sessionPart} toolTimeoutMs=${params.timeoutMs}${requestedPart}; per-tool-call watchdog, not session idle${retryHint}`,
|
|
352
|
+
meta: {
|
|
353
|
+
...baseMeta,
|
|
354
|
+
processAction: action,
|
|
355
|
+
processSessionId: sessionId,
|
|
356
|
+
processRequestedTimeoutMs: requestedTimeoutMs,
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function createCodexSteeringQueue(params: {
|
|
362
|
+
client: CodexAppServerClient;
|
|
363
|
+
threadId: string;
|
|
364
|
+
turnId: string;
|
|
365
|
+
answerPendingUserInput: (text: string) => boolean;
|
|
366
|
+
signal: AbortSignal;
|
|
367
|
+
}) {
|
|
368
|
+
type PendingSteerText = {
|
|
369
|
+
text: string;
|
|
370
|
+
resolve: () => void;
|
|
371
|
+
reject: (error: unknown) => void;
|
|
372
|
+
};
|
|
373
|
+
let batchedTexts: PendingSteerText[] = [];
|
|
374
|
+
let batchTimer: NodeJS.Timeout | undefined;
|
|
375
|
+
let sendChain: Promise<void> = Promise.resolve();
|
|
376
|
+
|
|
377
|
+
const clearBatchTimer = () => {
|
|
378
|
+
if (batchTimer) {
|
|
379
|
+
clearTimeout(batchTimer);
|
|
380
|
+
batchTimer = undefined;
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const sendTexts = async (texts: string[]) => {
|
|
385
|
+
if (texts.length === 0) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (params.signal.aborted) {
|
|
389
|
+
throw new Error("codex app-server steering queue aborted");
|
|
390
|
+
}
|
|
391
|
+
await params.client.request("turn/steer", {
|
|
392
|
+
threadId: params.threadId,
|
|
393
|
+
expectedTurnId: params.turnId,
|
|
394
|
+
input: texts.map(toCodexTextInput),
|
|
395
|
+
});
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const enqueueSend = (texts: string[]) => {
|
|
399
|
+
const send = sendChain.then(() => sendTexts(texts));
|
|
400
|
+
sendChain = send.catch((error: unknown) => {
|
|
401
|
+
embeddedAgentLog.debug("codex app-server queued steer failed", { error });
|
|
402
|
+
});
|
|
403
|
+
return send;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const flushBatch = () => {
|
|
407
|
+
clearBatchTimer();
|
|
408
|
+
const items = batchedTexts;
|
|
409
|
+
batchedTexts = [];
|
|
410
|
+
const send = enqueueSend(items.map((item) => item.text));
|
|
411
|
+
void send.then(
|
|
412
|
+
() => {
|
|
413
|
+
for (const item of items) {
|
|
414
|
+
item.resolve();
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
(error: unknown) => {
|
|
418
|
+
for (const item of items) {
|
|
419
|
+
item.reject(error);
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
);
|
|
423
|
+
return send;
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
async queue(text: string, options?: CodexSteeringQueueOptions) {
|
|
428
|
+
if (params.answerPendingUserInput(text)) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
return await new Promise<void>((resolve, reject) => {
|
|
432
|
+
batchedTexts.push({ text, resolve, reject });
|
|
433
|
+
clearBatchTimer();
|
|
434
|
+
const debounceMs = normalizeCodexSteerDebounceMs(options?.debounceMs);
|
|
435
|
+
batchTimer = setTimeout(() => {
|
|
436
|
+
batchTimer = undefined;
|
|
437
|
+
void flushBatch().catch(() => undefined);
|
|
438
|
+
}, debounceMs);
|
|
439
|
+
});
|
|
440
|
+
},
|
|
441
|
+
async flushPending() {
|
|
442
|
+
await flushBatch().catch(() => undefined);
|
|
443
|
+
},
|
|
444
|
+
cancel() {
|
|
445
|
+
clearBatchTimer();
|
|
446
|
+
const items = batchedTexts;
|
|
447
|
+
batchedTexts = [];
|
|
448
|
+
for (const item of items) {
|
|
449
|
+
item.reject(new Error("codex app-server steering queue cancelled"));
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function normalizeCodexSteerDebounceMs(value: number | undefined): number {
|
|
456
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
457
|
+
? Math.floor(value)
|
|
458
|
+
: CODEX_STEER_ALL_DEBOUNCE_MS;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function toCodexTextInput(text: string): CodexUserInput {
|
|
462
|
+
return { type: "text", text, text_elements: [] };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
type AutoBotSandboxContext = Awaited<ReturnType<typeof resolveSandboxContext>>;
|
|
466
|
+
|
|
467
|
+
function resolveCodexAppServerSandboxPolicyForAutoBotSandbox(
|
|
468
|
+
appServer: CodexAppServerRuntimeOptions,
|
|
469
|
+
sandbox: AutoBotSandboxContext,
|
|
470
|
+
cwd: string,
|
|
471
|
+
): CodexSandboxPolicy | undefined {
|
|
472
|
+
if (!sandbox?.enabled || appServer.sandbox === "read-only") {
|
|
473
|
+
return undefined;
|
|
474
|
+
}
|
|
475
|
+
const networkAccess = codexNetworkAccessForAutoBotSandbox(sandbox);
|
|
476
|
+
const writableRoots = new Set([cwd]);
|
|
477
|
+
if (sandbox.backendId === "docker") {
|
|
478
|
+
for (const root of resolveWritableSandboxBindHostRoots(sandbox.docker.binds)) {
|
|
479
|
+
writableRoots.add(root);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
// Codex app-server still runs on the Gateway host, so keep Codex's
|
|
483
|
+
// filesystem sandbox while mirroring the AutoBot sandbox egress policy.
|
|
484
|
+
return {
|
|
485
|
+
type: "workspaceWrite",
|
|
486
|
+
writableRoots: [...writableRoots],
|
|
487
|
+
networkAccess,
|
|
488
|
+
excludeTmpdirEnvVar: false,
|
|
489
|
+
excludeSlashTmp: false,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function codexNetworkAccessForAutoBotSandbox(sandbox: AutoBotSandboxContext): boolean {
|
|
494
|
+
if (!sandbox?.enabled || sandbox.backendId !== "docker") {
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
return sandbox.docker.network.trim().toLowerCase() !== "none";
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function resolveCodexAppServerForAutoBotToolPolicy(params: {
|
|
501
|
+
appServer: CodexAppServerRuntimeOptions;
|
|
502
|
+
pluginConfig: CodexPluginConfig;
|
|
503
|
+
env: NodeJS.ProcessEnv;
|
|
504
|
+
shouldPromote: boolean;
|
|
505
|
+
canUseUntrustedApprovalPolicy: boolean;
|
|
506
|
+
}): CodexAppServerRuntimeOptions {
|
|
507
|
+
if (
|
|
508
|
+
!params.shouldPromote ||
|
|
509
|
+
!params.canUseUntrustedApprovalPolicy ||
|
|
510
|
+
params.appServer.approvalPolicy !== "never"
|
|
511
|
+
) {
|
|
512
|
+
return params.appServer;
|
|
513
|
+
}
|
|
514
|
+
const explicitMode =
|
|
515
|
+
params.pluginConfig.appServer?.mode !== undefined ||
|
|
516
|
+
isCodexAppServerPolicyMode(params.env.AUTOBOT_CODEX_APP_SERVER_MODE);
|
|
517
|
+
const explicitApprovalPolicy =
|
|
518
|
+
params.pluginConfig.appServer?.approvalPolicy !== undefined ||
|
|
519
|
+
isCodexAppServerApprovalPolicy(params.env.AUTOBOT_CODEX_APP_SERVER_APPROVAL_POLICY);
|
|
520
|
+
if (explicitMode || explicitApprovalPolicy) {
|
|
521
|
+
return params.appServer;
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
...params.appServer,
|
|
525
|
+
approvalPolicy: "untrusted",
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function isCodexAppServerPolicyMode(value: unknown): boolean {
|
|
530
|
+
return value === "guardian" || value === "yolo";
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function isCodexAppServerApprovalPolicy(value: unknown): boolean {
|
|
534
|
+
return (
|
|
535
|
+
value === "never" || value === "on-request" || value === "on-failure" || value === "untrusted"
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const CODEX_APP_SERVER_NATIVE_THREAD_MAX_TOKENS = 70_000;
|
|
540
|
+
const CODEX_APP_SERVER_BYTE_UNITS: Record<string, number> = {
|
|
541
|
+
b: 1,
|
|
542
|
+
k: 1024,
|
|
543
|
+
kb: 1024,
|
|
544
|
+
kib: 1024,
|
|
545
|
+
m: 1024 * 1024,
|
|
546
|
+
mb: 1024 * 1024,
|
|
547
|
+
mib: 1024 * 1024,
|
|
548
|
+
g: 1024 * 1024 * 1024,
|
|
549
|
+
gb: 1024 * 1024 * 1024,
|
|
550
|
+
gib: 1024 * 1024 * 1024,
|
|
551
|
+
t: 1024 * 1024 * 1024 * 1024,
|
|
552
|
+
tb: 1024 * 1024 * 1024 * 1024,
|
|
553
|
+
tib: 1024 * 1024 * 1024 * 1024,
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
function parseCodexAppServerByteLimit(value: unknown): number | undefined {
|
|
557
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
558
|
+
return Math.floor(value);
|
|
559
|
+
}
|
|
560
|
+
if (typeof value !== "string") {
|
|
561
|
+
return undefined;
|
|
562
|
+
}
|
|
563
|
+
const match = value.trim().match(/^(\d+(?:\.\d+)?)\s*([a-z]+)?$/i);
|
|
564
|
+
if (!match) {
|
|
565
|
+
return undefined;
|
|
566
|
+
}
|
|
567
|
+
const amount = Number(match[1]);
|
|
568
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
const unit = (match[2] ?? "b").toLowerCase();
|
|
572
|
+
const multiplier = CODEX_APP_SERVER_BYTE_UNITS[unit];
|
|
573
|
+
if (multiplier === undefined) {
|
|
574
|
+
return undefined;
|
|
575
|
+
}
|
|
576
|
+
return Math.max(1, Math.round(amount * multiplier));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async function listCodexAppServerRolloutFilesForThread(
|
|
580
|
+
agentDir: string,
|
|
581
|
+
threadId: string,
|
|
582
|
+
codexHome?: string,
|
|
583
|
+
): Promise<Array<{ path: string; bytes: number }>> {
|
|
584
|
+
const resolvedAgentDir = path.resolve(agentDir);
|
|
585
|
+
const resolvedCodexHome = codexHome?.trim()
|
|
586
|
+
? path.resolve(codexHome)
|
|
587
|
+
: resolveCodexAppServerHomeDir(resolvedAgentDir);
|
|
588
|
+
const roots = [
|
|
589
|
+
path.join(resolvedCodexHome, "sessions"),
|
|
590
|
+
path.join(resolveCodexAppServerHomeDir(resolvedAgentDir), "sessions"),
|
|
591
|
+
path.join(resolvedAgentDir, "agent", "codex-home", "sessions"),
|
|
592
|
+
path.join(path.dirname(resolvedAgentDir), "codex-home", "sessions"),
|
|
593
|
+
];
|
|
594
|
+
const files: Array<{ path: string; bytes: number }> = [];
|
|
595
|
+
const visited = new Set<string>();
|
|
596
|
+
for (const root of roots) {
|
|
597
|
+
if (visited.has(root)) {
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
visited.add(root);
|
|
601
|
+
const stack = [root];
|
|
602
|
+
while (stack.length > 0) {
|
|
603
|
+
const dir = stack.pop();
|
|
604
|
+
if (!dir) {
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
let entries: Dirent[];
|
|
608
|
+
try {
|
|
609
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
610
|
+
} catch {
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
for (const entry of entries) {
|
|
614
|
+
const file = path.join(dir, entry.name);
|
|
615
|
+
if (entry.isDirectory()) {
|
|
616
|
+
stack.push(file);
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl") || !entry.name.includes(threadId)) {
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
try {
|
|
623
|
+
files.push({ path: file, bytes: (await fs.stat(file)).size });
|
|
624
|
+
} catch {
|
|
625
|
+
// Ignore rollout files that disappeared while the guard was scanning.
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return files;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
async function readCodexSessionRecordForSessionFile(
|
|
634
|
+
sessionFile: string,
|
|
635
|
+
): Promise<(Record<string, unknown> & { sessionKey: string }) | undefined> {
|
|
636
|
+
const sessionsFile = path.join(path.dirname(sessionFile), "sessions.json");
|
|
637
|
+
let store: JsonValue | undefined;
|
|
638
|
+
try {
|
|
639
|
+
store = JSON.parse(await fs.readFile(sessionsFile, "utf8")) as JsonValue;
|
|
640
|
+
} catch {
|
|
641
|
+
return undefined;
|
|
642
|
+
}
|
|
643
|
+
if (!isJsonObject(store)) {
|
|
644
|
+
return undefined;
|
|
645
|
+
}
|
|
646
|
+
const resolvedSessionFile = path.resolve(sessionFile);
|
|
647
|
+
for (const [sessionKey, record] of Object.entries(store)) {
|
|
648
|
+
if (!isJsonObject(record) || typeof record.sessionFile !== "string") {
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (path.resolve(record.sessionFile) !== resolvedSessionFile) {
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
return { sessionKey, ...record };
|
|
655
|
+
}
|
|
656
|
+
return undefined;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async function readCodexAppServerRolloutTokenUsage(file: string): Promise<number | undefined> {
|
|
660
|
+
let handle: Awaited<ReturnType<typeof fs.open>>;
|
|
661
|
+
try {
|
|
662
|
+
handle = await fs.open(file, "r");
|
|
663
|
+
} catch {
|
|
664
|
+
return undefined;
|
|
665
|
+
}
|
|
666
|
+
let totalTokens: number | undefined;
|
|
667
|
+
try {
|
|
668
|
+
for await (const line of handle.readLines()) {
|
|
669
|
+
const lineTokens = readCodexAppServerRolloutTokenUsageLine(line);
|
|
670
|
+
if (lineTokens !== undefined) {
|
|
671
|
+
totalTokens = lineTokens;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
} finally {
|
|
675
|
+
await handle.close();
|
|
676
|
+
}
|
|
677
|
+
return totalTokens;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function readCodexAppServerRolloutTokenUsageLine(line: string): number | undefined {
|
|
681
|
+
if (!line.trim()) {
|
|
682
|
+
return undefined;
|
|
683
|
+
}
|
|
684
|
+
try {
|
|
685
|
+
const parsed = JSON.parse(line) as JsonValue;
|
|
686
|
+
const payload = isJsonObject(parsed) ? parsed.payload : undefined;
|
|
687
|
+
const info =
|
|
688
|
+
isJsonObject(payload) && payload.type === "token_count" && isJsonObject(payload.info)
|
|
689
|
+
? payload.info
|
|
690
|
+
: undefined;
|
|
691
|
+
if (!info) {
|
|
692
|
+
return undefined;
|
|
693
|
+
}
|
|
694
|
+
const usage = isJsonObject(info.last_token_usage)
|
|
695
|
+
? info.last_token_usage
|
|
696
|
+
: isJsonObject(info.total_token_usage)
|
|
697
|
+
? info.total_token_usage
|
|
698
|
+
: undefined;
|
|
699
|
+
const value = usage?.total_tokens ?? usage?.totalTokens;
|
|
700
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
701
|
+
} catch {
|
|
702
|
+
return undefined;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function maxFiniteNumber(values: Array<number | undefined>): number | undefined {
|
|
707
|
+
const nums = values.filter(
|
|
708
|
+
(value): value is number => typeof value === "number" && Number.isFinite(value),
|
|
709
|
+
);
|
|
710
|
+
if (nums.length === 0) {
|
|
711
|
+
return undefined;
|
|
712
|
+
}
|
|
713
|
+
return Math.max(...nums);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function rotateOversizedCodexAppServerStartupBinding(params: {
|
|
717
|
+
binding: CodexAppServerThreadBinding | undefined;
|
|
718
|
+
sessionFile: string;
|
|
719
|
+
agentDir: string;
|
|
720
|
+
codexHome?: string;
|
|
721
|
+
config: EmbeddedRunAttemptParams["config"] | undefined;
|
|
722
|
+
}): Promise<CodexAppServerThreadBinding | undefined> {
|
|
723
|
+
const binding = params.binding;
|
|
724
|
+
if (!binding?.threadId) {
|
|
725
|
+
return binding;
|
|
726
|
+
}
|
|
727
|
+
if (params.config?.agents?.defaults?.compaction?.truncateAfterCompaction !== true) {
|
|
728
|
+
return binding;
|
|
729
|
+
}
|
|
730
|
+
const sessionRecord = await readCodexSessionRecordForSessionFile(params.sessionFile);
|
|
731
|
+
const maxBytes = parseCodexAppServerByteLimit(
|
|
732
|
+
params.config?.agents?.defaults?.compaction?.maxActiveTranscriptBytes,
|
|
733
|
+
);
|
|
734
|
+
const rolloutFiles = await listCodexAppServerRolloutFilesForThread(
|
|
735
|
+
params.agentDir,
|
|
736
|
+
binding.threadId,
|
|
737
|
+
params.codexHome,
|
|
738
|
+
);
|
|
739
|
+
if (maxBytes !== undefined) {
|
|
740
|
+
const oversizedFiles = rolloutFiles.filter((file) => file.bytes >= maxBytes);
|
|
741
|
+
if (oversizedFiles.length > 0) {
|
|
742
|
+
embeddedAgentLog.warn(
|
|
743
|
+
"codex app-server native transcript exceeded active byte limit; starting a fresh thread",
|
|
744
|
+
{
|
|
745
|
+
threadId: binding.threadId,
|
|
746
|
+
maxBytes,
|
|
747
|
+
files: oversizedFiles.map((file) => ({ path: file.path, bytes: file.bytes })),
|
|
748
|
+
},
|
|
749
|
+
);
|
|
750
|
+
await clearCodexAppServerBinding(params.sessionFile);
|
|
751
|
+
return undefined;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
const nativeTokens = maxFiniteNumber(
|
|
755
|
+
await Promise.all(
|
|
756
|
+
rolloutFiles.map(async (file) => readCodexAppServerRolloutTokenUsage(file.path)),
|
|
757
|
+
),
|
|
758
|
+
);
|
|
759
|
+
const sessionTokens =
|
|
760
|
+
sessionRecord?.totalTokensFresh !== false &&
|
|
761
|
+
typeof sessionRecord?.totalTokens === "number" &&
|
|
762
|
+
Number.isFinite(sessionRecord.totalTokens)
|
|
763
|
+
? sessionRecord.totalTokens
|
|
764
|
+
: undefined;
|
|
765
|
+
const tokenCount = maxFiniteNumber([sessionTokens, nativeTokens]);
|
|
766
|
+
if (tokenCount !== undefined && tokenCount >= CODEX_APP_SERVER_NATIVE_THREAD_MAX_TOKENS) {
|
|
767
|
+
embeddedAgentLog.warn(
|
|
768
|
+
"codex app-server native transcript exceeded active token limit; starting a fresh thread",
|
|
769
|
+
{
|
|
770
|
+
threadId: binding.threadId,
|
|
771
|
+
maxTokens: CODEX_APP_SERVER_NATIVE_THREAD_MAX_TOKENS,
|
|
772
|
+
sessionKey: sessionRecord?.sessionKey,
|
|
773
|
+
sessionTokens,
|
|
774
|
+
nativeTokens,
|
|
775
|
+
},
|
|
776
|
+
);
|
|
777
|
+
await clearCodexAppServerBinding(params.sessionFile);
|
|
778
|
+
return undefined;
|
|
779
|
+
}
|
|
780
|
+
return binding;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
export async function runCodexAppServerAttempt(
|
|
784
|
+
params: EmbeddedRunAttemptParams,
|
|
785
|
+
options: {
|
|
786
|
+
pluginConfig?: unknown;
|
|
787
|
+
startupTimeoutFloorMs?: number;
|
|
788
|
+
nativeHookRelay?: {
|
|
789
|
+
enabled?: boolean;
|
|
790
|
+
events?: readonly NativeHookRelayEvent[];
|
|
791
|
+
ttlMs?: number;
|
|
792
|
+
gatewayTimeoutMs?: number;
|
|
793
|
+
hookTimeoutSec?: number;
|
|
794
|
+
};
|
|
795
|
+
turnCompletionIdleTimeoutMs?: number;
|
|
796
|
+
turnAssistantCompletionIdleTimeoutMs?: number;
|
|
797
|
+
turnTerminalIdleTimeoutMs?: number;
|
|
798
|
+
clientFactory?: CodexAppServerClientFactory;
|
|
799
|
+
} = {},
|
|
800
|
+
): Promise<EmbeddedRunAttemptResult> {
|
|
801
|
+
const attemptStartedAt = Date.now();
|
|
802
|
+
const attemptClientFactory = options.clientFactory ?? defaultCodexAppServerClientFactory;
|
|
803
|
+
const pluginConfig = readCodexPluginConfig(options.pluginConfig);
|
|
804
|
+
const configuredAppServer = resolveCodexAppServerRuntimeOptions({ pluginConfig });
|
|
805
|
+
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
|
806
|
+
await fs.mkdir(resolvedWorkspace, { recursive: true });
|
|
807
|
+
const sandboxSessionKey =
|
|
808
|
+
params.sandboxSessionKey?.trim() || params.sessionKey?.trim() || params.sessionId;
|
|
809
|
+
const sandbox = await resolveSandboxContext({
|
|
810
|
+
config: params.config,
|
|
811
|
+
sessionKey: sandboxSessionKey,
|
|
812
|
+
workspaceDir: resolvedWorkspace,
|
|
813
|
+
});
|
|
814
|
+
const effectiveWorkspace = sandbox?.enabled
|
|
815
|
+
? sandbox.workspaceAccess === "rw"
|
|
816
|
+
? resolvedWorkspace
|
|
817
|
+
: sandbox.workspaceDir
|
|
818
|
+
: resolvedWorkspace;
|
|
819
|
+
await fs.mkdir(effectiveWorkspace, { recursive: true });
|
|
820
|
+
const codexSandboxPolicy = resolveCodexAppServerSandboxPolicyForAutoBotSandbox(
|
|
821
|
+
configuredAppServer,
|
|
822
|
+
sandbox,
|
|
823
|
+
effectiveWorkspace,
|
|
824
|
+
);
|
|
825
|
+
const appServer = resolveCodexAppServerForAutoBotToolPolicy({
|
|
826
|
+
appServer: configuredAppServer,
|
|
827
|
+
pluginConfig,
|
|
828
|
+
env: process.env,
|
|
829
|
+
shouldPromote: hasBeforeToolCallPolicy(),
|
|
830
|
+
canUseUntrustedApprovalPolicy:
|
|
831
|
+
configuredAppServer.start.transport !== "stdio" ||
|
|
832
|
+
isCodexAppServerApprovalPolicyAllowedByRequirements("untrusted"),
|
|
833
|
+
});
|
|
834
|
+
let pluginAppServer: CodexAppServerRuntimeOptions = appServer;
|
|
835
|
+
const nativeHookRelayEvents = resolveCodexNativeHookRelayEvents({
|
|
836
|
+
configuredEvents: options.nativeHookRelay?.events,
|
|
837
|
+
appServer,
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
const runAbortController = new AbortController();
|
|
841
|
+
const abortFromUpstream = () => {
|
|
842
|
+
runAbortController.abort(params.abortSignal?.reason ?? "upstream_abort");
|
|
843
|
+
};
|
|
844
|
+
if (params.abortSignal?.aborted) {
|
|
845
|
+
abortFromUpstream();
|
|
846
|
+
} else {
|
|
847
|
+
params.abortSignal?.addEventListener("abort", abortFromUpstream, { once: true });
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const { sessionAgentId } = resolveSessionAgentIds({
|
|
851
|
+
sessionKey: params.sessionKey,
|
|
852
|
+
config: params.config,
|
|
853
|
+
agentId: params.agentId,
|
|
854
|
+
});
|
|
855
|
+
const agentDir = params.agentDir ?? resolveAgentDir(params.config ?? {}, sessionAgentId);
|
|
856
|
+
let startupBinding = await readCodexAppServerBinding(params.sessionFile);
|
|
857
|
+
const startupBindingAuthProfileId = startupBinding?.authProfileId;
|
|
858
|
+
startupBinding = await rotateOversizedCodexAppServerStartupBinding({
|
|
859
|
+
binding: startupBinding,
|
|
860
|
+
sessionFile: params.sessionFile,
|
|
861
|
+
agentDir,
|
|
862
|
+
codexHome: appServer.start.env?.CODEX_HOME,
|
|
863
|
+
config: params.config,
|
|
864
|
+
});
|
|
865
|
+
const startupAuthProfileCandidate =
|
|
866
|
+
params.runtimePlan?.auth.forwardedAuthProfileId ??
|
|
867
|
+
params.authProfileId ??
|
|
868
|
+
startupBinding?.authProfileId ??
|
|
869
|
+
startupBindingAuthProfileId;
|
|
870
|
+
const startupAuthProfileId = params.authProfileStore
|
|
871
|
+
? resolveCodexAppServerAuthProfileId({
|
|
872
|
+
authProfileId: startupAuthProfileCandidate,
|
|
873
|
+
store: params.authProfileStore,
|
|
874
|
+
config: params.config,
|
|
875
|
+
})
|
|
876
|
+
: resolveCodexAppServerAuthProfileIdForAgent({
|
|
877
|
+
authProfileId: startupAuthProfileCandidate,
|
|
878
|
+
agentDir,
|
|
879
|
+
config: params.config,
|
|
880
|
+
});
|
|
881
|
+
const runtimeParams = {
|
|
882
|
+
...params,
|
|
883
|
+
sessionKey: sandboxSessionKey,
|
|
884
|
+
...(startupAuthProfileId ? { authProfileId: startupAuthProfileId } : {}),
|
|
885
|
+
};
|
|
886
|
+
let activeSessionId = params.sessionId;
|
|
887
|
+
let activeSessionFile = params.sessionFile;
|
|
888
|
+
const buildActiveRunAttemptParams = (): EmbeddedRunAttemptParams => ({
|
|
889
|
+
...runtimeParams,
|
|
890
|
+
sessionId: activeSessionId,
|
|
891
|
+
sessionFile: activeSessionFile,
|
|
892
|
+
});
|
|
893
|
+
const adoptContextEngineCompactionTranscript = (compactResult: {
|
|
894
|
+
result?: { sessionId?: string; sessionFile?: string };
|
|
895
|
+
}): void => {
|
|
896
|
+
if (compactResult.result?.sessionId) {
|
|
897
|
+
activeSessionId = compactResult.result.sessionId;
|
|
898
|
+
}
|
|
899
|
+
if (compactResult.result?.sessionFile) {
|
|
900
|
+
activeSessionFile = compactResult.result.sessionFile;
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
const startupAuthAccountCacheKey = await resolveCodexAppServerAuthAccountCacheKey({
|
|
904
|
+
authProfileId: startupAuthProfileId,
|
|
905
|
+
authProfileStore: params.authProfileStore,
|
|
906
|
+
agentDir,
|
|
907
|
+
config: params.config,
|
|
908
|
+
});
|
|
909
|
+
const startupEnvApiKeyCacheKey = startupAuthProfileId
|
|
910
|
+
? undefined
|
|
911
|
+
: resolveCodexAppServerEnvApiKeyCacheKey({
|
|
912
|
+
startOptions: appServer.start,
|
|
913
|
+
});
|
|
914
|
+
const bundleMcpThreadConfig = await loadCodexBundleMcpThreadConfig({
|
|
915
|
+
workspaceDir: effectiveWorkspace,
|
|
916
|
+
cfg: params.config,
|
|
917
|
+
toolsEnabled: supportsModelTools(params.model),
|
|
918
|
+
disableTools: params.disableTools,
|
|
919
|
+
toolsAllow: params.toolsAllow,
|
|
920
|
+
});
|
|
921
|
+
const nativeToolSurfaceEnabled = shouldEnableCodexAppServerNativeToolSurface(params, sandbox);
|
|
922
|
+
for (const diagnostic of bundleMcpThreadConfig.diagnostics) {
|
|
923
|
+
embeddedAgentLog.warn(`bundle-mcp: ${diagnostic.pluginId}: ${diagnostic.message}`);
|
|
924
|
+
}
|
|
925
|
+
const activeContextEngine = isActiveHarnessContextEngine(params.contextEngine)
|
|
926
|
+
? params.contextEngine
|
|
927
|
+
: undefined;
|
|
928
|
+
const hookChannelId = resolveCodexAppServerHookChannelId(params, sandboxSessionKey);
|
|
929
|
+
let yieldDetected = false;
|
|
930
|
+
const tools = await buildDynamicTools({
|
|
931
|
+
params,
|
|
932
|
+
resolvedWorkspace,
|
|
933
|
+
effectiveWorkspace,
|
|
934
|
+
sandboxSessionKey,
|
|
935
|
+
sandbox,
|
|
936
|
+
nativeToolSurfaceEnabled,
|
|
937
|
+
runAbortController,
|
|
938
|
+
sessionAgentId,
|
|
939
|
+
pluginConfig,
|
|
940
|
+
onYieldDetected: () => {
|
|
941
|
+
yieldDetected = true;
|
|
942
|
+
},
|
|
943
|
+
});
|
|
944
|
+
const toolBridge = createCodexDynamicToolBridge({
|
|
945
|
+
tools,
|
|
946
|
+
signal: runAbortController.signal,
|
|
947
|
+
loading: resolveCodexDynamicToolsLoading(pluginConfig),
|
|
948
|
+
directToolNames: shouldForceMessageTool(params) ? ["message"] : [],
|
|
949
|
+
hookContext: {
|
|
950
|
+
agentId: sessionAgentId,
|
|
951
|
+
config: params.config,
|
|
952
|
+
sessionId: params.sessionId,
|
|
953
|
+
sessionKey: sandboxSessionKey,
|
|
954
|
+
runId: params.runId,
|
|
955
|
+
channelId: hookChannelId,
|
|
956
|
+
},
|
|
957
|
+
});
|
|
958
|
+
const hadSessionFile = await pathExists(activeSessionFile);
|
|
959
|
+
let historyMessages = (await readMirroredSessionHistoryMessages(activeSessionFile)) ?? [];
|
|
960
|
+
const hookContextWindowFields = {
|
|
961
|
+
...(params.contextWindowInfo?.tokens
|
|
962
|
+
? { contextTokenBudget: params.contextWindowInfo.tokens }
|
|
963
|
+
: params.contextTokenBudget
|
|
964
|
+
? { contextTokenBudget: params.contextTokenBudget }
|
|
965
|
+
: {}),
|
|
966
|
+
...(params.contextWindowInfo?.source
|
|
967
|
+
? { contextWindowSource: params.contextWindowInfo.source }
|
|
968
|
+
: {}),
|
|
969
|
+
...(params.contextWindowInfo?.referenceTokens
|
|
970
|
+
? { contextWindowReferenceTokens: params.contextWindowInfo.referenceTokens }
|
|
971
|
+
: {}),
|
|
972
|
+
};
|
|
973
|
+
const hookContext = {
|
|
974
|
+
runId: params.runId,
|
|
975
|
+
agentId: sessionAgentId,
|
|
976
|
+
sessionKey: sandboxSessionKey,
|
|
977
|
+
sessionId: params.sessionId,
|
|
978
|
+
workspaceDir: params.workspaceDir,
|
|
979
|
+
messageProvider: params.messageProvider ?? undefined,
|
|
980
|
+
trigger: params.trigger,
|
|
981
|
+
channelId: hookChannelId,
|
|
982
|
+
...hookContextWindowFields,
|
|
983
|
+
};
|
|
984
|
+
const activeContextEnginePluginId = activeContextEngine
|
|
985
|
+
? resolveContextEngineOwnerPluginId(activeContextEngine)
|
|
986
|
+
: undefined;
|
|
987
|
+
const buildActiveContextEngineRuntimeContext = () =>
|
|
988
|
+
buildHarnessContextEngineRuntimeContext({
|
|
989
|
+
attempt: buildActiveRunAttemptParams(),
|
|
990
|
+
workspaceDir: effectiveWorkspace,
|
|
991
|
+
agentDir,
|
|
992
|
+
activeAgentId: sessionAgentId,
|
|
993
|
+
contextEnginePluginId: activeContextEnginePluginId,
|
|
994
|
+
tokenBudget: params.contextTokenBudget,
|
|
995
|
+
});
|
|
996
|
+
if (activeContextEngine) {
|
|
997
|
+
await bootstrapHarnessContextEngine({
|
|
998
|
+
hadSessionFile,
|
|
999
|
+
contextEngine: activeContextEngine,
|
|
1000
|
+
sessionId: activeSessionId,
|
|
1001
|
+
sessionKey: sandboxSessionKey,
|
|
1002
|
+
sessionFile: activeSessionFile,
|
|
1003
|
+
runtimeContext: buildActiveContextEngineRuntimeContext(),
|
|
1004
|
+
runMaintenance: runHarnessContextEngineMaintenance,
|
|
1005
|
+
config: params.config,
|
|
1006
|
+
warn: (message) => embeddedAgentLog.warn(message),
|
|
1007
|
+
});
|
|
1008
|
+
historyMessages =
|
|
1009
|
+
(await readMirroredSessionHistoryMessages(activeSessionFile)) ?? historyMessages;
|
|
1010
|
+
}
|
|
1011
|
+
const workspaceBootstrapContext = await buildCodexWorkspaceBootstrapContext({
|
|
1012
|
+
params,
|
|
1013
|
+
resolvedWorkspace,
|
|
1014
|
+
effectiveWorkspace,
|
|
1015
|
+
sessionKey: sandboxSessionKey,
|
|
1016
|
+
sessionAgentId,
|
|
1017
|
+
});
|
|
1018
|
+
const baseDeveloperInstructions = joinPresentSections(
|
|
1019
|
+
buildDeveloperInstructions(params, {
|
|
1020
|
+
dynamicTools: toolBridge.specs,
|
|
1021
|
+
}),
|
|
1022
|
+
workspaceBootstrapContext.developerInstructions,
|
|
1023
|
+
);
|
|
1024
|
+
const autoBotPromptContext = buildCodexAutoBotPromptContext({
|
|
1025
|
+
params,
|
|
1026
|
+
skillsPrompt: params.skillsSnapshot?.prompt,
|
|
1027
|
+
workspacePromptContext: workspaceBootstrapContext.promptContext,
|
|
1028
|
+
});
|
|
1029
|
+
let promptText = params.prompt;
|
|
1030
|
+
let developerInstructions = baseDeveloperInstructions;
|
|
1031
|
+
let prePromptMessageCount = historyMessages.length;
|
|
1032
|
+
let contextEngineProjection: CodexContextEngineThreadBootstrapProjection | undefined;
|
|
1033
|
+
const resetCodexPromptInputs = () => {
|
|
1034
|
+
promptText = params.prompt;
|
|
1035
|
+
developerInstructions = baseDeveloperInstructions;
|
|
1036
|
+
prePromptMessageCount = historyMessages.length;
|
|
1037
|
+
contextEngineProjection = undefined;
|
|
1038
|
+
};
|
|
1039
|
+
const applyActiveContextEngineProjection = async (
|
|
1040
|
+
decisionStartupBinding: CodexAppServerThreadBinding | undefined,
|
|
1041
|
+
) => {
|
|
1042
|
+
if (!activeContextEngine) {
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
const assembled = await assembleHarnessContextEngine({
|
|
1046
|
+
contextEngine: activeContextEngine,
|
|
1047
|
+
sessionId: activeSessionId,
|
|
1048
|
+
sessionKey: sandboxSessionKey,
|
|
1049
|
+
messages: historyMessages,
|
|
1050
|
+
tokenBudget: params.contextTokenBudget,
|
|
1051
|
+
availableTools: new Set(toolBridge.specs.map((tool) => tool.name).filter(isNonEmptyString)),
|
|
1052
|
+
citationsMode: params.config?.memory?.citations,
|
|
1053
|
+
modelId: params.modelId,
|
|
1054
|
+
prompt: params.prompt,
|
|
1055
|
+
});
|
|
1056
|
+
if (!assembled) {
|
|
1057
|
+
throw new Error("context engine assemble returned no result");
|
|
1058
|
+
}
|
|
1059
|
+
contextEngineProjection = readContextEngineThreadBootstrapProjection(
|
|
1060
|
+
assembled.contextProjection,
|
|
1061
|
+
);
|
|
1062
|
+
const projection = projectContextEngineAssemblyForCodex({
|
|
1063
|
+
assembledMessages: assembled.messages,
|
|
1064
|
+
originalHistoryMessages: historyMessages,
|
|
1065
|
+
prompt: params.prompt,
|
|
1066
|
+
systemPromptAddition: assembled.systemPromptAddition,
|
|
1067
|
+
maxRenderedContextChars: resolveCodexContextEngineProjectionMaxChars({
|
|
1068
|
+
contextTokenBudget: params.contextTokenBudget,
|
|
1069
|
+
reserveTokens: resolveCodexContextEngineProjectionReserveTokens({
|
|
1070
|
+
config: params.config,
|
|
1071
|
+
}),
|
|
1072
|
+
}),
|
|
1073
|
+
toolPayloadMode: contextEngineProjection ? "preserve" : "elide",
|
|
1074
|
+
});
|
|
1075
|
+
const projectionDecision = contextEngineProjection
|
|
1076
|
+
? resolveContextEngineBootstrapProjectionDecision({
|
|
1077
|
+
startupBinding: decisionStartupBinding,
|
|
1078
|
+
expectedBinding: buildContextEngineBinding(
|
|
1079
|
+
buildActiveRunAttemptParams(),
|
|
1080
|
+
contextEngineProjection,
|
|
1081
|
+
),
|
|
1082
|
+
projection: contextEngineProjection,
|
|
1083
|
+
dynamicToolsFingerprint: codexDynamicToolsFingerprint(toolBridge.specs),
|
|
1084
|
+
})
|
|
1085
|
+
: { project: true, reason: "per-turn-projection" };
|
|
1086
|
+
embeddedAgentLog.info("codex app-server context-engine projection decision", {
|
|
1087
|
+
sessionId: params.sessionId,
|
|
1088
|
+
sessionKey: sandboxSessionKey,
|
|
1089
|
+
engineId: activeContextEngine.info.id,
|
|
1090
|
+
mode: contextEngineProjection?.mode ?? assembled.contextProjection?.mode ?? "per_turn",
|
|
1091
|
+
epoch: contextEngineProjection?.epoch,
|
|
1092
|
+
fingerprint: contextEngineProjection?.fingerprint,
|
|
1093
|
+
previousThreadId: decisionStartupBinding?.threadId,
|
|
1094
|
+
previousEpoch: decisionStartupBinding?.contextEngine?.projection?.epoch,
|
|
1095
|
+
previousFingerprint: decisionStartupBinding?.contextEngine?.projection?.fingerprint,
|
|
1096
|
+
projected: projectionDecision.project,
|
|
1097
|
+
reason: projectionDecision.reason,
|
|
1098
|
+
assembledMessages: assembled.messages.length,
|
|
1099
|
+
originalHistoryMessages: historyMessages.length,
|
|
1100
|
+
projectedPromptChars: projection.promptText.length,
|
|
1101
|
+
developerInstructionAdditionChars: projection.developerInstructionAddition?.length ?? 0,
|
|
1102
|
+
});
|
|
1103
|
+
promptText = projectionDecision.project ? projection.promptText : params.prompt;
|
|
1104
|
+
developerInstructions = joinPresentSections(
|
|
1105
|
+
baseDeveloperInstructions,
|
|
1106
|
+
projection.developerInstructionAddition,
|
|
1107
|
+
);
|
|
1108
|
+
prePromptMessageCount = projection.prePromptMessageCount;
|
|
1109
|
+
};
|
|
1110
|
+
if (activeContextEngine) {
|
|
1111
|
+
try {
|
|
1112
|
+
await applyActiveContextEngineProjection(startupBinding);
|
|
1113
|
+
} catch (assembleErr) {
|
|
1114
|
+
embeddedAgentLog.warn("context engine assemble failed; using Codex baseline prompt", {
|
|
1115
|
+
error: formatErrorMessage(assembleErr),
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
} else if (
|
|
1119
|
+
shouldProjectMirroredHistoryForCodexStart({
|
|
1120
|
+
startupBinding,
|
|
1121
|
+
dynamicToolsFingerprint: codexDynamicToolsFingerprint(toolBridge.specs),
|
|
1122
|
+
historyMessages,
|
|
1123
|
+
})
|
|
1124
|
+
) {
|
|
1125
|
+
const projection = projectContextEngineAssemblyForCodex({
|
|
1126
|
+
assembledMessages: historyMessages,
|
|
1127
|
+
originalHistoryMessages: historyMessages,
|
|
1128
|
+
prompt: params.prompt,
|
|
1129
|
+
});
|
|
1130
|
+
promptText = projection.promptText;
|
|
1131
|
+
prePromptMessageCount = projection.prePromptMessageCount;
|
|
1132
|
+
}
|
|
1133
|
+
const buildPromptFromCurrentInputs = () =>
|
|
1134
|
+
resolveAgentHarnessBeforePromptBuildResult({
|
|
1135
|
+
prompt: prependCurrentInboundContext(promptText, params.currentInboundContext),
|
|
1136
|
+
developerInstructions,
|
|
1137
|
+
messages: historyMessages,
|
|
1138
|
+
ctx: hookContext,
|
|
1139
|
+
});
|
|
1140
|
+
let promptBuild = await buildPromptFromCurrentInputs();
|
|
1141
|
+
const decorateCodexTurnPromptText = (prompt: string) =>
|
|
1142
|
+
prependCodexAutoBotPromptContext(prompt, autoBotPromptContext);
|
|
1143
|
+
let codexTurnPromptText = decorateCodexTurnPromptText(promptBuild.prompt);
|
|
1144
|
+
const refreshCodexTurnPromptText = () => {
|
|
1145
|
+
codexTurnPromptText = decorateCodexTurnPromptText(promptBuild.prompt);
|
|
1146
|
+
};
|
|
1147
|
+
const systemPromptReport = buildCodexSystemPromptReport({
|
|
1148
|
+
attempt: params,
|
|
1149
|
+
sessionKey: sandboxSessionKey,
|
|
1150
|
+
workspaceDir: effectiveWorkspace,
|
|
1151
|
+
developerInstructions: promptBuild.developerInstructions,
|
|
1152
|
+
workspaceBootstrapContext,
|
|
1153
|
+
skillsPrompt: autoBotPromptContext ? (params.skillsSnapshot?.prompt ?? "") : "",
|
|
1154
|
+
tools: toolBridge.specs,
|
|
1155
|
+
});
|
|
1156
|
+
const trajectoryRecorder = createCodexTrajectoryRecorder({
|
|
1157
|
+
attempt: params,
|
|
1158
|
+
cwd: effectiveWorkspace,
|
|
1159
|
+
developerInstructions: promptBuild.developerInstructions,
|
|
1160
|
+
prompt: codexTurnPromptText,
|
|
1161
|
+
tools: toolBridge.specs,
|
|
1162
|
+
});
|
|
1163
|
+
let client: CodexAppServerClient;
|
|
1164
|
+
let thread: CodexAppServerThreadLifecycleBinding;
|
|
1165
|
+
let trajectoryEndRecorded = false;
|
|
1166
|
+
let nativeHookRelay: NativeHookRelayRegistrationHandle | undefined;
|
|
1167
|
+
let startupClientForCleanup: CodexAppServerClient | undefined;
|
|
1168
|
+
let restartContextEngineCodexThread:
|
|
1169
|
+
| (() => Promise<CodexAppServerThreadLifecycleBinding>)
|
|
1170
|
+
| undefined;
|
|
1171
|
+
const startupTimeoutMs = resolveCodexStartupTimeoutMs({
|
|
1172
|
+
timeoutMs: params.timeoutMs,
|
|
1173
|
+
timeoutFloorMs: options.startupTimeoutFloorMs,
|
|
1174
|
+
});
|
|
1175
|
+
try {
|
|
1176
|
+
emitCodexAppServerEvent(params, {
|
|
1177
|
+
stream: "codex_app_server.lifecycle",
|
|
1178
|
+
data: { phase: "startup" },
|
|
1179
|
+
});
|
|
1180
|
+
nativeHookRelay = createCodexNativeHookRelay({
|
|
1181
|
+
options: options.nativeHookRelay,
|
|
1182
|
+
events: nativeHookRelayEvents,
|
|
1183
|
+
agentId: sessionAgentId,
|
|
1184
|
+
sessionId: params.sessionId,
|
|
1185
|
+
sessionKey: sandboxSessionKey,
|
|
1186
|
+
config: params.config,
|
|
1187
|
+
runId: params.runId,
|
|
1188
|
+
channelId: hookChannelId,
|
|
1189
|
+
attemptTimeoutMs: params.timeoutMs,
|
|
1190
|
+
startupTimeoutMs,
|
|
1191
|
+
turnStartTimeoutMs: params.timeoutMs,
|
|
1192
|
+
signal: runAbortController.signal,
|
|
1193
|
+
});
|
|
1194
|
+
const nativeHookRelayConfig = nativeHookRelay
|
|
1195
|
+
? buildCodexNativeHookRelayConfig({
|
|
1196
|
+
relay: nativeHookRelay,
|
|
1197
|
+
events: nativeHookRelayEvents,
|
|
1198
|
+
hookTimeoutSec: options.nativeHookRelay?.hookTimeoutSec,
|
|
1199
|
+
})
|
|
1200
|
+
: options.nativeHookRelay?.enabled === false
|
|
1201
|
+
? buildCodexNativeHookRelayDisabledConfig()
|
|
1202
|
+
: undefined;
|
|
1203
|
+
const threadConfig = mergeCodexThreadConfigs(
|
|
1204
|
+
bundleMcpThreadConfig?.configPatch as JsonObject | undefined,
|
|
1205
|
+
);
|
|
1206
|
+
const nativeToolSurfaceRestricted = !nativeToolSurfaceEnabled;
|
|
1207
|
+
const pluginThreadConfigRequired =
|
|
1208
|
+
nativeToolSurfaceRestricted || shouldBuildCodexPluginThreadConfig(pluginConfig);
|
|
1209
|
+
// Restricted runs still need a plugin thread config so thread/start
|
|
1210
|
+
// carries the explicit apps._default denial patch without app/list.
|
|
1211
|
+
const pluginThreadConfigPluginConfig = nativeToolSurfaceEnabled
|
|
1212
|
+
? pluginConfig
|
|
1213
|
+
: disableCodexPluginThreadConfig(pluginConfig);
|
|
1214
|
+
const pluginAppCacheKeyInput = {
|
|
1215
|
+
appServer,
|
|
1216
|
+
agentDir,
|
|
1217
|
+
authProfileId: startupAuthProfileId,
|
|
1218
|
+
accountId: startupAuthAccountCacheKey,
|
|
1219
|
+
envApiKeyFingerprint: startupEnvApiKeyCacheKey,
|
|
1220
|
+
};
|
|
1221
|
+
const pluginAppCacheKey = buildCodexPluginAppCacheKey(pluginAppCacheKeyInput);
|
|
1222
|
+
const pluginThreadConfigInputFingerprint = pluginThreadConfigRequired
|
|
1223
|
+
? buildCodexPluginThreadConfigInputFingerprint({
|
|
1224
|
+
pluginConfig: pluginThreadConfigPluginConfig,
|
|
1225
|
+
appCacheKey: pluginAppCacheKey,
|
|
1226
|
+
})
|
|
1227
|
+
: undefined;
|
|
1228
|
+
const resolvedPluginPolicy = pluginThreadConfigRequired
|
|
1229
|
+
? resolveCodexPluginsPolicy(pluginThreadConfigPluginConfig)
|
|
1230
|
+
: undefined;
|
|
1231
|
+
const enabledPluginConfigKeys = resolvedPluginPolicy
|
|
1232
|
+
? resolvedPluginPolicy.pluginPolicies
|
|
1233
|
+
.filter((plugin) => plugin.enabled)
|
|
1234
|
+
.map((plugin) => plugin.configKey)
|
|
1235
|
+
.toSorted()
|
|
1236
|
+
: undefined;
|
|
1237
|
+
embeddedAgentLog.info(
|
|
1238
|
+
"codex plugin thread config eligibility",
|
|
1239
|
+
buildCodexPluginThreadConfigEligibilityLogData({
|
|
1240
|
+
sessionId: params.sessionId,
|
|
1241
|
+
sessionKey: sandboxSessionKey,
|
|
1242
|
+
pluginThreadConfigRequired,
|
|
1243
|
+
resolvedPluginPolicy,
|
|
1244
|
+
enabledPluginConfigKeys,
|
|
1245
|
+
pluginAppCacheKey,
|
|
1246
|
+
startupAuthProfileId,
|
|
1247
|
+
appServer,
|
|
1248
|
+
}),
|
|
1249
|
+
);
|
|
1250
|
+
pluginAppServer =
|
|
1251
|
+
resolvedPluginPolicy?.enabled === true
|
|
1252
|
+
? {
|
|
1253
|
+
...appServer,
|
|
1254
|
+
approvalPolicy: withMcpElicitationsApprovalPolicy(appServer.approvalPolicy),
|
|
1255
|
+
}
|
|
1256
|
+
: appServer;
|
|
1257
|
+
({ client, thread } = await withCodexStartupTimeout({
|
|
1258
|
+
timeoutMs: startupTimeoutMs,
|
|
1259
|
+
signal: runAbortController.signal,
|
|
1260
|
+
operation: async () => {
|
|
1261
|
+
let attemptedClient: CodexAppServerClient | undefined;
|
|
1262
|
+
const startupAttempt = async () => {
|
|
1263
|
+
const startupClient = await attemptClientFactory(
|
|
1264
|
+
appServer.start,
|
|
1265
|
+
startupAuthProfileId,
|
|
1266
|
+
agentDir,
|
|
1267
|
+
params.config,
|
|
1268
|
+
);
|
|
1269
|
+
attemptedClient = startupClient;
|
|
1270
|
+
startupClientForCleanup = startupClient;
|
|
1271
|
+
await ensureCodexComputerUse({
|
|
1272
|
+
client: startupClient,
|
|
1273
|
+
pluginConfig: options.pluginConfig,
|
|
1274
|
+
timeoutMs: appServer.requestTimeoutMs,
|
|
1275
|
+
signal: runAbortController.signal,
|
|
1276
|
+
});
|
|
1277
|
+
const buildThreadLifecycleParams = () =>
|
|
1278
|
+
({
|
|
1279
|
+
client: startupClient,
|
|
1280
|
+
params: buildActiveRunAttemptParams(),
|
|
1281
|
+
agentId: sessionAgentId,
|
|
1282
|
+
cwd: effectiveWorkspace,
|
|
1283
|
+
dynamicTools: toolBridge.specs,
|
|
1284
|
+
appServer: pluginAppServer,
|
|
1285
|
+
developerInstructions: promptBuild.developerInstructions,
|
|
1286
|
+
config: threadConfig,
|
|
1287
|
+
finalConfigPatch: nativeHookRelayConfig,
|
|
1288
|
+
nativeCodeModeEnabled: nativeToolSurfaceEnabled,
|
|
1289
|
+
nativeCodeModeOnlyEnabled: appServer.codeModeOnly,
|
|
1290
|
+
userMcpServersEnabled: nativeToolSurfaceEnabled,
|
|
1291
|
+
mcpServersFingerprint: bundleMcpThreadConfig.fingerprint,
|
|
1292
|
+
mcpServersFingerprintEvaluated: bundleMcpThreadConfig.evaluated,
|
|
1293
|
+
contextEngineProjection,
|
|
1294
|
+
pluginThreadConfig: pluginThreadConfigRequired
|
|
1295
|
+
? {
|
|
1296
|
+
enabled: true,
|
|
1297
|
+
inputFingerprint: pluginThreadConfigInputFingerprint,
|
|
1298
|
+
enabledPluginConfigKeys,
|
|
1299
|
+
build: () =>
|
|
1300
|
+
buildCodexPluginThreadConfig({
|
|
1301
|
+
pluginConfig: pluginThreadConfigPluginConfig,
|
|
1302
|
+
request: (method, requestParams) =>
|
|
1303
|
+
startupClient.request(method, requestParams, {
|
|
1304
|
+
timeoutMs: appServer.requestTimeoutMs,
|
|
1305
|
+
signal: runAbortController.signal,
|
|
1306
|
+
}),
|
|
1307
|
+
appCache: defaultCodexAppInventoryCache,
|
|
1308
|
+
appCacheKey: pluginAppCacheKey,
|
|
1309
|
+
}),
|
|
1310
|
+
}
|
|
1311
|
+
: undefined,
|
|
1312
|
+
}) satisfies Parameters<typeof startOrResumeThread>[0];
|
|
1313
|
+
restartContextEngineCodexThread = () => startOrResumeThread(buildThreadLifecycleParams());
|
|
1314
|
+
const startupThread = await startOrResumeThread(buildThreadLifecycleParams());
|
|
1315
|
+
return { client: startupClient, thread: startupThread };
|
|
1316
|
+
};
|
|
1317
|
+
for (
|
|
1318
|
+
let attempt = 1;
|
|
1319
|
+
attempt <= CODEX_APP_SERVER_STARTUP_CONNECTION_CLOSE_MAX_ATTEMPTS;
|
|
1320
|
+
attempt += 1
|
|
1321
|
+
) {
|
|
1322
|
+
try {
|
|
1323
|
+
return await startupAttempt();
|
|
1324
|
+
} catch (error) {
|
|
1325
|
+
if (
|
|
1326
|
+
runAbortController.signal.aborted ||
|
|
1327
|
+
!isCodexAppServerConnectionClosedError(error)
|
|
1328
|
+
) {
|
|
1329
|
+
throw error;
|
|
1330
|
+
}
|
|
1331
|
+
const failedClient = attemptedClient;
|
|
1332
|
+
const clearedSharedClient = clearSharedCodexAppServerClientIfCurrent(failedClient);
|
|
1333
|
+
if (startupClientForCleanup === failedClient) {
|
|
1334
|
+
startupClientForCleanup = undefined;
|
|
1335
|
+
}
|
|
1336
|
+
attemptedClient = undefined;
|
|
1337
|
+
if (attempt >= CODEX_APP_SERVER_STARTUP_CONNECTION_CLOSE_MAX_ATTEMPTS) {
|
|
1338
|
+
embeddedAgentLog.warn(
|
|
1339
|
+
"codex app-server connection closed during startup; retries exhausted",
|
|
1340
|
+
{
|
|
1341
|
+
attempt,
|
|
1342
|
+
maxAttempts: CODEX_APP_SERVER_STARTUP_CONNECTION_CLOSE_MAX_ATTEMPTS,
|
|
1343
|
+
clearedSharedClient,
|
|
1344
|
+
error: formatErrorMessage(error),
|
|
1345
|
+
},
|
|
1346
|
+
);
|
|
1347
|
+
throw error;
|
|
1348
|
+
}
|
|
1349
|
+
embeddedAgentLog.warn(
|
|
1350
|
+
"codex app-server connection closed during startup; restarting app-server and retrying",
|
|
1351
|
+
{
|
|
1352
|
+
attempt,
|
|
1353
|
+
nextAttempt: attempt + 1,
|
|
1354
|
+
maxAttempts: CODEX_APP_SERVER_STARTUP_CONNECTION_CLOSE_MAX_ATTEMPTS,
|
|
1355
|
+
clearedSharedClient,
|
|
1356
|
+
error: formatErrorMessage(error),
|
|
1357
|
+
},
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
throw new Error("codex app-server startup retry loop exited unexpectedly");
|
|
1362
|
+
},
|
|
1363
|
+
}));
|
|
1364
|
+
startupClientForCleanup = undefined;
|
|
1365
|
+
emitCodexAppServerEvent(params, {
|
|
1366
|
+
stream: "codex_app_server.lifecycle",
|
|
1367
|
+
data: { phase: "thread_ready", threadId: thread.threadId },
|
|
1368
|
+
});
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
nativeHookRelay?.unregister();
|
|
1371
|
+
clearSharedCodexAppServerClientIfCurrent(startupClientForCleanup);
|
|
1372
|
+
params.abortSignal?.removeEventListener("abort", abortFromUpstream);
|
|
1373
|
+
throw error;
|
|
1374
|
+
}
|
|
1375
|
+
trajectoryRecorder?.recordEvent("session.started", {
|
|
1376
|
+
sessionFile: params.sessionFile,
|
|
1377
|
+
threadId: thread.threadId,
|
|
1378
|
+
authProfileId: startupAuthProfileId,
|
|
1379
|
+
workspaceDir: effectiveWorkspace,
|
|
1380
|
+
toolCount: toolBridge.specs.length,
|
|
1381
|
+
});
|
|
1382
|
+
recordCodexTrajectoryContext(trajectoryRecorder, {
|
|
1383
|
+
attempt: params,
|
|
1384
|
+
cwd: effectiveWorkspace,
|
|
1385
|
+
developerInstructions: promptBuild.developerInstructions,
|
|
1386
|
+
prompt: codexTurnPromptText,
|
|
1387
|
+
tools: toolBridge.specs,
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
let projector: CodexAppServerEventProjector | undefined;
|
|
1391
|
+
let turnId: string | undefined;
|
|
1392
|
+
const pendingNotifications: CodexServerNotification[] = [];
|
|
1393
|
+
let userInputBridge: ReturnType<typeof createCodexUserInputBridge> | undefined;
|
|
1394
|
+
let steeringQueue: ReturnType<typeof createCodexSteeringQueue> | undefined;
|
|
1395
|
+
let completed = false;
|
|
1396
|
+
let terminalTurnNotificationQueued = false;
|
|
1397
|
+
let timedOut = false;
|
|
1398
|
+
let turnCompletionIdleTimedOut = false;
|
|
1399
|
+
let turnCompletionIdleTimeoutMessage: string | undefined;
|
|
1400
|
+
let clientClosedPromptError: string | undefined;
|
|
1401
|
+
let clientClosedAbort = false;
|
|
1402
|
+
let lifecycleStarted = false;
|
|
1403
|
+
let lifecycleTerminalEmitted = false;
|
|
1404
|
+
let resolveCompletion: (() => void) | undefined;
|
|
1405
|
+
const completion = new Promise<void>((resolve) => {
|
|
1406
|
+
resolveCompletion = resolve;
|
|
1407
|
+
});
|
|
1408
|
+
let notificationQueue: Promise<void> = Promise.resolve();
|
|
1409
|
+
const turnCompletionIdleTimeoutMs = resolveCodexTurnCompletionIdleTimeoutMs(
|
|
1410
|
+
options.turnCompletionIdleTimeoutMs ?? appServer.turnCompletionIdleTimeoutMs,
|
|
1411
|
+
);
|
|
1412
|
+
const turnAssistantCompletionIdleTimeoutMs = resolveCodexTurnAssistantCompletionIdleTimeoutMs(
|
|
1413
|
+
options.turnAssistantCompletionIdleTimeoutMs,
|
|
1414
|
+
);
|
|
1415
|
+
const turnTerminalIdleTimeoutMs = resolveCodexTurnTerminalIdleTimeoutMs(
|
|
1416
|
+
options.turnTerminalIdleTimeoutMs,
|
|
1417
|
+
);
|
|
1418
|
+
let turnCompletionIdleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1419
|
+
let turnCompletionIdleWatchArmed = false;
|
|
1420
|
+
let turnCompletionIdleWatchPinnedByTerminalError = false;
|
|
1421
|
+
let turnCompletionIdleTimeoutOverrideMs: number | undefined;
|
|
1422
|
+
let turnAssistantCompletionIdleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1423
|
+
let turnAssistantCompletionIdleWatchArmed = false;
|
|
1424
|
+
let turnAssistantCompletionLastActivityAt = Date.now();
|
|
1425
|
+
let turnAssistantCompletionLastActivityDetails: Record<string, unknown> | undefined;
|
|
1426
|
+
const turnAttemptIdleTimeoutMs = Math.max(100, Math.floor(params.timeoutMs));
|
|
1427
|
+
let turnAttemptIdleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1428
|
+
let turnAttemptIdleWatchArmed = false;
|
|
1429
|
+
let turnTerminalIdleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1430
|
+
let turnTerminalIdleWatchArmed = false;
|
|
1431
|
+
let turnCompletionLastActivityAt = Date.now();
|
|
1432
|
+
let turnCompletionLastActivityReason = "startup";
|
|
1433
|
+
let turnCompletionLastActivityDetails: Record<string, unknown> | undefined;
|
|
1434
|
+
let turnAttemptLastProgressAt = Date.now();
|
|
1435
|
+
let turnAttemptLastProgressReason = "startup";
|
|
1436
|
+
let turnAttemptLastProgressDetails: Record<string, unknown> | undefined;
|
|
1437
|
+
let nativeHookRelayLastRenewedAt = 0;
|
|
1438
|
+
let activeAppServerTurnRequests = 0;
|
|
1439
|
+
const pendingAutoBotDynamicToolCompletionIds = new Set<string>();
|
|
1440
|
+
const activeTurnItemIds = new Set<string>();
|
|
1441
|
+
let turnCrossedToolHandoff = false;
|
|
1442
|
+
|
|
1443
|
+
const clearTurnCompletionIdleTimer = () => {
|
|
1444
|
+
if (turnCompletionIdleTimer) {
|
|
1445
|
+
clearTimeout(turnCompletionIdleTimer);
|
|
1446
|
+
turnCompletionIdleTimer = undefined;
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
|
|
1450
|
+
const clearTurnTerminalIdleTimer = () => {
|
|
1451
|
+
if (turnTerminalIdleTimer) {
|
|
1452
|
+
clearTimeout(turnTerminalIdleTimer);
|
|
1453
|
+
turnTerminalIdleTimer = undefined;
|
|
1454
|
+
}
|
|
1455
|
+
};
|
|
1456
|
+
|
|
1457
|
+
const clearTurnAssistantCompletionIdleTimer = () => {
|
|
1458
|
+
if (turnAssistantCompletionIdleTimer) {
|
|
1459
|
+
clearTimeout(turnAssistantCompletionIdleTimer);
|
|
1460
|
+
turnAssistantCompletionIdleTimer = undefined;
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
|
|
1464
|
+
const clearTurnAttemptIdleTimer = () => {
|
|
1465
|
+
if (turnAttemptIdleTimer) {
|
|
1466
|
+
clearTimeout(turnAttemptIdleTimer);
|
|
1467
|
+
turnAttemptIdleTimer = undefined;
|
|
1468
|
+
}
|
|
1469
|
+
};
|
|
1470
|
+
|
|
1471
|
+
const fireTurnAssistantCompletionIdleRelease = () => {
|
|
1472
|
+
if (completed || runAbortController.signal.aborted || !turnAssistantCompletionIdleWatchArmed) {
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
if (activeAppServerTurnRequests > 0 || activeTurnItemIds.size > 0) {
|
|
1476
|
+
scheduleTurnAssistantCompletionIdleWatch();
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
const idleMs = Math.max(0, Date.now() - turnAssistantCompletionLastActivityAt);
|
|
1480
|
+
if (idleMs < turnAssistantCompletionIdleTimeoutMs) {
|
|
1481
|
+
scheduleTurnAssistantCompletionIdleWatch();
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
turnAssistantCompletionIdleWatchArmed = false;
|
|
1485
|
+
clearTurnCompletionIdleTimer();
|
|
1486
|
+
clearTurnTerminalIdleTimer();
|
|
1487
|
+
trajectoryRecorder?.recordEvent("turn.assistant_completion_idle_release", {
|
|
1488
|
+
threadId: thread.threadId,
|
|
1489
|
+
turnId,
|
|
1490
|
+
idleMs,
|
|
1491
|
+
timeoutMs: turnAssistantCompletionIdleTimeoutMs,
|
|
1492
|
+
...turnAssistantCompletionLastActivityDetails,
|
|
1493
|
+
});
|
|
1494
|
+
embeddedAgentLog.warn(
|
|
1495
|
+
"codex app-server turn released after completed assistant item without terminal event",
|
|
1496
|
+
{
|
|
1497
|
+
threadId: thread.threadId,
|
|
1498
|
+
turnId,
|
|
1499
|
+
idleMs,
|
|
1500
|
+
timeoutMs: turnAssistantCompletionIdleTimeoutMs,
|
|
1501
|
+
...turnAssistantCompletionLastActivityDetails,
|
|
1502
|
+
},
|
|
1503
|
+
);
|
|
1504
|
+
if (turnId) {
|
|
1505
|
+
interruptCodexTurnBestEffort(client, {
|
|
1506
|
+
threadId: thread.threadId,
|
|
1507
|
+
turnId,
|
|
1508
|
+
timeoutMs: CODEX_APP_SERVER_INTERRUPT_TIMEOUT_MS,
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
completed = true;
|
|
1512
|
+
resolveCompletion?.();
|
|
1513
|
+
};
|
|
1514
|
+
|
|
1515
|
+
const fireTurnAttemptIdleTimeout = () => {
|
|
1516
|
+
if (completed || runAbortController.signal.aborted || !turnAttemptIdleWatchArmed) {
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
const idleMs = Math.max(0, Date.now() - turnAttemptLastProgressAt);
|
|
1520
|
+
if (idleMs < turnAttemptIdleTimeoutMs) {
|
|
1521
|
+
scheduleTurnAttemptIdleWatch();
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
timedOut = true;
|
|
1525
|
+
turnCompletionIdleTimedOut = true;
|
|
1526
|
+
turnCompletionIdleTimeoutMessage =
|
|
1527
|
+
"codex app-server turn idle timed out waiting for turn/completed";
|
|
1528
|
+
projector?.markTimedOut();
|
|
1529
|
+
trajectoryRecorder?.recordEvent("turn.progress_idle_timeout", {
|
|
1530
|
+
threadId: thread.threadId,
|
|
1531
|
+
turnId,
|
|
1532
|
+
idleMs,
|
|
1533
|
+
timeoutMs: turnAttemptIdleTimeoutMs,
|
|
1534
|
+
lastActivityReason: turnAttemptLastProgressReason,
|
|
1535
|
+
...turnAttemptLastProgressDetails,
|
|
1536
|
+
});
|
|
1537
|
+
embeddedAgentLog.warn("codex app-server turn idle timed out waiting for progress", {
|
|
1538
|
+
threadId: thread.threadId,
|
|
1539
|
+
turnId,
|
|
1540
|
+
idleMs,
|
|
1541
|
+
timeoutMs: turnAttemptIdleTimeoutMs,
|
|
1542
|
+
lastActivityReason: turnAttemptLastProgressReason,
|
|
1543
|
+
...turnAttemptLastProgressDetails,
|
|
1544
|
+
});
|
|
1545
|
+
runAbortController.abort("turn_progress_idle_timeout");
|
|
1546
|
+
};
|
|
1547
|
+
|
|
1548
|
+
const fireTurnCompletionIdleTimeout = () => {
|
|
1549
|
+
if (
|
|
1550
|
+
completed ||
|
|
1551
|
+
runAbortController.signal.aborted ||
|
|
1552
|
+
!turnCompletionIdleWatchArmed ||
|
|
1553
|
+
activeAppServerTurnRequests > 0
|
|
1554
|
+
) {
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
const timeoutMs = turnCompletionIdleTimeoutOverrideMs ?? turnCompletionIdleTimeoutMs;
|
|
1558
|
+
const idleMs = Math.max(0, Date.now() - turnCompletionLastActivityAt);
|
|
1559
|
+
if (idleMs < timeoutMs) {
|
|
1560
|
+
scheduleTurnCompletionIdleWatch();
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
timedOut = true;
|
|
1564
|
+
turnCompletionIdleTimedOut = true;
|
|
1565
|
+
turnCompletionIdleTimeoutMessage =
|
|
1566
|
+
"codex app-server turn idle timed out waiting for turn/completed";
|
|
1567
|
+
projector?.markTimedOut();
|
|
1568
|
+
trajectoryRecorder?.recordEvent("turn.completion_idle_timeout", {
|
|
1569
|
+
threadId: thread.threadId,
|
|
1570
|
+
turnId,
|
|
1571
|
+
idleMs,
|
|
1572
|
+
timeoutMs,
|
|
1573
|
+
lastActivityReason: turnCompletionLastActivityReason,
|
|
1574
|
+
...turnCompletionLastActivityDetails,
|
|
1575
|
+
});
|
|
1576
|
+
embeddedAgentLog.warn("codex app-server turn idle timed out waiting for completion", {
|
|
1577
|
+
threadId: thread.threadId,
|
|
1578
|
+
turnId,
|
|
1579
|
+
idleMs,
|
|
1580
|
+
timeoutMs,
|
|
1581
|
+
lastActivityReason: turnCompletionLastActivityReason,
|
|
1582
|
+
...turnCompletionLastActivityDetails,
|
|
1583
|
+
});
|
|
1584
|
+
runAbortController.abort("turn_completion_idle_timeout");
|
|
1585
|
+
};
|
|
1586
|
+
|
|
1587
|
+
const fireTurnTerminalIdleTimeout = () => {
|
|
1588
|
+
if (
|
|
1589
|
+
completed ||
|
|
1590
|
+
runAbortController.signal.aborted ||
|
|
1591
|
+
!turnTerminalIdleWatchArmed ||
|
|
1592
|
+
activeAppServerTurnRequests > 0
|
|
1593
|
+
) {
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
const idleMs = Math.max(0, Date.now() - turnCompletionLastActivityAt);
|
|
1597
|
+
if (idleMs < turnTerminalIdleTimeoutMs) {
|
|
1598
|
+
scheduleTurnTerminalIdleWatch();
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
timedOut = true;
|
|
1602
|
+
turnCompletionIdleTimedOut = true;
|
|
1603
|
+
turnCompletionIdleTimeoutMessage =
|
|
1604
|
+
"codex app-server turn idle timed out waiting for turn/completed";
|
|
1605
|
+
projector?.markTimedOut();
|
|
1606
|
+
trajectoryRecorder?.recordEvent("turn.terminal_idle_timeout", {
|
|
1607
|
+
threadId: thread.threadId,
|
|
1608
|
+
turnId,
|
|
1609
|
+
idleMs,
|
|
1610
|
+
timeoutMs: turnTerminalIdleTimeoutMs,
|
|
1611
|
+
lastActivityReason: turnCompletionLastActivityReason,
|
|
1612
|
+
...turnCompletionLastActivityDetails,
|
|
1613
|
+
});
|
|
1614
|
+
embeddedAgentLog.warn("codex app-server turn idle timed out waiting for terminal event", {
|
|
1615
|
+
threadId: thread.threadId,
|
|
1616
|
+
turnId,
|
|
1617
|
+
idleMs,
|
|
1618
|
+
timeoutMs: turnTerminalIdleTimeoutMs,
|
|
1619
|
+
lastActivityReason: turnCompletionLastActivityReason,
|
|
1620
|
+
...turnCompletionLastActivityDetails,
|
|
1621
|
+
});
|
|
1622
|
+
runAbortController.abort("turn_terminal_idle_timeout");
|
|
1623
|
+
};
|
|
1624
|
+
|
|
1625
|
+
function scheduleTurnCompletionIdleWatch() {
|
|
1626
|
+
clearTurnCompletionIdleTimer();
|
|
1627
|
+
if (
|
|
1628
|
+
completed ||
|
|
1629
|
+
runAbortController.signal.aborted ||
|
|
1630
|
+
!turnCompletionIdleWatchArmed ||
|
|
1631
|
+
activeAppServerTurnRequests > 0
|
|
1632
|
+
) {
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
const elapsedMs = Math.max(0, Date.now() - turnCompletionLastActivityAt);
|
|
1636
|
+
const timeoutMs = turnCompletionIdleTimeoutOverrideMs ?? turnCompletionIdleTimeoutMs;
|
|
1637
|
+
const delayMs = Math.max(1, timeoutMs - elapsedMs);
|
|
1638
|
+
turnCompletionIdleTimer = setTimeout(fireTurnCompletionIdleTimeout, delayMs);
|
|
1639
|
+
turnCompletionIdleTimer.unref?.();
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
function scheduleTurnAssistantCompletionIdleWatch() {
|
|
1643
|
+
clearTurnAssistantCompletionIdleTimer();
|
|
1644
|
+
if (completed || runAbortController.signal.aborted || !turnAssistantCompletionIdleWatchArmed) {
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
const elapsedMs = Math.max(0, Date.now() - turnAssistantCompletionLastActivityAt);
|
|
1648
|
+
const delayMs = Math.max(1, turnAssistantCompletionIdleTimeoutMs - elapsedMs);
|
|
1649
|
+
turnAssistantCompletionIdleTimer = setTimeout(fireTurnAssistantCompletionIdleRelease, delayMs);
|
|
1650
|
+
turnAssistantCompletionIdleTimer.unref?.();
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
function scheduleTurnAttemptIdleWatch() {
|
|
1654
|
+
clearTurnAttemptIdleTimer();
|
|
1655
|
+
if (completed || runAbortController.signal.aborted || !turnAttemptIdleWatchArmed) {
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
const elapsedMs = Math.max(0, Date.now() - turnAttemptLastProgressAt);
|
|
1659
|
+
const delayMs = Math.max(1, turnAttemptIdleTimeoutMs - elapsedMs);
|
|
1660
|
+
turnAttemptIdleTimer = setTimeout(fireTurnAttemptIdleTimeout, delayMs);
|
|
1661
|
+
turnAttemptIdleTimer.unref?.();
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
function scheduleTurnTerminalIdleWatch() {
|
|
1665
|
+
clearTurnTerminalIdleTimer();
|
|
1666
|
+
if (
|
|
1667
|
+
completed ||
|
|
1668
|
+
runAbortController.signal.aborted ||
|
|
1669
|
+
!turnTerminalIdleWatchArmed ||
|
|
1670
|
+
activeAppServerTurnRequests > 0
|
|
1671
|
+
) {
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
const elapsedMs = Math.max(0, Date.now() - turnCompletionLastActivityAt);
|
|
1675
|
+
const delayMs = Math.max(1, turnTerminalIdleTimeoutMs - elapsedMs);
|
|
1676
|
+
turnTerminalIdleTimer = setTimeout(fireTurnTerminalIdleTimeout, delayMs);
|
|
1677
|
+
turnTerminalIdleTimer.unref?.();
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
function scheduleTurnProgressWatches() {
|
|
1681
|
+
scheduleTurnAttemptIdleWatch();
|
|
1682
|
+
scheduleTurnCompletionIdleWatch();
|
|
1683
|
+
scheduleTurnTerminalIdleWatch();
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
const renewNativeHookRelayForTurnProgress = () => {
|
|
1687
|
+
if (!nativeHookRelay || options.nativeHookRelay?.ttlMs !== undefined) {
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
const now = Date.now();
|
|
1691
|
+
const renewsRecently =
|
|
1692
|
+
now - nativeHookRelayLastRenewedAt < CODEX_NATIVE_HOOK_RELAY_RENEW_INTERVAL_MS;
|
|
1693
|
+
const expiresSoon = now >= nativeHookRelay.expiresAtMs - CODEX_NATIVE_HOOK_RELAY_TTL_GRACE_MS;
|
|
1694
|
+
if (renewsRecently && !expiresSoon) {
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
nativeHookRelayLastRenewedAt = now;
|
|
1698
|
+
nativeHookRelay.renew(
|
|
1699
|
+
resolveCodexNativeHookRelayTtlMs({
|
|
1700
|
+
explicitTtlMs: undefined,
|
|
1701
|
+
attemptTimeoutMs: turnAttemptIdleTimeoutMs,
|
|
1702
|
+
startupTimeoutMs,
|
|
1703
|
+
turnStartTimeoutMs: params.timeoutMs,
|
|
1704
|
+
}),
|
|
1705
|
+
);
|
|
1706
|
+
};
|
|
1707
|
+
|
|
1708
|
+
const touchTurnCompletionActivity = (
|
|
1709
|
+
reason: string,
|
|
1710
|
+
options?: { arm?: boolean; details?: Record<string, unknown>; attemptProgress?: boolean },
|
|
1711
|
+
) => {
|
|
1712
|
+
turnCompletionLastActivityAt = Date.now();
|
|
1713
|
+
turnCompletionLastActivityReason = reason;
|
|
1714
|
+
turnCompletionLastActivityDetails = options?.details;
|
|
1715
|
+
turnCompletionIdleTimeoutOverrideMs = undefined;
|
|
1716
|
+
if (options?.attemptProgress) {
|
|
1717
|
+
turnAttemptLastProgressAt = turnCompletionLastActivityAt;
|
|
1718
|
+
turnAttemptLastProgressReason = reason;
|
|
1719
|
+
turnAttemptLastProgressDetails = options.details;
|
|
1720
|
+
renewNativeHookRelayForTurnProgress();
|
|
1721
|
+
params.onRunProgress?.({
|
|
1722
|
+
reason,
|
|
1723
|
+
provider: params.provider,
|
|
1724
|
+
model: params.modelId,
|
|
1725
|
+
backend: "codex-app-server",
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
emitTrustedDiagnosticEvent({
|
|
1729
|
+
type: "run.progress",
|
|
1730
|
+
runId: params.runId,
|
|
1731
|
+
sessionId: params.sessionId,
|
|
1732
|
+
sessionKey: params.sessionKey,
|
|
1733
|
+
reason: `codex_app_server:${reason}`,
|
|
1734
|
+
});
|
|
1735
|
+
if (options?.arm) {
|
|
1736
|
+
turnCompletionIdleWatchArmed = true;
|
|
1737
|
+
turnCompletionIdleWatchPinnedByTerminalError = false;
|
|
1738
|
+
}
|
|
1739
|
+
scheduleTurnProgressWatches();
|
|
1740
|
+
};
|
|
1741
|
+
|
|
1742
|
+
const disarmTurnCompletionIdleWatch = () => {
|
|
1743
|
+
turnCompletionIdleWatchArmed = false;
|
|
1744
|
+
turnCompletionIdleWatchPinnedByTerminalError = false;
|
|
1745
|
+
turnCompletionIdleTimeoutOverrideMs = undefined;
|
|
1746
|
+
clearTurnCompletionIdleTimer();
|
|
1747
|
+
};
|
|
1748
|
+
|
|
1749
|
+
const disarmTurnAssistantCompletionIdleWatch = () => {
|
|
1750
|
+
turnAssistantCompletionIdleWatchArmed = false;
|
|
1751
|
+
turnAssistantCompletionLastActivityDetails = undefined;
|
|
1752
|
+
clearTurnAssistantCompletionIdleTimer();
|
|
1753
|
+
};
|
|
1754
|
+
|
|
1755
|
+
const armTurnAssistantCompletionIdleWatch = (details?: Record<string, unknown>) => {
|
|
1756
|
+
turnAssistantCompletionIdleWatchArmed = true;
|
|
1757
|
+
turnAssistantCompletionLastActivityAt = Date.now();
|
|
1758
|
+
turnAssistantCompletionLastActivityDetails = details;
|
|
1759
|
+
scheduleTurnAssistantCompletionIdleWatch();
|
|
1760
|
+
};
|
|
1761
|
+
|
|
1762
|
+
const armTurnCompletionIdleWatch = (options?: {
|
|
1763
|
+
pinnedByTerminalError?: boolean;
|
|
1764
|
+
timeoutMs?: number;
|
|
1765
|
+
}) => {
|
|
1766
|
+
turnCompletionIdleWatchArmed = true;
|
|
1767
|
+
turnCompletionIdleWatchPinnedByTerminalError = options?.pinnedByTerminalError === true;
|
|
1768
|
+
turnCompletionIdleTimeoutOverrideMs =
|
|
1769
|
+
options?.timeoutMs !== undefined ? Math.max(1, Math.floor(options.timeoutMs)) : undefined;
|
|
1770
|
+
scheduleTurnCompletionIdleWatch();
|
|
1771
|
+
};
|
|
1772
|
+
|
|
1773
|
+
const emitLifecycleStart = () => {
|
|
1774
|
+
emitCodexAppServerEvent(params, {
|
|
1775
|
+
stream: "lifecycle",
|
|
1776
|
+
data: { phase: "start", startedAt: attemptStartedAt },
|
|
1777
|
+
});
|
|
1778
|
+
lifecycleStarted = true;
|
|
1779
|
+
};
|
|
1780
|
+
|
|
1781
|
+
const emitLifecycleTerminal = (data: Record<string, unknown> & { phase: "end" | "error" }) => {
|
|
1782
|
+
if (!lifecycleStarted || lifecycleTerminalEmitted) {
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
emitCodexAppServerEvent(params, {
|
|
1786
|
+
stream: "lifecycle",
|
|
1787
|
+
data: {
|
|
1788
|
+
startedAt: attemptStartedAt,
|
|
1789
|
+
endedAt: Date.now(),
|
|
1790
|
+
...data,
|
|
1791
|
+
},
|
|
1792
|
+
});
|
|
1793
|
+
lifecycleTerminalEmitted = true;
|
|
1794
|
+
};
|
|
1795
|
+
|
|
1796
|
+
const executionPhaseKeys = new Set<string>();
|
|
1797
|
+
const emitExecutionPhaseOnce = (
|
|
1798
|
+
key: string,
|
|
1799
|
+
info: Parameters<NonNullable<EmbeddedRunAttemptParams["onExecutionPhase"]>>[0],
|
|
1800
|
+
) => {
|
|
1801
|
+
if (executionPhaseKeys.has(key)) {
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
executionPhaseKeys.add(key);
|
|
1805
|
+
params.onExecutionPhase?.({
|
|
1806
|
+
provider: params.provider,
|
|
1807
|
+
model: params.modelId,
|
|
1808
|
+
backend: "codex-app-server",
|
|
1809
|
+
...info,
|
|
1810
|
+
});
|
|
1811
|
+
};
|
|
1812
|
+
const reportCodexExecutionNotification = (notification: CodexServerNotification) => {
|
|
1813
|
+
if (notification.method === "turn/started") {
|
|
1814
|
+
emitExecutionPhaseOnce("turn_accepted", { phase: "turn_accepted" });
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
if (notification.method === "item/agentMessage/delta") {
|
|
1818
|
+
emitExecutionPhaseOnce("assistant_output_started", { phase: "assistant_output_started" });
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
if (notification.method !== "item/started") {
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
const item = readCodexNotificationItem(notification.params);
|
|
1825
|
+
const tool = item ? codexExecutionToolName(item) : undefined;
|
|
1826
|
+
if (!item || !tool) {
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
emitExecutionPhaseOnce(`tool:${item.id}`, {
|
|
1830
|
+
phase: "tool_execution_started",
|
|
1831
|
+
tool,
|
|
1832
|
+
itemId: item.id,
|
|
1833
|
+
});
|
|
1834
|
+
};
|
|
1835
|
+
|
|
1836
|
+
const isTerminalTurnNotificationForTurn = (
|
|
1837
|
+
notification: CodexServerNotification,
|
|
1838
|
+
notificationTurnId: string,
|
|
1839
|
+
): boolean => {
|
|
1840
|
+
if (!isTurnNotification(notification.params, thread.threadId, notificationTurnId)) {
|
|
1841
|
+
return false;
|
|
1842
|
+
}
|
|
1843
|
+
return (
|
|
1844
|
+
notification.method === "turn/completed" ||
|
|
1845
|
+
isCodexTurnAbortMarkerNotification(notification, {
|
|
1846
|
+
currentPromptTexts: [codexTurnPromptText],
|
|
1847
|
+
})
|
|
1848
|
+
);
|
|
1849
|
+
};
|
|
1850
|
+
|
|
1851
|
+
const handleNotification = async (notification: CodexServerNotification) => {
|
|
1852
|
+
userInputBridge?.handleNotification(notification);
|
|
1853
|
+
if (!projector || !turnId) {
|
|
1854
|
+
pendingNotifications.push(notification);
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
const isCurrentTurnNotification = isTurnNotification(
|
|
1858
|
+
notification.params,
|
|
1859
|
+
thread.threadId,
|
|
1860
|
+
turnId,
|
|
1861
|
+
);
|
|
1862
|
+
const isTurnCompletion = notification.method === "turn/completed" && isCurrentTurnNotification;
|
|
1863
|
+
if (isCurrentTurnNotification) {
|
|
1864
|
+
touchTurnCompletionActivity(`notification:${notification.method}`, {
|
|
1865
|
+
details: describeNotificationActivity(notification),
|
|
1866
|
+
attemptProgress: true,
|
|
1867
|
+
});
|
|
1868
|
+
reportCodexExecutionNotification(notification);
|
|
1869
|
+
}
|
|
1870
|
+
if (isCurrentTurnNotification) {
|
|
1871
|
+
updateActiveTurnItemIds(notification, activeTurnItemIds);
|
|
1872
|
+
}
|
|
1873
|
+
const unblockedAssistantCompletionRelease =
|
|
1874
|
+
isCurrentTurnNotification &&
|
|
1875
|
+
turnAssistantCompletionIdleWatchArmed &&
|
|
1876
|
+
notification.method === "item/completed" &&
|
|
1877
|
+
activeTurnItemIds.size === 0;
|
|
1878
|
+
const trackedDynamicToolCompletion = isPendingAutoBotDynamicToolCompletionNotification(
|
|
1879
|
+
notification,
|
|
1880
|
+
pendingAutoBotDynamicToolCompletionIds,
|
|
1881
|
+
);
|
|
1882
|
+
const rawToolOutputCompletion = isRawToolOutputCompletionNotification(notification);
|
|
1883
|
+
if (
|
|
1884
|
+
isCurrentTurnNotification &&
|
|
1885
|
+
(rawToolOutputCompletion || isNativeToolProgressNotification(notification))
|
|
1886
|
+
) {
|
|
1887
|
+
turnCrossedToolHandoff = true;
|
|
1888
|
+
}
|
|
1889
|
+
const assistantCompletionCanRelease = isAssistantCompletionReleaseNotification(
|
|
1890
|
+
notification,
|
|
1891
|
+
turnCrossedToolHandoff,
|
|
1892
|
+
);
|
|
1893
|
+
const postToolRawAssistantCompletionNeedsTerminalGuard =
|
|
1894
|
+
isCurrentTurnNotification &&
|
|
1895
|
+
turnCrossedToolHandoff &&
|
|
1896
|
+
isRawAssistantCompletionNotification(notification) &&
|
|
1897
|
+
activeTurnItemIds.size === 0;
|
|
1898
|
+
const shouldRearmCompletionIdleWatchAfterLastCurrentTurnItem =
|
|
1899
|
+
isCurrentTurnNotification &&
|
|
1900
|
+
notification.method === "item/completed" &&
|
|
1901
|
+
activeTurnItemIds.size === 0 &&
|
|
1902
|
+
!trackedDynamicToolCompletion &&
|
|
1903
|
+
!assistantCompletionCanRelease;
|
|
1904
|
+
if (isCurrentTurnNotification && notification.method === "error") {
|
|
1905
|
+
if (isRetryableErrorNotification(notification.params)) {
|
|
1906
|
+
disarmTurnCompletionIdleWatch();
|
|
1907
|
+
} else {
|
|
1908
|
+
armTurnCompletionIdleWatch({ pinnedByTerminalError: true });
|
|
1909
|
+
}
|
|
1910
|
+
disarmTurnAssistantCompletionIdleWatch();
|
|
1911
|
+
} else if (isTurnCompletion) {
|
|
1912
|
+
disarmTurnAssistantCompletionIdleWatch();
|
|
1913
|
+
} else if (isCurrentTurnNotification && assistantCompletionCanRelease) {
|
|
1914
|
+
armTurnAssistantCompletionIdleWatch(describeNotificationActivity(notification));
|
|
1915
|
+
} else if (postToolRawAssistantCompletionNeedsTerminalGuard) {
|
|
1916
|
+
armTurnCompletionIdleWatch({ timeoutMs: turnAssistantCompletionIdleTimeoutMs });
|
|
1917
|
+
} else if (unblockedAssistantCompletionRelease) {
|
|
1918
|
+
armTurnAssistantCompletionIdleWatch(describeNotificationActivity(notification));
|
|
1919
|
+
} else if (shouldRearmCompletionIdleWatchAfterLastCurrentTurnItem) {
|
|
1920
|
+
// If a non-assistant current-turn item is the last active item and the
|
|
1921
|
+
// bridge then goes quiet, reset the short completion-idle guard from that
|
|
1922
|
+
// final completion so the remaining silent-turn gap fails fast.
|
|
1923
|
+
armTurnCompletionIdleWatch();
|
|
1924
|
+
} else if (isCurrentTurnNotification && rawToolOutputCompletion) {
|
|
1925
|
+
// Raw OpenAI response streams can report the tool-output handoff without
|
|
1926
|
+
// a matching app-server `item/completed`; keep the post-tool guard alive.
|
|
1927
|
+
armTurnCompletionIdleWatch();
|
|
1928
|
+
} else if (
|
|
1929
|
+
isCurrentTurnNotification &&
|
|
1930
|
+
shouldDisarmAssistantCompletionIdleWatch(notification)
|
|
1931
|
+
) {
|
|
1932
|
+
disarmTurnAssistantCompletionIdleWatch();
|
|
1933
|
+
}
|
|
1934
|
+
if (
|
|
1935
|
+
turnCompletionIdleWatchArmed &&
|
|
1936
|
+
!turnCompletionIdleWatchPinnedByTerminalError &&
|
|
1937
|
+
notification.method !== "turn/completed" &&
|
|
1938
|
+
isCurrentTurnNotification &&
|
|
1939
|
+
!trackedDynamicToolCompletion &&
|
|
1940
|
+
!rawToolOutputCompletion &&
|
|
1941
|
+
!postToolRawAssistantCompletionNeedsTerminalGuard &&
|
|
1942
|
+
!shouldRearmCompletionIdleWatchAfterLastCurrentTurnItem
|
|
1943
|
+
) {
|
|
1944
|
+
// The short completion-idle watchdog guards blind gaps after Codex
|
|
1945
|
+
// accepts a turn or after AutoBot hands a turn-scoped request result
|
|
1946
|
+
// back to Codex. Bookkeeping that closes the just-served AutoBot
|
|
1947
|
+
// dynamic tool item is still part of that handoff, so keep the short
|
|
1948
|
+
// watchdog armed for that notification.
|
|
1949
|
+
disarmTurnCompletionIdleWatch();
|
|
1950
|
+
}
|
|
1951
|
+
if (trackedDynamicToolCompletion) {
|
|
1952
|
+
const itemId = readNotificationItemId(notification);
|
|
1953
|
+
if (itemId) {
|
|
1954
|
+
pendingAutoBotDynamicToolCompletionIds.delete(itemId);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
// Determine terminal-turn status before invoking the projector so a throw
|
|
1958
|
+
// inside projector.handleNotification still releases the session lane.
|
|
1959
|
+
// See autobot/autobot#67996.
|
|
1960
|
+
const isTurnAbortMarker =
|
|
1961
|
+
isCurrentTurnNotification &&
|
|
1962
|
+
isCodexTurnAbortMarkerNotification(notification, {
|
|
1963
|
+
currentPromptTexts: [codexTurnPromptText],
|
|
1964
|
+
});
|
|
1965
|
+
const isTurnTerminal = isTerminalTurnNotificationForTurn(notification, turnId);
|
|
1966
|
+
if (isTurnTerminal) {
|
|
1967
|
+
terminalTurnNotificationQueued = true;
|
|
1968
|
+
}
|
|
1969
|
+
try {
|
|
1970
|
+
await waitForCodexNotificationDispatchTurn();
|
|
1971
|
+
await projector.handleNotification(notification);
|
|
1972
|
+
} catch (error) {
|
|
1973
|
+
embeddedAgentLog.debug("codex app-server projector notification threw", {
|
|
1974
|
+
method: notification.method,
|
|
1975
|
+
error,
|
|
1976
|
+
});
|
|
1977
|
+
} finally {
|
|
1978
|
+
if (isTurnTerminal) {
|
|
1979
|
+
if (isTurnAbortMarker) {
|
|
1980
|
+
projector.markAborted();
|
|
1981
|
+
}
|
|
1982
|
+
if (!timedOut && !runAbortController.signal.aborted) {
|
|
1983
|
+
await steeringQueue?.flushPending();
|
|
1984
|
+
}
|
|
1985
|
+
completed = true;
|
|
1986
|
+
clearTurnCompletionIdleTimer();
|
|
1987
|
+
clearTurnAssistantCompletionIdleTimer();
|
|
1988
|
+
clearTurnTerminalIdleTimer();
|
|
1989
|
+
resolveCompletion?.();
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
};
|
|
1993
|
+
const enqueueNotification = (notification: CodexServerNotification): Promise<void> => {
|
|
1994
|
+
if (!projector || !turnId) {
|
|
1995
|
+
userInputBridge?.handleNotification(notification);
|
|
1996
|
+
pendingNotifications.push(notification);
|
|
1997
|
+
return Promise.resolve();
|
|
1998
|
+
}
|
|
1999
|
+
if (isTerminalTurnNotificationForTurn(notification, turnId)) {
|
|
2000
|
+
terminalTurnNotificationQueued = true;
|
|
2001
|
+
}
|
|
2002
|
+
notificationQueue = notificationQueue.then(
|
|
2003
|
+
() => handleNotification(notification),
|
|
2004
|
+
() => handleNotification(notification),
|
|
2005
|
+
);
|
|
2006
|
+
return notificationQueue;
|
|
2007
|
+
};
|
|
2008
|
+
|
|
2009
|
+
const notificationCleanup = client.addNotificationHandler(enqueueNotification);
|
|
2010
|
+
const requestCleanup = client.addRequestHandler(async (request) => {
|
|
2011
|
+
let armCompletionWatchOnResponse = false;
|
|
2012
|
+
let requestCountsAsTurnActivity = false;
|
|
2013
|
+
const markCurrentTurnRequestProgress = () => {
|
|
2014
|
+
activeAppServerTurnRequests += 1;
|
|
2015
|
+
clearTurnCompletionIdleTimer();
|
|
2016
|
+
disarmTurnAssistantCompletionIdleWatch();
|
|
2017
|
+
requestCountsAsTurnActivity = true;
|
|
2018
|
+
touchTurnCompletionActivity(`request:${request.method}:start`, {
|
|
2019
|
+
attemptProgress: true,
|
|
2020
|
+
});
|
|
2021
|
+
};
|
|
2022
|
+
try {
|
|
2023
|
+
if (request.method === "account/chatgptAuthTokens/refresh") {
|
|
2024
|
+
return refreshCodexAppServerAuthTokens({
|
|
2025
|
+
agentDir,
|
|
2026
|
+
authProfileId: startupAuthProfileId,
|
|
2027
|
+
config: params.config,
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
if (!turnId) {
|
|
2031
|
+
return undefined;
|
|
2032
|
+
}
|
|
2033
|
+
if (request.method === "mcpServer/elicitation/request") {
|
|
2034
|
+
if (isCurrentThreadOptionalTurnRequestParams(request.params, thread.threadId, turnId)) {
|
|
2035
|
+
armCompletionWatchOnResponse = true;
|
|
2036
|
+
markCurrentTurnRequestProgress();
|
|
2037
|
+
}
|
|
2038
|
+
return handleCodexAppServerElicitationRequest({
|
|
2039
|
+
requestParams: request.params,
|
|
2040
|
+
paramsForRun: params,
|
|
2041
|
+
threadId: thread.threadId,
|
|
2042
|
+
turnId,
|
|
2043
|
+
pluginAppPolicyContext: thread.pluginAppPolicyContext,
|
|
2044
|
+
signal: runAbortController.signal,
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
if (request.method === "item/tool/requestUserInput") {
|
|
2048
|
+
if (isCurrentThreadTurnRequestParams(request.params, thread.threadId, turnId)) {
|
|
2049
|
+
armCompletionWatchOnResponse = true;
|
|
2050
|
+
markCurrentTurnRequestProgress();
|
|
2051
|
+
}
|
|
2052
|
+
return userInputBridge?.handleRequest({
|
|
2053
|
+
id: request.id,
|
|
2054
|
+
params: request.params,
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
if (request.method !== "item/tool/call") {
|
|
2058
|
+
if (isCodexAppServerApprovalRequest(request.method)) {
|
|
2059
|
+
if (isCurrentApprovalTurnRequestParams(request.params, thread.threadId, turnId)) {
|
|
2060
|
+
armCompletionWatchOnResponse = true;
|
|
2061
|
+
markCurrentTurnRequestProgress();
|
|
2062
|
+
}
|
|
2063
|
+
return handleApprovalRequest({
|
|
2064
|
+
method: request.method,
|
|
2065
|
+
params: request.params,
|
|
2066
|
+
paramsForRun: params,
|
|
2067
|
+
threadId: thread.threadId,
|
|
2068
|
+
turnId,
|
|
2069
|
+
nativeHookRelay,
|
|
2070
|
+
signal: runAbortController.signal,
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
return undefined;
|
|
2074
|
+
}
|
|
2075
|
+
const call = readDynamicToolCallParams(request.params);
|
|
2076
|
+
if (!call || call.threadId !== thread.threadId || call.turnId !== turnId) {
|
|
2077
|
+
return undefined;
|
|
2078
|
+
}
|
|
2079
|
+
armCompletionWatchOnResponse = true;
|
|
2080
|
+
markCurrentTurnRequestProgress();
|
|
2081
|
+
turnCrossedToolHandoff = true;
|
|
2082
|
+
pendingAutoBotDynamicToolCompletionIds.add(call.callId);
|
|
2083
|
+
trajectoryRecorder?.recordEvent("tool.call", {
|
|
2084
|
+
threadId: call.threadId,
|
|
2085
|
+
turnId: call.turnId,
|
|
2086
|
+
toolCallId: call.callId,
|
|
2087
|
+
name: call.tool,
|
|
2088
|
+
arguments: call.arguments,
|
|
2089
|
+
});
|
|
2090
|
+
projector?.recordDynamicToolCall({
|
|
2091
|
+
callId: call.callId,
|
|
2092
|
+
tool: call.tool,
|
|
2093
|
+
arguments: call.arguments,
|
|
2094
|
+
});
|
|
2095
|
+
emitExecutionPhaseOnce(`tool:${call.callId}`, {
|
|
2096
|
+
phase: "tool_execution_started",
|
|
2097
|
+
tool: call.tool,
|
|
2098
|
+
toolCallId: call.callId,
|
|
2099
|
+
});
|
|
2100
|
+
emitDynamicToolStartedDiagnostic({
|
|
2101
|
+
call,
|
|
2102
|
+
runId: params.runId,
|
|
2103
|
+
sessionId: params.sessionId,
|
|
2104
|
+
sessionKey: params.sessionKey,
|
|
2105
|
+
});
|
|
2106
|
+
const toolProgressDetailMode = resolveCodexToolProgressDetailMode(params.toolProgressDetail);
|
|
2107
|
+
const toolMeta = inferCodexDynamicToolMeta(call, toolProgressDetailMode);
|
|
2108
|
+
const toolArgs = sanitizeCodexToolArguments(call.arguments);
|
|
2109
|
+
const shouldEmitDynamicToolProgress = shouldEmitTranscriptToolProgress(call.tool, toolArgs);
|
|
2110
|
+
if (shouldEmitDynamicToolProgress) {
|
|
2111
|
+
emitCodexAppServerEvent(params, {
|
|
2112
|
+
stream: "tool",
|
|
2113
|
+
data: {
|
|
2114
|
+
phase: "start",
|
|
2115
|
+
name: call.tool,
|
|
2116
|
+
toolCallId: call.callId,
|
|
2117
|
+
...(toolMeta ? { meta: toolMeta } : {}),
|
|
2118
|
+
...(toolArgs ? { args: toolArgs } : {}),
|
|
2119
|
+
},
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
const dynamicToolTimeoutMs = resolveDynamicToolCallTimeoutMs({
|
|
2123
|
+
call,
|
|
2124
|
+
config: params.config,
|
|
2125
|
+
});
|
|
2126
|
+
const toolStartedAt = Date.now();
|
|
2127
|
+
let terminalDiagnosticObserved = false;
|
|
2128
|
+
const unsubscribeToolDiagnosticObserver = onInternalDiagnosticEvent((event) => {
|
|
2129
|
+
if (isDynamicToolTerminalDiagnosticEvent(event)) {
|
|
2130
|
+
if (
|
|
2131
|
+
isMatchingDynamicToolTerminalDiagnostic({
|
|
2132
|
+
event,
|
|
2133
|
+
call,
|
|
2134
|
+
runId: params.runId,
|
|
2135
|
+
sessionId: params.sessionId,
|
|
2136
|
+
sessionKey: params.sessionKey,
|
|
2137
|
+
})
|
|
2138
|
+
) {
|
|
2139
|
+
terminalDiagnosticObserved = true;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
try {
|
|
2144
|
+
const response = await handleDynamicToolCallWithTimeout({
|
|
2145
|
+
call,
|
|
2146
|
+
toolBridge,
|
|
2147
|
+
signal: runAbortController.signal,
|
|
2148
|
+
timeoutMs: dynamicToolTimeoutMs,
|
|
2149
|
+
onTimeout: () => {
|
|
2150
|
+
trajectoryRecorder?.recordEvent("tool.timeout", {
|
|
2151
|
+
threadId: call.threadId,
|
|
2152
|
+
turnId: call.turnId,
|
|
2153
|
+
toolCallId: call.callId,
|
|
2154
|
+
name: call.tool,
|
|
2155
|
+
timeoutMs: dynamicToolTimeoutMs,
|
|
2156
|
+
});
|
|
2157
|
+
},
|
|
2158
|
+
});
|
|
2159
|
+
const protocolResponse = toCodexDynamicToolProtocolResponse(response);
|
|
2160
|
+
trajectoryRecorder?.recordEvent("tool.result", {
|
|
2161
|
+
threadId: call.threadId,
|
|
2162
|
+
turnId: call.turnId,
|
|
2163
|
+
toolCallId: call.callId,
|
|
2164
|
+
name: call.tool,
|
|
2165
|
+
success: protocolResponse.success,
|
|
2166
|
+
contentItems: protocolResponse.contentItems,
|
|
2167
|
+
});
|
|
2168
|
+
projector?.recordDynamicToolResult({
|
|
2169
|
+
callId: call.callId,
|
|
2170
|
+
tool: call.tool,
|
|
2171
|
+
success: protocolResponse.success,
|
|
2172
|
+
contentItems: protocolResponse.contentItems,
|
|
2173
|
+
});
|
|
2174
|
+
if (shouldEmitDynamicToolProgress) {
|
|
2175
|
+
emitCodexAppServerEvent(params, {
|
|
2176
|
+
stream: "tool",
|
|
2177
|
+
data: {
|
|
2178
|
+
phase: "result",
|
|
2179
|
+
name: call.tool,
|
|
2180
|
+
toolCallId: call.callId,
|
|
2181
|
+
...(toolMeta ? { meta: toolMeta } : {}),
|
|
2182
|
+
isError: !protocolResponse.success,
|
|
2183
|
+
result: sanitizeCodexToolResponse(protocolResponse),
|
|
2184
|
+
},
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
if (
|
|
2188
|
+
!terminalDiagnosticObserved &&
|
|
2189
|
+
!hasPendingDynamicToolTerminalDiagnostic({
|
|
2190
|
+
call,
|
|
2191
|
+
runId: params.runId,
|
|
2192
|
+
sessionId: params.sessionId,
|
|
2193
|
+
sessionKey: params.sessionKey,
|
|
2194
|
+
})
|
|
2195
|
+
) {
|
|
2196
|
+
emitDynamicToolTerminalDiagnostic({
|
|
2197
|
+
response,
|
|
2198
|
+
call,
|
|
2199
|
+
runId: params.runId,
|
|
2200
|
+
sessionId: params.sessionId,
|
|
2201
|
+
sessionKey: params.sessionKey,
|
|
2202
|
+
durationMs: Math.max(0, Date.now() - toolStartedAt),
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
return protocolResponse as JsonValue;
|
|
2206
|
+
} catch (error) {
|
|
2207
|
+
if (
|
|
2208
|
+
!terminalDiagnosticObserved &&
|
|
2209
|
+
!hasPendingDynamicToolTerminalDiagnostic({
|
|
2210
|
+
call,
|
|
2211
|
+
runId: params.runId,
|
|
2212
|
+
sessionId: params.sessionId,
|
|
2213
|
+
sessionKey: params.sessionKey,
|
|
2214
|
+
})
|
|
2215
|
+
) {
|
|
2216
|
+
emitDynamicToolErrorDiagnostic({
|
|
2217
|
+
call,
|
|
2218
|
+
runId: params.runId,
|
|
2219
|
+
sessionId: params.sessionId,
|
|
2220
|
+
sessionKey: params.sessionKey,
|
|
2221
|
+
durationMs: Math.max(0, Date.now() - toolStartedAt),
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
throw error;
|
|
2225
|
+
} finally {
|
|
2226
|
+
unsubscribeToolDiagnosticObserver();
|
|
2227
|
+
}
|
|
2228
|
+
} finally {
|
|
2229
|
+
if (requestCountsAsTurnActivity) {
|
|
2230
|
+
activeAppServerTurnRequests = Math.max(0, activeAppServerTurnRequests - 1);
|
|
2231
|
+
touchTurnCompletionActivity(`request:${request.method}:response`, {
|
|
2232
|
+
arm: armCompletionWatchOnResponse,
|
|
2233
|
+
attemptProgress: true,
|
|
2234
|
+
});
|
|
2235
|
+
} else {
|
|
2236
|
+
scheduleTurnProgressWatches();
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
});
|
|
2240
|
+
let closeCleanup: (() => void) | undefined;
|
|
2241
|
+
|
|
2242
|
+
const forceContextEngineCompactionForCodexOverflow = async (error: unknown): Promise<boolean> => {
|
|
2243
|
+
if (!activeContextEngine?.info.ownsCompaction) {
|
|
2244
|
+
return false;
|
|
2245
|
+
}
|
|
2246
|
+
embeddedAgentLog.warn(
|
|
2247
|
+
"codex app-server context-engine turn overflowed; forcing context-engine compaction",
|
|
2248
|
+
{
|
|
2249
|
+
sessionId: activeSessionId,
|
|
2250
|
+
sessionKey: sandboxSessionKey,
|
|
2251
|
+
threadId: thread.threadId,
|
|
2252
|
+
engineId: activeContextEngine.info.id,
|
|
2253
|
+
tokenBudget: params.contextTokenBudget,
|
|
2254
|
+
error: formatErrorMessage(error),
|
|
2255
|
+
},
|
|
2256
|
+
);
|
|
2257
|
+
try {
|
|
2258
|
+
const runtimeContext = buildActiveContextEngineRuntimeContext();
|
|
2259
|
+
const overflowTokenCount = params.contextTokenBudget ?? params.contextWindowInfo?.tokens;
|
|
2260
|
+
// Bound the plugin-owned compaction with the same finite safety timeout
|
|
2261
|
+
// that protects native runtime compaction, and thread the run-level
|
|
2262
|
+
// abort signal through, so a slow/hung plugin compact() cannot stall
|
|
2263
|
+
// Codex overflow recovery indefinitely. A timeout/abort surfaces as a
|
|
2264
|
+
// thrown error handled by the catch below.
|
|
2265
|
+
const compactResult = await compactContextEngineWithSafetyTimeout(
|
|
2266
|
+
activeContextEngine,
|
|
2267
|
+
{
|
|
2268
|
+
sessionId: activeSessionId,
|
|
2269
|
+
sessionKey: sandboxSessionKey,
|
|
2270
|
+
sessionFile: activeSessionFile,
|
|
2271
|
+
tokenBudget: params.contextTokenBudget,
|
|
2272
|
+
force: true,
|
|
2273
|
+
...(overflowTokenCount ? { currentTokenCount: overflowTokenCount } : {}),
|
|
2274
|
+
compactionTarget: "threshold",
|
|
2275
|
+
runtimeContext: overflowTokenCount
|
|
2276
|
+
? {
|
|
2277
|
+
...runtimeContext,
|
|
2278
|
+
currentTokenCount: overflowTokenCount,
|
|
2279
|
+
}
|
|
2280
|
+
: runtimeContext,
|
|
2281
|
+
},
|
|
2282
|
+
resolveCompactionTimeoutMs(params.config),
|
|
2283
|
+
runAbortController.signal,
|
|
2284
|
+
);
|
|
2285
|
+
embeddedAgentLog.info("codex app-server context-engine forced compaction result", {
|
|
2286
|
+
sessionId: activeSessionId,
|
|
2287
|
+
sessionKey: sandboxSessionKey,
|
|
2288
|
+
engineId: activeContextEngine.info.id,
|
|
2289
|
+
ok: compactResult.ok,
|
|
2290
|
+
compacted: compactResult.compacted,
|
|
2291
|
+
reason: compactResult.reason,
|
|
2292
|
+
tokensBefore: compactResult.result?.tokensBefore,
|
|
2293
|
+
tokensAfter: compactResult.result?.tokensAfter,
|
|
2294
|
+
});
|
|
2295
|
+
if (!compactResult.ok || !compactResult.compacted) {
|
|
2296
|
+
return false;
|
|
2297
|
+
}
|
|
2298
|
+
adoptContextEngineCompactionTranscript(compactResult);
|
|
2299
|
+
const maintenanceRuntimeContext = buildActiveContextEngineRuntimeContext();
|
|
2300
|
+
await runHarnessContextEngineMaintenance({
|
|
2301
|
+
contextEngine: activeContextEngine,
|
|
2302
|
+
sessionId: activeSessionId,
|
|
2303
|
+
sessionKey: sandboxSessionKey,
|
|
2304
|
+
sessionFile: activeSessionFile,
|
|
2305
|
+
reason: "compaction",
|
|
2306
|
+
runtimeContext: maintenanceRuntimeContext,
|
|
2307
|
+
config: params.config,
|
|
2308
|
+
});
|
|
2309
|
+
return true;
|
|
2310
|
+
} catch (compactErr) {
|
|
2311
|
+
embeddedAgentLog.warn("codex app-server context-engine forced compaction failed", {
|
|
2312
|
+
sessionId: params.sessionId,
|
|
2313
|
+
sessionKey: sandboxSessionKey,
|
|
2314
|
+
engineId: activeContextEngine.info.id,
|
|
2315
|
+
error: formatErrorMessage(compactErr),
|
|
2316
|
+
});
|
|
2317
|
+
return false;
|
|
2318
|
+
}
|
|
2319
|
+
};
|
|
2320
|
+
const rebuildPromptAfterContextEngineCompaction = async () => {
|
|
2321
|
+
historyMessages =
|
|
2322
|
+
(await readMirroredSessionHistoryMessages(activeSessionFile)) ?? historyMessages;
|
|
2323
|
+
resetCodexPromptInputs();
|
|
2324
|
+
try {
|
|
2325
|
+
await applyActiveContextEngineProjection(undefined);
|
|
2326
|
+
} catch (assembleErr) {
|
|
2327
|
+
embeddedAgentLog.warn(
|
|
2328
|
+
"context engine assemble failed after forced compaction; using Codex baseline prompt",
|
|
2329
|
+
{
|
|
2330
|
+
error: formatErrorMessage(assembleErr),
|
|
2331
|
+
},
|
|
2332
|
+
);
|
|
2333
|
+
}
|
|
2334
|
+
promptBuild = await buildPromptFromCurrentInputs();
|
|
2335
|
+
refreshCodexTurnPromptText();
|
|
2336
|
+
};
|
|
2337
|
+
const buildLlmInputEvent = () => ({
|
|
2338
|
+
runId: params.runId,
|
|
2339
|
+
sessionId: params.sessionId,
|
|
2340
|
+
provider: params.provider,
|
|
2341
|
+
model: params.modelId,
|
|
2342
|
+
systemPrompt: promptBuild.developerInstructions,
|
|
2343
|
+
prompt: codexTurnPromptText,
|
|
2344
|
+
historyMessages,
|
|
2345
|
+
imagesCount: params.images?.length ?? 0,
|
|
2346
|
+
});
|
|
2347
|
+
const buildTurnStartFailureMessages = () => [
|
|
2348
|
+
...historyMessages,
|
|
2349
|
+
buildCodexUserPromptMessage({ ...params, prompt: codexTurnPromptText }),
|
|
2350
|
+
];
|
|
2351
|
+
|
|
2352
|
+
let turn: CodexTurnStartResponse | undefined;
|
|
2353
|
+
const startCodexTurn = async (): Promise<CodexTurnStartResponse> =>
|
|
2354
|
+
assertCodexTurnStartResponse(
|
|
2355
|
+
await client.request(
|
|
2356
|
+
"turn/start",
|
|
2357
|
+
buildTurnStartParams(params, {
|
|
2358
|
+
threadId: thread.threadId,
|
|
2359
|
+
cwd: effectiveWorkspace,
|
|
2360
|
+
appServer: pluginAppServer,
|
|
2361
|
+
promptText: codexTurnPromptText,
|
|
2362
|
+
sandboxPolicy: codexSandboxPolicy,
|
|
2363
|
+
heartbeatCollaborationInstructions:
|
|
2364
|
+
workspaceBootstrapContext.heartbeatCollaborationInstructions,
|
|
2365
|
+
}),
|
|
2366
|
+
{ timeoutMs: params.timeoutMs, signal: runAbortController.signal },
|
|
2367
|
+
),
|
|
2368
|
+
);
|
|
2369
|
+
try {
|
|
2370
|
+
runAgentHarnessLlmInputHook({
|
|
2371
|
+
event: buildLlmInputEvent(),
|
|
2372
|
+
ctx: hookContext,
|
|
2373
|
+
});
|
|
2374
|
+
emitCodexAppServerEvent(params, {
|
|
2375
|
+
stream: "codex_app_server.lifecycle",
|
|
2376
|
+
data: { phase: "turn_starting", threadId: thread.threadId },
|
|
2377
|
+
});
|
|
2378
|
+
turn = await startCodexTurn();
|
|
2379
|
+
} catch (error) {
|
|
2380
|
+
let turnStartError = error;
|
|
2381
|
+
if (
|
|
2382
|
+
shouldRetryContextEngineTurnOnFreshCodexThread({
|
|
2383
|
+
error: turnStartError,
|
|
2384
|
+
contextEngineActive: Boolean(activeContextEngine),
|
|
2385
|
+
thread,
|
|
2386
|
+
}) &&
|
|
2387
|
+
restartContextEngineCodexThread
|
|
2388
|
+
) {
|
|
2389
|
+
embeddedAgentLog.warn(
|
|
2390
|
+
"codex app-server context-engine turn overflowed on resume; retrying with fresh thread",
|
|
2391
|
+
{
|
|
2392
|
+
threadId: thread.threadId,
|
|
2393
|
+
error: formatErrorMessage(turnStartError),
|
|
2394
|
+
},
|
|
2395
|
+
);
|
|
2396
|
+
const preRetrySessionFile = activeSessionFile;
|
|
2397
|
+
const compactedForRetry = await forceContextEngineCompactionForCodexOverflow(turnStartError);
|
|
2398
|
+
await clearCodexAppServerBinding(preRetrySessionFile);
|
|
2399
|
+
if (activeSessionFile !== preRetrySessionFile) {
|
|
2400
|
+
await clearCodexAppServerBinding(activeSessionFile);
|
|
2401
|
+
}
|
|
2402
|
+
if (compactedForRetry) {
|
|
2403
|
+
await rebuildPromptAfterContextEngineCompaction();
|
|
2404
|
+
}
|
|
2405
|
+
thread = await restartContextEngineCodexThread();
|
|
2406
|
+
emitCodexAppServerEvent(params, {
|
|
2407
|
+
stream: "codex_app_server.lifecycle",
|
|
2408
|
+
data: { phase: "thread_ready_retry", threadId: thread.threadId },
|
|
2409
|
+
});
|
|
2410
|
+
try {
|
|
2411
|
+
turn = await startCodexTurn();
|
|
2412
|
+
} catch (retryError) {
|
|
2413
|
+
turnStartError = retryError;
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
if (turn === undefined) {
|
|
2417
|
+
const usageLimitError = await formatCodexTurnStartUsageLimitError({
|
|
2418
|
+
client,
|
|
2419
|
+
error: turnStartError,
|
|
2420
|
+
pendingNotifications,
|
|
2421
|
+
timeoutMs: appServer.requestTimeoutMs,
|
|
2422
|
+
signal: runAbortController.signal,
|
|
2423
|
+
});
|
|
2424
|
+
const turnStartErrorMessage = usageLimitError?.message ?? formatErrorMessage(turnStartError);
|
|
2425
|
+
if (isInvalidCodexImagePayloadError(turnStartErrorMessage)) {
|
|
2426
|
+
await clearCodexBindingAfterInvalidImagePayload(activeSessionFile, {
|
|
2427
|
+
phase: "turn_start",
|
|
2428
|
+
threadId: thread.threadId,
|
|
2429
|
+
error: turnStartErrorMessage,
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
emitCodexAppServerEvent(params, {
|
|
2433
|
+
stream: "codex_app_server.lifecycle",
|
|
2434
|
+
data: { phase: "turn_start_failed", error: turnStartErrorMessage },
|
|
2435
|
+
});
|
|
2436
|
+
trajectoryRecorder?.recordEvent("session.ended", {
|
|
2437
|
+
status: "error",
|
|
2438
|
+
threadId: thread.threadId,
|
|
2439
|
+
timedOut,
|
|
2440
|
+
aborted: runAbortController.signal.aborted,
|
|
2441
|
+
promptError: turnStartErrorMessage,
|
|
2442
|
+
});
|
|
2443
|
+
trajectoryEndRecorded = true;
|
|
2444
|
+
runAgentHarnessLlmOutputHook({
|
|
2445
|
+
event: {
|
|
2446
|
+
runId: params.runId,
|
|
2447
|
+
sessionId: params.sessionId,
|
|
2448
|
+
provider: params.provider,
|
|
2449
|
+
model: params.modelId,
|
|
2450
|
+
...hookContextWindowFields,
|
|
2451
|
+
resolvedRef:
|
|
2452
|
+
params.runtimePlan?.observability.resolvedRef ?? `${params.provider}/${params.modelId}`,
|
|
2453
|
+
...(params.runtimePlan?.observability.harnessId
|
|
2454
|
+
? { harnessId: params.runtimePlan.observability.harnessId }
|
|
2455
|
+
: {}),
|
|
2456
|
+
assistantTexts: [],
|
|
2457
|
+
},
|
|
2458
|
+
ctx: hookContext,
|
|
2459
|
+
});
|
|
2460
|
+
runAgentHarnessAgentEndHook({
|
|
2461
|
+
event: {
|
|
2462
|
+
messages: buildTurnStartFailureMessages(),
|
|
2463
|
+
success: false,
|
|
2464
|
+
error: turnStartErrorMessage,
|
|
2465
|
+
durationMs: Date.now() - attemptStartedAt,
|
|
2466
|
+
},
|
|
2467
|
+
ctx: hookContext,
|
|
2468
|
+
});
|
|
2469
|
+
notificationCleanup();
|
|
2470
|
+
requestCleanup();
|
|
2471
|
+
nativeHookRelay?.unregister();
|
|
2472
|
+
await runAgentCleanupStep({
|
|
2473
|
+
runId: params.runId,
|
|
2474
|
+
sessionId: params.sessionId,
|
|
2475
|
+
step: "codex-trajectory-flush-startup-failure",
|
|
2476
|
+
log: embeddedAgentLog,
|
|
2477
|
+
cleanup: async () => {
|
|
2478
|
+
await trajectoryRecorder?.flush();
|
|
2479
|
+
},
|
|
2480
|
+
});
|
|
2481
|
+
params.abortSignal?.removeEventListener("abort", abortFromUpstream);
|
|
2482
|
+
if (usageLimitError) {
|
|
2483
|
+
await markCodexAuthProfileBlockedFromRateLimits({
|
|
2484
|
+
params,
|
|
2485
|
+
authProfileId: startupAuthProfileId,
|
|
2486
|
+
rateLimits: usageLimitError.rateLimitsForProfile,
|
|
2487
|
+
});
|
|
2488
|
+
return buildCodexTurnStartFailureResult({
|
|
2489
|
+
params,
|
|
2490
|
+
message: usageLimitError.message,
|
|
2491
|
+
messagesSnapshot: buildTurnStartFailureMessages(),
|
|
2492
|
+
systemPromptReport,
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
throw turnStartError;
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
if (!turn) {
|
|
2499
|
+
throw new Error("codex app-server turn/start failed without an error");
|
|
2500
|
+
}
|
|
2501
|
+
turnId = turn.turn.id;
|
|
2502
|
+
const activeTurnId = turn.turn.id;
|
|
2503
|
+
emitExecutionPhaseOnce("turn_accepted", { phase: "turn_accepted" });
|
|
2504
|
+
userInputBridge = createCodexUserInputBridge({
|
|
2505
|
+
paramsForRun: params,
|
|
2506
|
+
threadId: thread.threadId,
|
|
2507
|
+
turnId: activeTurnId,
|
|
2508
|
+
signal: runAbortController.signal,
|
|
2509
|
+
});
|
|
2510
|
+
trajectoryRecorder?.recordEvent("prompt.submitted", {
|
|
2511
|
+
threadId: thread.threadId,
|
|
2512
|
+
turnId: activeTurnId,
|
|
2513
|
+
prompt: codexTurnPromptText,
|
|
2514
|
+
imagesCount: params.images?.length ?? 0,
|
|
2515
|
+
});
|
|
2516
|
+
projector = new CodexAppServerEventProjector(params, thread.threadId, activeTurnId, {
|
|
2517
|
+
nativePostToolUseRelayEnabled:
|
|
2518
|
+
nativeHookRelay?.allowedEvents.includes("post_tool_use") === true &&
|
|
2519
|
+
nativeHookRelay.shouldRelayEvent("post_tool_use"),
|
|
2520
|
+
trajectoryRecorder,
|
|
2521
|
+
});
|
|
2522
|
+
if (
|
|
2523
|
+
isTerminalTurnStatus(turn.turn.status) ||
|
|
2524
|
+
pendingNotifications.some((notification) =>
|
|
2525
|
+
isTerminalTurnNotificationForTurn(notification, activeTurnId),
|
|
2526
|
+
)
|
|
2527
|
+
) {
|
|
2528
|
+
terminalTurnNotificationQueued = true;
|
|
2529
|
+
}
|
|
2530
|
+
closeCleanup = (
|
|
2531
|
+
client as {
|
|
2532
|
+
addCloseHandler?: (handler: (client: CodexAppServerClient) => void) => () => void;
|
|
2533
|
+
}
|
|
2534
|
+
).addCloseHandler?.(() => {
|
|
2535
|
+
if (completed || terminalTurnNotificationQueued || runAbortController.signal.aborted) {
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
clientClosedPromptError = "codex app-server client closed before turn completed";
|
|
2539
|
+
trajectoryRecorder?.recordEvent("turn.client_closed", {
|
|
2540
|
+
threadId: thread.threadId,
|
|
2541
|
+
turnId: activeTurnId,
|
|
2542
|
+
});
|
|
2543
|
+
embeddedAgentLog.warn("codex app-server client closed before turn completed", {
|
|
2544
|
+
threadId: thread.threadId,
|
|
2545
|
+
turnId: activeTurnId,
|
|
2546
|
+
});
|
|
2547
|
+
clientClosedAbort = true;
|
|
2548
|
+
runAbortController.abort("client_closed");
|
|
2549
|
+
completed = true;
|
|
2550
|
+
clearTurnAttemptIdleTimer();
|
|
2551
|
+
clearTurnCompletionIdleTimer();
|
|
2552
|
+
clearTurnAssistantCompletionIdleTimer();
|
|
2553
|
+
clearTurnTerminalIdleTimer();
|
|
2554
|
+
resolveCompletion?.();
|
|
2555
|
+
});
|
|
2556
|
+
emitLifecycleStart();
|
|
2557
|
+
const activeProjector = projector;
|
|
2558
|
+
turnTerminalIdleWatchArmed = true;
|
|
2559
|
+
touchTurnCompletionActivity("turn:start", { arm: true });
|
|
2560
|
+
for (const notification of pendingNotifications.splice(0)) {
|
|
2561
|
+
await enqueueNotification(notification);
|
|
2562
|
+
}
|
|
2563
|
+
if (!completed && isTerminalTurnStatus(turn.turn.status)) {
|
|
2564
|
+
await enqueueNotification({
|
|
2565
|
+
method: "turn/completed",
|
|
2566
|
+
params: {
|
|
2567
|
+
threadId: thread.threadId,
|
|
2568
|
+
turnId: activeTurnId,
|
|
2569
|
+
turn: turn.turn as unknown as JsonObject,
|
|
2570
|
+
},
|
|
2571
|
+
});
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
const activeSteeringQueue = createCodexSteeringQueue({
|
|
2575
|
+
client,
|
|
2576
|
+
threadId: thread.threadId,
|
|
2577
|
+
turnId: activeTurnId,
|
|
2578
|
+
answerPendingUserInput: (text) => userInputBridge?.handleQueuedMessage(text) ?? false,
|
|
2579
|
+
signal: runAbortController.signal,
|
|
2580
|
+
});
|
|
2581
|
+
steeringQueue = activeSteeringQueue;
|
|
2582
|
+
const handle = {
|
|
2583
|
+
kind: "embedded" as const,
|
|
2584
|
+
queueMessage: async (text: string, options?: CodexSteeringQueueOptions) =>
|
|
2585
|
+
activeSteeringQueue.queue(text, options),
|
|
2586
|
+
isStreaming: () => !completed,
|
|
2587
|
+
isCompacting: () => projector?.isCompacting() ?? false,
|
|
2588
|
+
cancel: () => runAbortController.abort("cancelled"),
|
|
2589
|
+
abort: () => runAbortController.abort("aborted"),
|
|
2590
|
+
};
|
|
2591
|
+
setActiveEmbeddedRun(params.sessionId, handle, params.sessionKey);
|
|
2592
|
+
turnAttemptIdleWatchArmed = true;
|
|
2593
|
+
turnTerminalIdleWatchArmed = true;
|
|
2594
|
+
touchTurnCompletionActivity("turn:start", { attemptProgress: true });
|
|
2595
|
+
|
|
2596
|
+
const abortListener = () => {
|
|
2597
|
+
const shouldRetireClient = timedOut;
|
|
2598
|
+
interruptCodexTurnBestEffort(client, {
|
|
2599
|
+
threadId: thread.threadId,
|
|
2600
|
+
turnId: activeTurnId,
|
|
2601
|
+
timeoutMs: shouldRetireClient ? CODEX_APP_SERVER_INTERRUPT_TIMEOUT_MS : undefined,
|
|
2602
|
+
});
|
|
2603
|
+
if (shouldRetireClient) {
|
|
2604
|
+
retireCodexAppServerClientAfterTimedOutTurn(client, {
|
|
2605
|
+
threadId: thread.threadId,
|
|
2606
|
+
turnId: activeTurnId,
|
|
2607
|
+
reason: String(runAbortController.signal.reason ?? "timeout"),
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
resolveCompletion?.();
|
|
2611
|
+
};
|
|
2612
|
+
runAbortController.signal.addEventListener("abort", abortListener, { once: true });
|
|
2613
|
+
if (runAbortController.signal.aborted) {
|
|
2614
|
+
abortListener();
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
try {
|
|
2618
|
+
await completion;
|
|
2619
|
+
const result = activeProjector.buildResult(toolBridge.telemetry, { yieldDetected });
|
|
2620
|
+
const finalAborted =
|
|
2621
|
+
result.aborted || (runAbortController.signal.aborted && !clientClosedAbort);
|
|
2622
|
+
let finalPromptError =
|
|
2623
|
+
clientClosedPromptError ??
|
|
2624
|
+
(turnCompletionIdleTimedOut
|
|
2625
|
+
? turnCompletionIdleTimeoutMessage
|
|
2626
|
+
: timedOut
|
|
2627
|
+
? "codex app-server attempt timed out"
|
|
2628
|
+
: result.promptError);
|
|
2629
|
+
const finalPromptErrorMessage =
|
|
2630
|
+
typeof finalPromptError === "string"
|
|
2631
|
+
? finalPromptError
|
|
2632
|
+
: finalPromptError
|
|
2633
|
+
? formatErrorMessage(finalPromptError)
|
|
2634
|
+
: undefined;
|
|
2635
|
+
if (isInvalidCodexImagePayloadError(finalPromptErrorMessage)) {
|
|
2636
|
+
await clearCodexBindingAfterInvalidImagePayload(activeSessionFile, {
|
|
2637
|
+
phase: "turn_completed",
|
|
2638
|
+
threadId: thread.threadId,
|
|
2639
|
+
turnId: activeTurnId,
|
|
2640
|
+
error: finalPromptErrorMessage,
|
|
2641
|
+
});
|
|
2642
|
+
}
|
|
2643
|
+
if (shouldRefreshCodexRateLimitsForUsageLimitMessage(finalPromptErrorMessage)) {
|
|
2644
|
+
finalPromptError = await refreshCodexUsageLimitErrorMessage({
|
|
2645
|
+
client,
|
|
2646
|
+
source: {
|
|
2647
|
+
message: finalPromptErrorMessage,
|
|
2648
|
+
codexErrorInfo: "usageLimitExceeded",
|
|
2649
|
+
rateLimits: readRecentCodexRateLimits(),
|
|
2650
|
+
},
|
|
2651
|
+
timeoutMs: appServer.requestTimeoutMs,
|
|
2652
|
+
signal: runAbortController.signal,
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
const finalPromptErrorSource =
|
|
2656
|
+
timedOut || clientClosedPromptError ? "prompt" : result.promptErrorSource;
|
|
2657
|
+
recordCodexTrajectoryCompletion(trajectoryRecorder, {
|
|
2658
|
+
attempt: params,
|
|
2659
|
+
result,
|
|
2660
|
+
threadId: thread.threadId,
|
|
2661
|
+
turnId: activeTurnId,
|
|
2662
|
+
timedOut,
|
|
2663
|
+
yieldDetected,
|
|
2664
|
+
});
|
|
2665
|
+
trajectoryRecorder?.recordEvent("session.ended", {
|
|
2666
|
+
status: finalPromptError ? "error" : finalAborted || timedOut ? "interrupted" : "success",
|
|
2667
|
+
threadId: thread.threadId,
|
|
2668
|
+
turnId: activeTurnId,
|
|
2669
|
+
timedOut,
|
|
2670
|
+
yieldDetected,
|
|
2671
|
+
promptError: normalizeCodexTrajectoryError(finalPromptError),
|
|
2672
|
+
});
|
|
2673
|
+
trajectoryEndRecorded = true;
|
|
2674
|
+
await mirrorTranscriptBestEffort({
|
|
2675
|
+
params,
|
|
2676
|
+
agentId: sessionAgentId,
|
|
2677
|
+
result,
|
|
2678
|
+
sessionKey: sandboxSessionKey,
|
|
2679
|
+
threadId: thread.threadId,
|
|
2680
|
+
turnId: activeTurnId,
|
|
2681
|
+
});
|
|
2682
|
+
const terminalAssistantText = collectTerminalAssistantText(result);
|
|
2683
|
+
if (terminalAssistantText && !finalAborted && !finalPromptError) {
|
|
2684
|
+
emitCodexAppServerEvent(params, {
|
|
2685
|
+
stream: "assistant",
|
|
2686
|
+
data: { text: terminalAssistantText },
|
|
2687
|
+
});
|
|
2688
|
+
}
|
|
2689
|
+
if (finalPromptError) {
|
|
2690
|
+
emitLifecycleTerminal({
|
|
2691
|
+
phase: "error",
|
|
2692
|
+
error: formatErrorMessage(finalPromptError),
|
|
2693
|
+
});
|
|
2694
|
+
} else {
|
|
2695
|
+
emitLifecycleTerminal({
|
|
2696
|
+
phase: "end",
|
|
2697
|
+
...(finalAborted ? { aborted: true } : {}),
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
if (activeContextEngine) {
|
|
2701
|
+
const activeContextEnginePluginId = resolveContextEngineOwnerPluginId(activeContextEngine);
|
|
2702
|
+
const finalMessages =
|
|
2703
|
+
(await readMirroredSessionHistoryMessages(activeSessionFile)) ??
|
|
2704
|
+
historyMessages.concat(result.messagesSnapshot);
|
|
2705
|
+
await finalizeHarnessContextEngineTurn({
|
|
2706
|
+
contextEngine: activeContextEngine,
|
|
2707
|
+
promptError: Boolean(finalPromptError),
|
|
2708
|
+
aborted: finalAborted,
|
|
2709
|
+
yieldAborted: Boolean(result.yieldDetected),
|
|
2710
|
+
sessionIdUsed: activeSessionId,
|
|
2711
|
+
sessionKey: sandboxSessionKey,
|
|
2712
|
+
sessionFile: activeSessionFile,
|
|
2713
|
+
messagesSnapshot: finalMessages,
|
|
2714
|
+
prePromptMessageCount,
|
|
2715
|
+
tokenBudget: params.contextTokenBudget,
|
|
2716
|
+
runtimeContext: buildHarnessContextEngineRuntimeContextFromUsage({
|
|
2717
|
+
attempt: buildActiveRunAttemptParams(),
|
|
2718
|
+
workspaceDir: effectiveWorkspace,
|
|
2719
|
+
agentDir,
|
|
2720
|
+
activeAgentId: sessionAgentId,
|
|
2721
|
+
contextEnginePluginId: activeContextEnginePluginId,
|
|
2722
|
+
tokenBudget: params.contextTokenBudget,
|
|
2723
|
+
lastCallUsage: result.attemptUsage,
|
|
2724
|
+
promptCache: result.promptCache,
|
|
2725
|
+
}),
|
|
2726
|
+
runMaintenance: runHarnessContextEngineMaintenance,
|
|
2727
|
+
config: params.config,
|
|
2728
|
+
warn: (message) => embeddedAgentLog.warn(message),
|
|
2729
|
+
});
|
|
2730
|
+
}
|
|
2731
|
+
runAgentHarnessLlmOutputHook({
|
|
2732
|
+
event: {
|
|
2733
|
+
runId: params.runId,
|
|
2734
|
+
sessionId: params.sessionId,
|
|
2735
|
+
provider: params.provider,
|
|
2736
|
+
model: params.modelId,
|
|
2737
|
+
...hookContextWindowFields,
|
|
2738
|
+
resolvedRef:
|
|
2739
|
+
params.runtimePlan?.observability.resolvedRef ?? `${params.provider}/${params.modelId}`,
|
|
2740
|
+
...(params.runtimePlan?.observability.harnessId
|
|
2741
|
+
? { harnessId: params.runtimePlan.observability.harnessId }
|
|
2742
|
+
: {}),
|
|
2743
|
+
assistantTexts: result.assistantTexts,
|
|
2744
|
+
...(result.lastAssistant ? { lastAssistant: result.lastAssistant } : {}),
|
|
2745
|
+
...(result.attemptUsage ? { usage: result.attemptUsage } : {}),
|
|
2746
|
+
},
|
|
2747
|
+
ctx: hookContext,
|
|
2748
|
+
});
|
|
2749
|
+
runAgentHarnessAgentEndHook({
|
|
2750
|
+
event: {
|
|
2751
|
+
messages: result.messagesSnapshot,
|
|
2752
|
+
success: !finalAborted && !finalPromptError,
|
|
2753
|
+
...(finalPromptError ? { error: formatErrorMessage(finalPromptError) } : {}),
|
|
2754
|
+
durationMs: Date.now() - attemptStartedAt,
|
|
2755
|
+
},
|
|
2756
|
+
ctx: hookContext,
|
|
2757
|
+
});
|
|
2758
|
+
return {
|
|
2759
|
+
...result,
|
|
2760
|
+
timedOut,
|
|
2761
|
+
aborted: finalAborted,
|
|
2762
|
+
promptError: finalPromptError,
|
|
2763
|
+
promptErrorSource: finalPromptErrorSource,
|
|
2764
|
+
systemPromptReport,
|
|
2765
|
+
};
|
|
2766
|
+
} finally {
|
|
2767
|
+
emitLifecycleTerminal({
|
|
2768
|
+
phase: "error",
|
|
2769
|
+
error: "codex app-server run completed without lifecycle terminal event",
|
|
2770
|
+
});
|
|
2771
|
+
if (trajectoryRecorder && !trajectoryEndRecorded) {
|
|
2772
|
+
trajectoryRecorder.recordEvent("session.ended", {
|
|
2773
|
+
status:
|
|
2774
|
+
timedOut || (runAbortController.signal.aborted && !clientClosedAbort)
|
|
2775
|
+
? "interrupted"
|
|
2776
|
+
: "cleanup",
|
|
2777
|
+
threadId: thread.threadId,
|
|
2778
|
+
turnId: activeTurnId,
|
|
2779
|
+
timedOut,
|
|
2780
|
+
aborted: runAbortController.signal.aborted && !clientClosedAbort,
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2783
|
+
await runAgentCleanupStep({
|
|
2784
|
+
runId: params.runId,
|
|
2785
|
+
sessionId: params.sessionId,
|
|
2786
|
+
step: "codex-trajectory-flush",
|
|
2787
|
+
log: embeddedAgentLog,
|
|
2788
|
+
cleanup: async () => {
|
|
2789
|
+
await trajectoryRecorder?.flush();
|
|
2790
|
+
},
|
|
2791
|
+
});
|
|
2792
|
+
if (!timedOut && !runAbortController.signal.aborted) {
|
|
2793
|
+
await steeringQueue?.flushPending();
|
|
2794
|
+
}
|
|
2795
|
+
userInputBridge?.cancelPending();
|
|
2796
|
+
clearTurnAttemptIdleTimer();
|
|
2797
|
+
clearTurnCompletionIdleTimer();
|
|
2798
|
+
clearTurnAssistantCompletionIdleTimer();
|
|
2799
|
+
clearTurnTerminalIdleTimer();
|
|
2800
|
+
notificationCleanup();
|
|
2801
|
+
requestCleanup();
|
|
2802
|
+
closeCleanup?.();
|
|
2803
|
+
nativeHookRelay?.unregister();
|
|
2804
|
+
runAbortController.signal.removeEventListener("abort", abortListener);
|
|
2805
|
+
params.abortSignal?.removeEventListener("abort", abortFromUpstream);
|
|
2806
|
+
steeringQueue?.cancel();
|
|
2807
|
+
clearActiveEmbeddedRun(params.sessionId, handle, params.sessionKey);
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
async function markCodexAuthProfileBlockedFromRateLimits(params: {
|
|
2812
|
+
params: EmbeddedRunAttemptParams;
|
|
2813
|
+
authProfileId?: string;
|
|
2814
|
+
rateLimits?: JsonValue;
|
|
2815
|
+
}): Promise<void> {
|
|
2816
|
+
const authProfileId = params.authProfileId?.trim();
|
|
2817
|
+
if (!authProfileId || !params.params.authProfileStore) {
|
|
2818
|
+
return;
|
|
2819
|
+
}
|
|
2820
|
+
const blockedUntil = resolveCodexUsageLimitResetAtMs(params.rateLimits);
|
|
2821
|
+
if (!blockedUntil) {
|
|
2822
|
+
return;
|
|
2823
|
+
}
|
|
2824
|
+
try {
|
|
2825
|
+
await markAuthProfileBlockedUntil({
|
|
2826
|
+
store: params.params.authProfileStore,
|
|
2827
|
+
profileId: authProfileId,
|
|
2828
|
+
blockedUntil,
|
|
2829
|
+
source: "codex_rate_limits",
|
|
2830
|
+
agentDir: params.params.agentDir,
|
|
2831
|
+
runId: params.params.runId,
|
|
2832
|
+
modelId: params.params.modelId,
|
|
2833
|
+
});
|
|
2834
|
+
} catch (error) {
|
|
2835
|
+
embeddedAgentLog.debug("failed to mark Codex auth profile blocked from app-server limits", {
|
|
2836
|
+
authProfileId,
|
|
2837
|
+
error: formatErrorMessage(error),
|
|
2838
|
+
});
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
function buildCodexTurnStartFailureResult(params: {
|
|
2843
|
+
params: EmbeddedRunAttemptParams;
|
|
2844
|
+
message: string;
|
|
2845
|
+
messagesSnapshot: AgentMessage[];
|
|
2846
|
+
systemPromptReport: ReturnType<typeof buildCodexSystemPromptReport>;
|
|
2847
|
+
}): EmbeddedRunAttemptResult {
|
|
2848
|
+
return {
|
|
2849
|
+
aborted: false,
|
|
2850
|
+
externalAbort: false,
|
|
2851
|
+
timedOut: false,
|
|
2852
|
+
idleTimedOut: false,
|
|
2853
|
+
timedOutDuringCompaction: false,
|
|
2854
|
+
timedOutDuringToolExecution: false,
|
|
2855
|
+
promptError: params.message,
|
|
2856
|
+
promptErrorSource: "prompt",
|
|
2857
|
+
sessionIdUsed: params.params.sessionId,
|
|
2858
|
+
messagesSnapshot: params.messagesSnapshot,
|
|
2859
|
+
assistantTexts: [],
|
|
2860
|
+
toolMetas: [],
|
|
2861
|
+
lastAssistant: undefined,
|
|
2862
|
+
didSendViaMessagingTool: false,
|
|
2863
|
+
messagingToolSentTexts: [],
|
|
2864
|
+
messagingToolSentMediaUrls: [],
|
|
2865
|
+
messagingToolSentTargets: [],
|
|
2866
|
+
messagingToolSourceReplyPayloads: [],
|
|
2867
|
+
cloudCodeAssistFormatError: false,
|
|
2868
|
+
replayMetadata: {
|
|
2869
|
+
hadPotentialSideEffects: false,
|
|
2870
|
+
replaySafe: true,
|
|
2871
|
+
},
|
|
2872
|
+
itemLifecycle: {
|
|
2873
|
+
startedCount: 0,
|
|
2874
|
+
completedCount: 0,
|
|
2875
|
+
activeCount: 0,
|
|
2876
|
+
},
|
|
2877
|
+
systemPromptReport: params.systemPromptReport,
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
async function handleDynamicToolCallWithTimeout(params: {
|
|
2882
|
+
call: CodexDynamicToolCallParams;
|
|
2883
|
+
toolBridge: Pick<CodexDynamicToolBridge, "handleToolCall">;
|
|
2884
|
+
signal: AbortSignal;
|
|
2885
|
+
timeoutMs: number;
|
|
2886
|
+
onTimeout?: () => void;
|
|
2887
|
+
}): Promise<CodexDynamicToolCallResponse> {
|
|
2888
|
+
if (params.signal.aborted) {
|
|
2889
|
+
return failedDynamicToolResponse("AutoBot dynamic tool call aborted before execution.");
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
const controller = new AbortController();
|
|
2893
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
2894
|
+
let timedOut = false;
|
|
2895
|
+
let resolveAbort: ((response: CodexDynamicToolCallResponse) => void) | undefined;
|
|
2896
|
+
const abortFromRun = () => {
|
|
2897
|
+
const message = "AutoBot dynamic tool call aborted.";
|
|
2898
|
+
controller.abort(params.signal.reason ?? new Error(message));
|
|
2899
|
+
resolveAbort?.(failedDynamicToolResponse(message));
|
|
2900
|
+
};
|
|
2901
|
+
const abortPromise = new Promise<CodexDynamicToolCallResponse>((resolve) => {
|
|
2902
|
+
resolveAbort = resolve;
|
|
2903
|
+
});
|
|
2904
|
+
const timeoutPromise = new Promise<CodexDynamicToolCallResponse>((resolve) => {
|
|
2905
|
+
const timeoutMs = clampDynamicToolTimeoutMs(params.timeoutMs);
|
|
2906
|
+
timeout = setTimeout(() => {
|
|
2907
|
+
timedOut = true;
|
|
2908
|
+
const timeoutDetails = formatDynamicToolTimeoutDetails({ call: params.call, timeoutMs });
|
|
2909
|
+
controller.abort(new Error(timeoutDetails.responseMessage));
|
|
2910
|
+
params.onTimeout?.();
|
|
2911
|
+
embeddedAgentLog.warn("codex dynamic tool call timed out", {
|
|
2912
|
+
...timeoutDetails.meta,
|
|
2913
|
+
consoleMessage: timeoutDetails.consoleMessage,
|
|
2914
|
+
});
|
|
2915
|
+
resolve(failedDynamicToolResponse(timeoutDetails.responseMessage));
|
|
2916
|
+
}, timeoutMs);
|
|
2917
|
+
timeout.unref?.();
|
|
2918
|
+
});
|
|
2919
|
+
|
|
2920
|
+
try {
|
|
2921
|
+
params.signal.addEventListener("abort", abortFromRun, { once: true });
|
|
2922
|
+
if (params.signal.aborted) {
|
|
2923
|
+
abortFromRun();
|
|
2924
|
+
}
|
|
2925
|
+
return await Promise.race([
|
|
2926
|
+
params.toolBridge.handleToolCall(params.call, { signal: controller.signal }),
|
|
2927
|
+
abortPromise,
|
|
2928
|
+
timeoutPromise,
|
|
2929
|
+
]);
|
|
2930
|
+
} catch (error) {
|
|
2931
|
+
return failedDynamicToolResponse(error instanceof Error ? error.message : String(error));
|
|
2932
|
+
} finally {
|
|
2933
|
+
if (timeout) {
|
|
2934
|
+
clearTimeout(timeout);
|
|
2935
|
+
}
|
|
2936
|
+
params.signal.removeEventListener("abort", abortFromRun);
|
|
2937
|
+
resolveAbort = undefined;
|
|
2938
|
+
if (!timedOut && !controller.signal.aborted) {
|
|
2939
|
+
controller.abort(new Error("AutoBot dynamic tool call finished."));
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
function failedDynamicToolResponse(message: string): CodexDynamicToolCallResponse {
|
|
2945
|
+
const response: CodexDynamicToolCallResponse = {
|
|
2946
|
+
contentItems: [{ type: "inputText", text: message }],
|
|
2947
|
+
success: false,
|
|
2948
|
+
};
|
|
2949
|
+
Object.defineProperty(response, "diagnosticTerminalType", {
|
|
2950
|
+
configurable: true,
|
|
2951
|
+
enumerable: false,
|
|
2952
|
+
value: "error",
|
|
2953
|
+
});
|
|
2954
|
+
return response;
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
function toCodexDynamicToolProtocolResponse(
|
|
2958
|
+
response: CodexDynamicToolCallResponse,
|
|
2959
|
+
): CodexDynamicToolCallResponse {
|
|
2960
|
+
return {
|
|
2961
|
+
contentItems: response.contentItems,
|
|
2962
|
+
success: response.success,
|
|
2963
|
+
};
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
type TerminalToolExecutionDiagnostic = Extract<
|
|
2967
|
+
DiagnosticEventPayload,
|
|
2968
|
+
{ type: "tool.execution.blocked" | "tool.execution.completed" | "tool.execution.error" }
|
|
2969
|
+
>;
|
|
2970
|
+
|
|
2971
|
+
function isDynamicToolTerminalDiagnosticEvent(
|
|
2972
|
+
event: DiagnosticEventPayload,
|
|
2973
|
+
): event is TerminalToolExecutionDiagnostic {
|
|
2974
|
+
return (
|
|
2975
|
+
event.type === "tool.execution.completed" ||
|
|
2976
|
+
event.type === "tool.execution.error" ||
|
|
2977
|
+
event.type === "tool.execution.blocked"
|
|
2978
|
+
);
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
function isMatchingDynamicToolTerminalDiagnostic(params: {
|
|
2982
|
+
event: TerminalToolExecutionDiagnostic;
|
|
2983
|
+
call: CodexDynamicToolCallParams;
|
|
2984
|
+
runId?: string;
|
|
2985
|
+
sessionId?: string;
|
|
2986
|
+
sessionKey?: string;
|
|
2987
|
+
}): boolean {
|
|
2988
|
+
if (
|
|
2989
|
+
params.event.toolCallId !== params.call.callId ||
|
|
2990
|
+
params.event.toolName !== params.call.tool
|
|
2991
|
+
) {
|
|
2992
|
+
return false;
|
|
2993
|
+
}
|
|
2994
|
+
if (params.runId !== undefined) {
|
|
2995
|
+
return params.event.runId === params.runId;
|
|
2996
|
+
}
|
|
2997
|
+
if (params.sessionId !== undefined) {
|
|
2998
|
+
return params.event.sessionId === params.sessionId;
|
|
2999
|
+
}
|
|
3000
|
+
if (params.sessionKey !== undefined) {
|
|
3001
|
+
return params.event.sessionKey === params.sessionKey;
|
|
3002
|
+
}
|
|
3003
|
+
return (
|
|
3004
|
+
params.event.runId === undefined &&
|
|
3005
|
+
params.event.sessionId === undefined &&
|
|
3006
|
+
params.event.sessionKey === undefined
|
|
3007
|
+
);
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
function hasPendingDynamicToolTerminalDiagnostic(params: {
|
|
3011
|
+
call: CodexDynamicToolCallParams;
|
|
3012
|
+
runId?: string;
|
|
3013
|
+
sessionId?: string;
|
|
3014
|
+
sessionKey?: string;
|
|
3015
|
+
}): boolean {
|
|
3016
|
+
return hasPendingInternalDiagnosticEvent((event) => {
|
|
3017
|
+
if (!isDynamicToolTerminalDiagnosticEvent(event)) {
|
|
3018
|
+
return false;
|
|
3019
|
+
}
|
|
3020
|
+
return isMatchingDynamicToolTerminalDiagnostic({
|
|
3021
|
+
event,
|
|
3022
|
+
call: params.call,
|
|
3023
|
+
runId: params.runId,
|
|
3024
|
+
sessionId: params.sessionId,
|
|
3025
|
+
sessionKey: params.sessionKey,
|
|
3026
|
+
});
|
|
3027
|
+
});
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
function resolveDynamicToolCallTimeoutMs(params: {
|
|
3031
|
+
call: CodexDynamicToolCallParams;
|
|
3032
|
+
config: EmbeddedRunAttemptParams["config"];
|
|
3033
|
+
}): number {
|
|
3034
|
+
return clampDynamicToolTimeoutMs(
|
|
3035
|
+
readDynamicToolCallTimeoutMs(params.call.arguments) ??
|
|
3036
|
+
readConfiguredDynamicToolTimeoutMs(params.call.tool, params.config) ??
|
|
3037
|
+
CODEX_DYNAMIC_TOOL_TIMEOUT_MS,
|
|
3038
|
+
);
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
function readDynamicToolCallTimeoutMs(value: JsonValue | undefined): number | undefined {
|
|
3042
|
+
if (!isJsonObject(value)) {
|
|
3043
|
+
return undefined;
|
|
3044
|
+
}
|
|
3045
|
+
return readPositiveFiniteTimeoutMs(value.timeoutMs);
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
function readConfiguredDynamicToolTimeoutMs(
|
|
3049
|
+
toolName: string,
|
|
3050
|
+
config: EmbeddedRunAttemptParams["config"],
|
|
3051
|
+
): number | undefined {
|
|
3052
|
+
if (toolName === "image_generate") {
|
|
3053
|
+
const imageGenerationModel = config?.agents?.defaults?.imageGenerationModel;
|
|
3054
|
+
if (!imageGenerationModel || typeof imageGenerationModel !== "object") {
|
|
3055
|
+
return CODEX_DYNAMIC_IMAGE_GENERATION_TOOL_TIMEOUT_MS;
|
|
3056
|
+
}
|
|
3057
|
+
return (
|
|
3058
|
+
readPositiveFiniteTimeoutMs(imageGenerationModel.timeoutMs) ??
|
|
3059
|
+
CODEX_DYNAMIC_IMAGE_GENERATION_TOOL_TIMEOUT_MS
|
|
3060
|
+
);
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
if (toolName === "image") {
|
|
3064
|
+
return (
|
|
3065
|
+
readTimeoutSecondsAsMs(config?.tools?.media?.image?.timeoutSeconds) ??
|
|
3066
|
+
CODEX_DYNAMIC_IMAGE_TOOL_TIMEOUT_MS
|
|
3067
|
+
);
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
return undefined;
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
function readTimeoutSecondsAsMs(value: unknown): number | undefined {
|
|
3074
|
+
const seconds = readPositiveFiniteTimeoutMs(value);
|
|
3075
|
+
return seconds === undefined ? undefined : seconds * 1000;
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
function readPositiveFiniteTimeoutMs(value: unknown): number | undefined {
|
|
3079
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
3080
|
+
? Math.floor(value)
|
|
3081
|
+
: undefined;
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
function clampDynamicToolTimeoutMs(timeoutMs: number): number {
|
|
3085
|
+
return Math.max(1, Math.min(CODEX_DYNAMIC_TOOL_MAX_TIMEOUT_MS, Math.floor(timeoutMs)));
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
function createCodexNativeHookRelay(params: {
|
|
3089
|
+
options:
|
|
3090
|
+
| {
|
|
3091
|
+
enabled?: boolean;
|
|
3092
|
+
ttlMs?: number;
|
|
3093
|
+
gatewayTimeoutMs?: number;
|
|
3094
|
+
}
|
|
3095
|
+
| undefined;
|
|
3096
|
+
events: readonly NativeHookRelayEvent[];
|
|
3097
|
+
agentId: string | undefined;
|
|
3098
|
+
sessionId: string;
|
|
3099
|
+
sessionKey: string | undefined;
|
|
3100
|
+
config: EmbeddedRunAttemptParams["config"];
|
|
3101
|
+
runId: string;
|
|
3102
|
+
channelId?: string;
|
|
3103
|
+
attemptTimeoutMs: number;
|
|
3104
|
+
startupTimeoutMs: number;
|
|
3105
|
+
turnStartTimeoutMs: number;
|
|
3106
|
+
signal: AbortSignal;
|
|
3107
|
+
}): NativeHookRelayRegistrationHandle | undefined {
|
|
3108
|
+
if (params.options?.enabled === false) {
|
|
3109
|
+
return undefined;
|
|
3110
|
+
}
|
|
3111
|
+
return registerNativeHookRelay({
|
|
3112
|
+
provider: "codex",
|
|
3113
|
+
relayId: buildCodexNativeHookRelayId({
|
|
3114
|
+
agentId: params.agentId,
|
|
3115
|
+
sessionId: params.sessionId,
|
|
3116
|
+
sessionKey: params.sessionKey,
|
|
3117
|
+
}),
|
|
3118
|
+
...(params.agentId ? { agentId: params.agentId } : {}),
|
|
3119
|
+
sessionId: params.sessionId,
|
|
3120
|
+
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
|
|
3121
|
+
...(params.config ? { config: params.config } : {}),
|
|
3122
|
+
runId: params.runId,
|
|
3123
|
+
...(params.channelId ? { channelId: params.channelId } : {}),
|
|
3124
|
+
allowedEvents: params.events,
|
|
3125
|
+
ttlMs: resolveCodexNativeHookRelayTtlMs({
|
|
3126
|
+
explicitTtlMs: params.options?.ttlMs,
|
|
3127
|
+
attemptTimeoutMs: params.attemptTimeoutMs,
|
|
3128
|
+
startupTimeoutMs: params.startupTimeoutMs,
|
|
3129
|
+
turnStartTimeoutMs: params.turnStartTimeoutMs,
|
|
3130
|
+
}),
|
|
3131
|
+
signal: params.signal,
|
|
3132
|
+
command: {
|
|
3133
|
+
timeoutMs: params.options?.gatewayTimeoutMs,
|
|
3134
|
+
},
|
|
3135
|
+
});
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
function resolveCodexNativeHookRelayEvents(params: {
|
|
3139
|
+
configuredEvents?: readonly NativeHookRelayEvent[];
|
|
3140
|
+
appServer: Pick<CodexAppServerRuntimeOptions, "approvalPolicy">;
|
|
3141
|
+
}): readonly NativeHookRelayEvent[] {
|
|
3142
|
+
if (params.configuredEvents?.length) {
|
|
3143
|
+
return params.configuredEvents;
|
|
3144
|
+
}
|
|
3145
|
+
// Codex emits PermissionRequest before the app-server approval reviewer has
|
|
3146
|
+
// resolved the command. In native approval modes, let Codex's app-server
|
|
3147
|
+
// approval bridge own the real escalation instead of surfacing a stale
|
|
3148
|
+
// pre-guardian AutoBot plugin approval prompt.
|
|
3149
|
+
return params.appServer.approvalPolicy === "never"
|
|
3150
|
+
? CODEX_NATIVE_HOOK_RELAY_EVENTS
|
|
3151
|
+
: CODEX_NATIVE_HOOK_RELAY_EVENTS_WITH_APP_SERVER_APPROVALS;
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
function resolveCodexNativeHookRelayTtlMs(params: {
|
|
3155
|
+
explicitTtlMs: number | undefined;
|
|
3156
|
+
attemptTimeoutMs: number;
|
|
3157
|
+
startupTimeoutMs: number;
|
|
3158
|
+
turnStartTimeoutMs: number;
|
|
3159
|
+
}): number {
|
|
3160
|
+
if (params.explicitTtlMs !== undefined) {
|
|
3161
|
+
return params.explicitTtlMs;
|
|
3162
|
+
}
|
|
3163
|
+
const relayBudgetMs =
|
|
3164
|
+
params.attemptTimeoutMs +
|
|
3165
|
+
params.startupTimeoutMs +
|
|
3166
|
+
params.turnStartTimeoutMs +
|
|
3167
|
+
CODEX_NATIVE_HOOK_RELAY_TTL_GRACE_MS;
|
|
3168
|
+
return Math.max(CODEX_NATIVE_HOOK_RELAY_MIN_TTL_MS, Math.floor(relayBudgetMs));
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
function buildCodexNativeHookRelayId(params: {
|
|
3172
|
+
agentId: string | undefined;
|
|
3173
|
+
sessionId: string;
|
|
3174
|
+
sessionKey: string | undefined;
|
|
3175
|
+
}): string {
|
|
3176
|
+
const hash = createHash("sha256");
|
|
3177
|
+
hash.update("autobot:codex:native-hook-relay:v1");
|
|
3178
|
+
hash.update("\0");
|
|
3179
|
+
hash.update(params.agentId?.trim() || "");
|
|
3180
|
+
hash.update("\0");
|
|
3181
|
+
hash.update(params.sessionKey?.trim() || params.sessionId);
|
|
3182
|
+
return `codex-${hash.digest("hex").slice(0, 40)}`;
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
function fingerprintCodexLogValue(namespace: string, value: string): string {
|
|
3186
|
+
const hash = createHash("sha256");
|
|
3187
|
+
hash.update(namespace);
|
|
3188
|
+
hash.update("\0");
|
|
3189
|
+
hash.update(value);
|
|
3190
|
+
return `sha256:${hash.digest("hex").slice(0, 16)}`;
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
function buildCodexPluginThreadConfigEligibilityLogData(params: {
|
|
3194
|
+
sessionId: string;
|
|
3195
|
+
sessionKey: string;
|
|
3196
|
+
pluginThreadConfigRequired: boolean;
|
|
3197
|
+
resolvedPluginPolicy: ReturnType<typeof resolveCodexPluginsPolicy> | undefined;
|
|
3198
|
+
enabledPluginConfigKeys: string[] | undefined;
|
|
3199
|
+
pluginAppCacheKey: string;
|
|
3200
|
+
startupAuthProfileId: string | undefined;
|
|
3201
|
+
appServer: CodexAppServerRuntimeOptions;
|
|
3202
|
+
}): Record<string, unknown> {
|
|
3203
|
+
return {
|
|
3204
|
+
sessionId: params.sessionId,
|
|
3205
|
+
sessionKey: params.sessionKey,
|
|
3206
|
+
enabled: params.pluginThreadConfigRequired,
|
|
3207
|
+
policyConfigured: params.resolvedPluginPolicy?.configured === true,
|
|
3208
|
+
policyEnabled: params.resolvedPluginPolicy?.enabled === true,
|
|
3209
|
+
pluginConfigKeys: params.resolvedPluginPolicy?.pluginPolicies
|
|
3210
|
+
.map((plugin) => plugin.configKey)
|
|
3211
|
+
.toSorted(),
|
|
3212
|
+
enabledPluginConfigKeys: params.enabledPluginConfigKeys,
|
|
3213
|
+
appCacheKeyFingerprint: fingerprintCodexLogValue(
|
|
3214
|
+
"autobot:codex:plugin-app-cache-key:v1",
|
|
3215
|
+
params.pluginAppCacheKey,
|
|
3216
|
+
),
|
|
3217
|
+
authProfileId: params.startupAuthProfileId,
|
|
3218
|
+
appServerTransport: params.appServer.start.transport,
|
|
3219
|
+
appServerCommandSource: params.appServer.start.commandSource,
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
function interruptCodexTurnBestEffort(
|
|
3224
|
+
client: CodexAppServerClient,
|
|
3225
|
+
params: {
|
|
3226
|
+
threadId: string;
|
|
3227
|
+
turnId: string;
|
|
3228
|
+
timeoutMs?: number;
|
|
3229
|
+
},
|
|
3230
|
+
): void {
|
|
3231
|
+
const requestOptions =
|
|
3232
|
+
params.timeoutMs && Number.isFinite(params.timeoutMs) && params.timeoutMs > 0
|
|
3233
|
+
? { timeoutMs: params.timeoutMs }
|
|
3234
|
+
: undefined;
|
|
3235
|
+
const requestParams = { threadId: params.threadId, turnId: params.turnId };
|
|
3236
|
+
try {
|
|
3237
|
+
const interrupt = requestOptions
|
|
3238
|
+
? client.request("turn/interrupt", requestParams, requestOptions)
|
|
3239
|
+
: client.request("turn/interrupt", requestParams);
|
|
3240
|
+
void Promise.resolve(interrupt).catch((error: unknown) => {
|
|
3241
|
+
embeddedAgentLog.debug("codex app-server turn interrupt failed during abort", { error });
|
|
3242
|
+
});
|
|
3243
|
+
} catch (error) {
|
|
3244
|
+
embeddedAgentLog.debug("codex app-server turn interrupt failed during abort", { error });
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3248
|
+
function retireCodexAppServerClientAfterTimedOutTurn(
|
|
3249
|
+
client: CodexAppServerClient,
|
|
3250
|
+
params: {
|
|
3251
|
+
threadId: string;
|
|
3252
|
+
turnId: string;
|
|
3253
|
+
reason: string;
|
|
3254
|
+
},
|
|
3255
|
+
): void {
|
|
3256
|
+
const clearedSharedClient = clearSharedCodexAppServerClientIfCurrent(client);
|
|
3257
|
+
if (!clearedSharedClient) {
|
|
3258
|
+
const close = (client as { close?: () => void }).close;
|
|
3259
|
+
if (typeof close === "function") {
|
|
3260
|
+
close.call(client);
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
embeddedAgentLog.warn("codex app-server client retired after timed-out turn", {
|
|
3264
|
+
threadId: params.threadId,
|
|
3265
|
+
turnId: params.turnId,
|
|
3266
|
+
reason: params.reason,
|
|
3267
|
+
clearedSharedClient,
|
|
3268
|
+
});
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
type DynamicToolBuildParams = {
|
|
3272
|
+
params: EmbeddedRunAttemptParams;
|
|
3273
|
+
resolvedWorkspace: string;
|
|
3274
|
+
effectiveWorkspace: string;
|
|
3275
|
+
sandboxSessionKey: string;
|
|
3276
|
+
sandbox: Awaited<ReturnType<typeof resolveSandboxContext>>;
|
|
3277
|
+
nativeToolSurfaceEnabled?: boolean;
|
|
3278
|
+
runAbortController: AbortController;
|
|
3279
|
+
sessionAgentId: string;
|
|
3280
|
+
pluginConfig: CodexPluginConfig;
|
|
3281
|
+
onYieldDetected: () => void;
|
|
3282
|
+
};
|
|
3283
|
+
|
|
3284
|
+
function resolveAutoBotCodingToolsSessionKeys(
|
|
3285
|
+
params: EmbeddedRunAttemptParams,
|
|
3286
|
+
sandboxSessionKey: string,
|
|
3287
|
+
): Pick<AutoBotCodingToolsOptions, "sessionKey" | "runSessionKey"> {
|
|
3288
|
+
return {
|
|
3289
|
+
sessionKey: sandboxSessionKey,
|
|
3290
|
+
runSessionKey:
|
|
3291
|
+
params.sessionKey && params.sessionKey !== sandboxSessionKey ? params.sessionKey : undefined,
|
|
3292
|
+
};
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
function resolveCodexAppServerHookChannelId(
|
|
3296
|
+
params: EmbeddedRunAttemptParams,
|
|
3297
|
+
sandboxSessionKey: string,
|
|
3298
|
+
): string | undefined {
|
|
3299
|
+
return buildAgentHookContextChannelFields({
|
|
3300
|
+
sessionKey: sandboxSessionKey,
|
|
3301
|
+
messageChannel: params.messageChannel,
|
|
3302
|
+
messageProvider: params.messageProvider,
|
|
3303
|
+
currentChannelId: params.currentChannelId,
|
|
3304
|
+
messageTo: params.messageTo,
|
|
3305
|
+
}).channelId;
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
async function buildDynamicTools(input: DynamicToolBuildParams) {
|
|
3309
|
+
const { params } = input;
|
|
3310
|
+
if (params.disableTools || !supportsModelTools(params.model)) {
|
|
3311
|
+
return [];
|
|
3312
|
+
}
|
|
3313
|
+
const modelHasVision = params.model.input?.includes("image") ?? false;
|
|
3314
|
+
const agentDir = params.agentDir ?? resolveAgentDir(params.config ?? {}, input.sessionAgentId);
|
|
3315
|
+
const createAutoBotCodingTools =
|
|
3316
|
+
autoBotCodingToolsFactoryForTests ??
|
|
3317
|
+
(await import("autobot/plugin-sdk/agent-harness")).createAutoBotCodingTools;
|
|
3318
|
+
const sessionKeys = resolveAutoBotCodingToolsSessionKeys(params, input.sandboxSessionKey);
|
|
3319
|
+
const allTools = createAutoBotCodingTools({
|
|
3320
|
+
agentId: input.sessionAgentId,
|
|
3321
|
+
...buildEmbeddedAttemptToolRunContext(params),
|
|
3322
|
+
exec: {
|
|
3323
|
+
...params.execOverrides,
|
|
3324
|
+
elevated: params.bashElevated,
|
|
3325
|
+
},
|
|
3326
|
+
sandbox: input.sandbox,
|
|
3327
|
+
messageProvider: params.messageChannel ?? params.messageProvider,
|
|
3328
|
+
agentAccountId: params.agentAccountId,
|
|
3329
|
+
messageTo: params.messageTo,
|
|
3330
|
+
messageThreadId: params.messageThreadId,
|
|
3331
|
+
groupId: params.groupId,
|
|
3332
|
+
groupChannel: params.groupChannel,
|
|
3333
|
+
groupSpace: params.groupSpace,
|
|
3334
|
+
spawnedBy: params.spawnedBy,
|
|
3335
|
+
senderId: params.senderId,
|
|
3336
|
+
senderName: params.senderName,
|
|
3337
|
+
senderUsername: params.senderUsername,
|
|
3338
|
+
senderE164: params.senderE164,
|
|
3339
|
+
senderIsOwner: params.senderIsOwner,
|
|
3340
|
+
allowGatewaySubagentBinding:
|
|
3341
|
+
params.allowGatewaySubagentBinding || isForcedPrivateQaCodexRuntime(),
|
|
3342
|
+
...sessionKeys,
|
|
3343
|
+
sessionId: params.sessionId,
|
|
3344
|
+
runId: params.runId,
|
|
3345
|
+
agentDir,
|
|
3346
|
+
workspaceDir: input.effectiveWorkspace,
|
|
3347
|
+
spawnWorkspaceDir: resolveAttemptSpawnWorkspaceDir({
|
|
3348
|
+
sandbox: input.sandbox,
|
|
3349
|
+
resolvedWorkspace: input.resolvedWorkspace,
|
|
3350
|
+
}),
|
|
3351
|
+
config: params.config,
|
|
3352
|
+
authProfileStore: params.toolAuthProfileStore ?? params.authProfileStore,
|
|
3353
|
+
abortSignal: input.runAbortController.signal,
|
|
3354
|
+
emitBeforeToolCallDiagnostics: false,
|
|
3355
|
+
modelProvider: params.model.provider,
|
|
3356
|
+
modelId: params.modelId,
|
|
3357
|
+
modelCompat:
|
|
3358
|
+
params.model.compat && typeof params.model.compat === "object"
|
|
3359
|
+
? (params.model.compat as AutoBotCodingToolsOptions["modelCompat"])
|
|
3360
|
+
: undefined,
|
|
3361
|
+
modelApi: params.model.api,
|
|
3362
|
+
modelContextWindowTokens: params.model.contextWindow,
|
|
3363
|
+
modelAuthMode: resolveModelAuthMode(params.model.provider, params.config, undefined, {
|
|
3364
|
+
workspaceDir: input.effectiveWorkspace,
|
|
3365
|
+
}),
|
|
3366
|
+
suppressManagedWebSearch: false,
|
|
3367
|
+
currentChannelId: params.currentChannelId,
|
|
3368
|
+
hookChannelId: resolveCodexAppServerHookChannelId(params, input.sandboxSessionKey),
|
|
3369
|
+
currentThreadTs: params.currentThreadTs,
|
|
3370
|
+
currentMessageId: params.currentMessageId,
|
|
3371
|
+
replyToMode: params.replyToMode,
|
|
3372
|
+
hasRepliedRef: params.hasRepliedRef,
|
|
3373
|
+
modelHasVision,
|
|
3374
|
+
requireExplicitMessageTarget:
|
|
3375
|
+
params.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey),
|
|
3376
|
+
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
|
|
3377
|
+
disableMessageTool: params.disableMessageTool,
|
|
3378
|
+
forceMessageTool: shouldForceMessageTool(params),
|
|
3379
|
+
enableHeartbeatTool: params.trigger === "heartbeat",
|
|
3380
|
+
forceHeartbeatTool: params.trigger === "heartbeat",
|
|
3381
|
+
onYield: (message) => {
|
|
3382
|
+
input.onYieldDetected();
|
|
3383
|
+
emitCodexAppServerEvent(params, {
|
|
3384
|
+
stream: "codex_app_server.tool",
|
|
3385
|
+
data: { name: "sessions_yield", message },
|
|
3386
|
+
});
|
|
3387
|
+
input.runAbortController.abort("sessions_yield");
|
|
3388
|
+
},
|
|
3389
|
+
});
|
|
3390
|
+
const codexFilteredTools = addSandboxShellDynamicToolsIfAvailable(
|
|
3391
|
+
filterCodexDynamicTools(allTools, input.pluginConfig),
|
|
3392
|
+
allTools,
|
|
3393
|
+
input,
|
|
3394
|
+
);
|
|
3395
|
+
const visionFilteredTools = filterToolsForVisionInputs(codexFilteredTools, {
|
|
3396
|
+
modelHasVision,
|
|
3397
|
+
hasInboundImages: (params.images?.length ?? 0) > 0,
|
|
3398
|
+
});
|
|
3399
|
+
const toolsAllow = includeForcedMessageToolAllow(params.toolsAllow, params);
|
|
3400
|
+
const filteredTools = filterCodexDynamicToolsForAllowlist(visionFilteredTools, toolsAllow);
|
|
3401
|
+
return normalizeAgentRuntimeTools({
|
|
3402
|
+
runtimePlan: params.runtimePlan,
|
|
3403
|
+
tools: filteredTools,
|
|
3404
|
+
provider: params.provider,
|
|
3405
|
+
config: params.config,
|
|
3406
|
+
workspaceDir: input.effectiveWorkspace,
|
|
3407
|
+
env: process.env,
|
|
3408
|
+
modelId: params.modelId,
|
|
3409
|
+
modelApi: params.model.api,
|
|
3410
|
+
model: params.model,
|
|
3411
|
+
});
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
function includeForcedMessageToolAllow(
|
|
3415
|
+
toolsAllow: string[] | undefined,
|
|
3416
|
+
params: EmbeddedRunAttemptParams,
|
|
3417
|
+
): string[] | undefined {
|
|
3418
|
+
if (
|
|
3419
|
+
!shouldForceMessageTool(params) ||
|
|
3420
|
+
toolsAllow === undefined ||
|
|
3421
|
+
hasWildcardCodexToolsAllow(toolsAllow)
|
|
3422
|
+
) {
|
|
3423
|
+
return toolsAllow;
|
|
3424
|
+
}
|
|
3425
|
+
if (toolsAllow.length === 0) {
|
|
3426
|
+
return ["message"];
|
|
3427
|
+
}
|
|
3428
|
+
const normalized = new Set(toolsAllow.map((name) => normalizeCodexDynamicToolName(name)));
|
|
3429
|
+
return normalized.has("message") ? toolsAllow : [...toolsAllow, "message"];
|
|
3430
|
+
}
|
|
3431
|
+
|
|
3432
|
+
function shouldEnableCodexAppServerNativeToolSurface(
|
|
3433
|
+
params: EmbeddedRunAttemptParams,
|
|
3434
|
+
sandbox?: AutoBotSandboxContext,
|
|
3435
|
+
): boolean {
|
|
3436
|
+
const toolsAllow = includeForcedMessageToolAllow(params.toolsAllow, params);
|
|
3437
|
+
if (toolsAllow === undefined) {
|
|
3438
|
+
return canCodexAppServerNativeToolSurfaceHonorSandbox(sandbox);
|
|
3439
|
+
}
|
|
3440
|
+
// Codex native code mode exposes its shell/file surface as one app-server
|
|
3441
|
+
// capability, so narrow AutoBot allowlists must fail closed rather than
|
|
3442
|
+
// widening `message` or `web_search` into shell access.
|
|
3443
|
+
return (
|
|
3444
|
+
hasWildcardCodexToolsAllow(toolsAllow) &&
|
|
3445
|
+
canCodexAppServerNativeToolSurfaceHonorSandbox(sandbox)
|
|
3446
|
+
);
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
function canCodexAppServerNativeToolSurfaceHonorSandbox(
|
|
3450
|
+
sandbox: AutoBotSandboxContext | undefined,
|
|
3451
|
+
): boolean {
|
|
3452
|
+
if (!sandbox?.enabled || sandbox.backendId !== "docker") {
|
|
3453
|
+
return true;
|
|
3454
|
+
}
|
|
3455
|
+
return (
|
|
3456
|
+
!hasSandboxBindContainerPathAliases(sandbox.docker.binds) &&
|
|
3457
|
+
!hasSandboxBindReadonlyHostShadows(sandbox.docker.binds)
|
|
3458
|
+
);
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
function disableCodexPluginThreadConfig(pluginConfig?: unknown): CodexPluginConfig {
|
|
3462
|
+
const config = readCodexPluginConfig(pluginConfig);
|
|
3463
|
+
return {
|
|
3464
|
+
...config,
|
|
3465
|
+
codexPlugins: {
|
|
3466
|
+
...config.codexPlugins,
|
|
3467
|
+
enabled: false,
|
|
3468
|
+
},
|
|
3469
|
+
};
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
function addSandboxShellDynamicToolsIfAvailable(
|
|
3473
|
+
filteredTools: AutoBotDynamicTool[],
|
|
3474
|
+
allTools: AutoBotDynamicTool[],
|
|
3475
|
+
input: DynamicToolBuildParams,
|
|
3476
|
+
): AutoBotDynamicTool[] {
|
|
3477
|
+
if (
|
|
3478
|
+
!shouldExposeSandboxExecDynamicTool(input) ||
|
|
3479
|
+
isSandboxShellDynamicToolExcluded(input.pluginConfig)
|
|
3480
|
+
) {
|
|
3481
|
+
return filteredTools;
|
|
3482
|
+
}
|
|
3483
|
+
const execTool = allTools.find((tool) => normalizeCodexDynamicToolName(tool.name) === "exec");
|
|
3484
|
+
const processTool = allTools.find(
|
|
3485
|
+
(tool) => normalizeCodexDynamicToolName(tool.name) === "process",
|
|
3486
|
+
);
|
|
3487
|
+
if (!execTool || !processTool) {
|
|
3488
|
+
return filteredTools;
|
|
3489
|
+
}
|
|
3490
|
+
const sandboxExecTool: AutoBotDynamicTool = {
|
|
3491
|
+
...execTool,
|
|
3492
|
+
name: "sandbox_exec",
|
|
3493
|
+
description:
|
|
3494
|
+
"Run a shell command through AutoBot's configured sandbox backend for this session. Use only when the command must execute in the AutoBot sandbox backend, such as an SSH-backed sandbox or Docker container-path bind layout that Codex's native shell cannot represent. Use Codex's native shell for normal local workspace commands.",
|
|
3495
|
+
execute: async (toolCallId, args, signal, onUpdate) => {
|
|
3496
|
+
const result = await execTool.execute(toolCallId, args, signal, onUpdate);
|
|
3497
|
+
return {
|
|
3498
|
+
...result,
|
|
3499
|
+
content: result.content.map((item) =>
|
|
3500
|
+
item.type === "text"
|
|
3501
|
+
? Object.assign({}, item, {
|
|
3502
|
+
text: item.text.replace(
|
|
3503
|
+
"Use process (list/poll/log/write/send-keys/submit/paste/kill/clear/remove) for follow-up.",
|
|
3504
|
+
"Use sandbox_process (list/poll/log/write/send-keys/submit/paste/kill/clear/remove) for follow-up.",
|
|
3505
|
+
),
|
|
3506
|
+
})
|
|
3507
|
+
: item,
|
|
3508
|
+
),
|
|
3509
|
+
};
|
|
3510
|
+
},
|
|
3511
|
+
};
|
|
3512
|
+
const sandboxProcessTool: AutoBotDynamicTool = {
|
|
3513
|
+
...processTool,
|
|
3514
|
+
name: "sandbox_process",
|
|
3515
|
+
description:
|
|
3516
|
+
"Manage sandbox_exec sessions that were started through AutoBot's configured sandbox backend for this session: list, poll, log, write, send-keys, submit, paste, kill, clear, or remove. Use only for sandbox_exec follow-up; use Codex's native shell session handling for normal native shell commands.",
|
|
3517
|
+
};
|
|
3518
|
+
return [...filteredTools, sandboxExecTool, sandboxProcessTool];
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3521
|
+
function shouldExposeSandboxExecDynamicTool(input: DynamicToolBuildParams): boolean {
|
|
3522
|
+
const backendId = input.sandbox?.enabled ? input.sandbox.backendId.trim().toLowerCase() : "";
|
|
3523
|
+
return Boolean(backendId && (backendId !== "docker" || input.nativeToolSurfaceEnabled === false));
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
function isSandboxShellDynamicToolExcluded(config: CodexPluginConfig): boolean {
|
|
3527
|
+
return (config.codexDynamicToolsExclude ?? []).some((name) => {
|
|
3528
|
+
const normalized = normalizeCodexDynamicToolName(name);
|
|
3529
|
+
return (
|
|
3530
|
+
normalized === "exec" ||
|
|
3531
|
+
normalized === "sandbox_exec" ||
|
|
3532
|
+
normalized === "process" ||
|
|
3533
|
+
normalized === "sandbox_process"
|
|
3534
|
+
);
|
|
3535
|
+
});
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
function filterCodexDynamicToolsForAllowlist<T extends { name: string }>(
|
|
3539
|
+
tools: T[],
|
|
3540
|
+
toolsAllow?: string[],
|
|
3541
|
+
): T[] {
|
|
3542
|
+
if (!toolsAllow) {
|
|
3543
|
+
return tools;
|
|
3544
|
+
}
|
|
3545
|
+
if (toolsAllow.length === 0) {
|
|
3546
|
+
return [];
|
|
3547
|
+
}
|
|
3548
|
+
if (hasWildcardCodexToolsAllow(toolsAllow)) {
|
|
3549
|
+
return tools;
|
|
3550
|
+
}
|
|
3551
|
+
const allowSet = new Set(
|
|
3552
|
+
toolsAllow.map((name) => normalizeCodexDynamicToolName(name)).filter(Boolean),
|
|
3553
|
+
);
|
|
3554
|
+
return tools.filter((tool) => {
|
|
3555
|
+
const normalized = normalizeCodexDynamicToolName(tool.name);
|
|
3556
|
+
return (
|
|
3557
|
+
allowSet.has(normalized) ||
|
|
3558
|
+
(normalized === "sandbox_exec" && allowSet.has("exec")) ||
|
|
3559
|
+
(normalized === "sandbox_process" && (allowSet.has("exec") || allowSet.has("process")))
|
|
3560
|
+
);
|
|
3561
|
+
});
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
function hasWildcardCodexToolsAllow(toolsAllow: string[]): boolean {
|
|
3565
|
+
return toolsAllow.some((name) => normalizeCodexDynamicToolName(name) === "*");
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
function shouldForceMessageTool(params: EmbeddedRunAttemptParams): boolean {
|
|
3569
|
+
return (
|
|
3570
|
+
params.disableMessageTool !== true && params.sourceReplyDeliveryMode === "message_tool_only"
|
|
3571
|
+
);
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
function shouldProjectMirroredHistoryForCodexStart(params: {
|
|
3575
|
+
startupBinding: CodexAppServerThreadBinding | undefined;
|
|
3576
|
+
dynamicToolsFingerprint: string;
|
|
3577
|
+
historyMessages: AgentMessage[];
|
|
3578
|
+
}): boolean {
|
|
3579
|
+
if (!params.historyMessages.some((message) => message.role === "user")) {
|
|
3580
|
+
return false;
|
|
3581
|
+
}
|
|
3582
|
+
if (!params.startupBinding?.threadId) {
|
|
3583
|
+
return true;
|
|
3584
|
+
}
|
|
3585
|
+
return !areCodexDynamicToolFingerprintsCompatible({
|
|
3586
|
+
previous: params.startupBinding.dynamicToolsFingerprint,
|
|
3587
|
+
next: params.dynamicToolsFingerprint,
|
|
3588
|
+
});
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
function readContextEngineThreadBootstrapProjection(
|
|
3592
|
+
projection: ContextEngineProjection | undefined,
|
|
3593
|
+
): CodexContextEngineThreadBootstrapProjection | undefined {
|
|
3594
|
+
if (projection?.mode !== "thread_bootstrap") {
|
|
3595
|
+
return undefined;
|
|
3596
|
+
}
|
|
3597
|
+
const epoch = projection.epoch?.trim();
|
|
3598
|
+
if (!epoch) {
|
|
3599
|
+
embeddedAgentLog.warn(
|
|
3600
|
+
"context engine requested Codex thread-bootstrap projection without an epoch; using per-turn projection",
|
|
3601
|
+
);
|
|
3602
|
+
return undefined;
|
|
3603
|
+
}
|
|
3604
|
+
const fingerprint = projection.fingerprint?.trim();
|
|
3605
|
+
return {
|
|
3606
|
+
mode: "thread_bootstrap",
|
|
3607
|
+
epoch,
|
|
3608
|
+
...(fingerprint ? { fingerprint } : {}),
|
|
3609
|
+
};
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
function resolveContextEngineBootstrapProjectionDecision(params: {
|
|
3613
|
+
startupBinding: CodexAppServerThreadBinding | undefined;
|
|
3614
|
+
expectedBinding: ReturnType<typeof buildContextEngineBinding>;
|
|
3615
|
+
projection: CodexContextEngineThreadBootstrapProjection;
|
|
3616
|
+
dynamicToolsFingerprint: string;
|
|
3617
|
+
}): { project: boolean; reason: string } {
|
|
3618
|
+
const bindingProjection = params.startupBinding?.contextEngine?.projection;
|
|
3619
|
+
if (!params.startupBinding?.threadId || !bindingProjection) {
|
|
3620
|
+
return {
|
|
3621
|
+
project: true,
|
|
3622
|
+
reason: !params.startupBinding?.threadId
|
|
3623
|
+
? "missing-thread-binding"
|
|
3624
|
+
: "missing-projection-binding",
|
|
3625
|
+
};
|
|
3626
|
+
}
|
|
3627
|
+
if (
|
|
3628
|
+
!params.expectedBinding ||
|
|
3629
|
+
!isContextEngineBindingCompatible(params.startupBinding.contextEngine, params.expectedBinding)
|
|
3630
|
+
) {
|
|
3631
|
+
return { project: true, reason: "context-engine-binding-mismatch" };
|
|
3632
|
+
}
|
|
3633
|
+
if (
|
|
3634
|
+
!areCodexDynamicToolFingerprintsCompatible({
|
|
3635
|
+
previous: params.startupBinding.dynamicToolsFingerprint,
|
|
3636
|
+
next: params.dynamicToolsFingerprint,
|
|
3637
|
+
})
|
|
3638
|
+
) {
|
|
3639
|
+
return { project: true, reason: "dynamic-tools-mismatch" };
|
|
3640
|
+
}
|
|
3641
|
+
const projectionChanged =
|
|
3642
|
+
bindingProjection.mode !== "thread_bootstrap" ||
|
|
3643
|
+
bindingProjection.epoch !== params.projection.epoch ||
|
|
3644
|
+
bindingProjection.fingerprint !== params.projection.fingerprint;
|
|
3645
|
+
return projectionChanged
|
|
3646
|
+
? { project: true, reason: "projection-mismatch" }
|
|
3647
|
+
: { project: false, reason: "matching-thread-bootstrap-binding" };
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
async function withCodexStartupTimeout<T>(params: {
|
|
3651
|
+
timeoutMs: number;
|
|
3652
|
+
signal: AbortSignal;
|
|
3653
|
+
operation: () => Promise<T>;
|
|
3654
|
+
}): Promise<T> {
|
|
3655
|
+
if (params.signal.aborted) {
|
|
3656
|
+
throw new Error("codex app-server startup aborted");
|
|
3657
|
+
}
|
|
3658
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
3659
|
+
let abortCleanup: (() => void) | undefined;
|
|
3660
|
+
try {
|
|
3661
|
+
return await Promise.race([
|
|
3662
|
+
params.operation(),
|
|
3663
|
+
new Promise<never>((_, reject) => {
|
|
3664
|
+
const rejectOnce = (error: Error) => {
|
|
3665
|
+
if (timeout) {
|
|
3666
|
+
clearTimeout(timeout);
|
|
3667
|
+
timeout = undefined;
|
|
3668
|
+
}
|
|
3669
|
+
reject(error);
|
|
3670
|
+
};
|
|
3671
|
+
timeout = setTimeout(() => {
|
|
3672
|
+
rejectOnce(new Error("codex app-server startup timed out"));
|
|
3673
|
+
}, params.timeoutMs);
|
|
3674
|
+
const abortListener = () => rejectOnce(new Error("codex app-server startup aborted"));
|
|
3675
|
+
params.signal.addEventListener("abort", abortListener, { once: true });
|
|
3676
|
+
abortCleanup = () => params.signal.removeEventListener("abort", abortListener);
|
|
3677
|
+
}),
|
|
3678
|
+
]);
|
|
3679
|
+
} finally {
|
|
3680
|
+
if (timeout) {
|
|
3681
|
+
clearTimeout(timeout);
|
|
3682
|
+
}
|
|
3683
|
+
abortCleanup?.();
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
function resolveCodexStartupTimeoutMs(params: {
|
|
3688
|
+
timeoutMs: number;
|
|
3689
|
+
timeoutFloorMs?: number;
|
|
3690
|
+
}): number {
|
|
3691
|
+
return Math.max(
|
|
3692
|
+
params.timeoutFloorMs ?? CODEX_APP_SERVER_STARTUP_TIMEOUT_FLOOR_MS,
|
|
3693
|
+
params.timeoutMs,
|
|
3694
|
+
);
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
function resolveCodexTurnCompletionIdleTimeoutMs(value: number | undefined): number {
|
|
3698
|
+
if (value === undefined) {
|
|
3699
|
+
return CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS;
|
|
3700
|
+
}
|
|
3701
|
+
if (!Number.isFinite(value)) {
|
|
3702
|
+
return CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS;
|
|
3703
|
+
}
|
|
3704
|
+
return Math.max(1, Math.floor(value));
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3707
|
+
function resolveCodexTurnAssistantCompletionIdleTimeoutMs(value: number | undefined): number {
|
|
3708
|
+
if (value === undefined) {
|
|
3709
|
+
return CODEX_TURN_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS;
|
|
3710
|
+
}
|
|
3711
|
+
if (!Number.isFinite(value)) {
|
|
3712
|
+
return CODEX_TURN_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS;
|
|
3713
|
+
}
|
|
3714
|
+
return Math.max(1, Math.floor(value));
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3717
|
+
function resolveCodexTurnTerminalIdleTimeoutMs(value: number | undefined): number {
|
|
3718
|
+
if (value === undefined) {
|
|
3719
|
+
return CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS;
|
|
3720
|
+
}
|
|
3721
|
+
if (!Number.isFinite(value)) {
|
|
3722
|
+
return CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS;
|
|
3723
|
+
}
|
|
3724
|
+
return Math.max(1, Math.floor(value));
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3727
|
+
function readDynamicToolCallParams(
|
|
3728
|
+
value: JsonValue | undefined,
|
|
3729
|
+
): CodexDynamicToolCallParams | undefined {
|
|
3730
|
+
return readCodexDynamicToolCallParams(value);
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
type CodexUsageLimitErrorSource = {
|
|
3734
|
+
message?: string | null;
|
|
3735
|
+
codexErrorInfo?: JsonValue | null;
|
|
3736
|
+
rateLimits?: JsonValue;
|
|
3737
|
+
rateLimitsTrustedForProfile?: boolean;
|
|
3738
|
+
};
|
|
3739
|
+
|
|
3740
|
+
type CodexUsageLimitErrorResult = {
|
|
3741
|
+
message: string;
|
|
3742
|
+
rateLimitsForProfile?: JsonValue;
|
|
3743
|
+
};
|
|
3744
|
+
|
|
3745
|
+
async function formatCodexTurnStartUsageLimitError(params: {
|
|
3746
|
+
client: CodexAppServerClient;
|
|
3747
|
+
error: unknown;
|
|
3748
|
+
pendingNotifications: CodexServerNotification[];
|
|
3749
|
+
timeoutMs?: number;
|
|
3750
|
+
signal?: AbortSignal;
|
|
3751
|
+
}): Promise<CodexUsageLimitErrorResult | undefined> {
|
|
3752
|
+
return refreshCodexUsageLimitError({
|
|
3753
|
+
client: params.client,
|
|
3754
|
+
source: readCodexTurnStartUsageLimitErrorSource(params.error, params.pendingNotifications),
|
|
3755
|
+
timeoutMs: params.timeoutMs,
|
|
3756
|
+
signal: params.signal,
|
|
3757
|
+
});
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
async function refreshCodexUsageLimitErrorMessage(params: {
|
|
3761
|
+
client: CodexAppServerClient;
|
|
3762
|
+
source: CodexUsageLimitErrorSource;
|
|
3763
|
+
timeoutMs?: number;
|
|
3764
|
+
signal?: AbortSignal;
|
|
3765
|
+
}): Promise<string | undefined> {
|
|
3766
|
+
return (
|
|
3767
|
+
await refreshCodexUsageLimitError({
|
|
3768
|
+
client: params.client,
|
|
3769
|
+
source: params.source,
|
|
3770
|
+
timeoutMs: params.timeoutMs,
|
|
3771
|
+
signal: params.signal,
|
|
3772
|
+
})
|
|
3773
|
+
)?.message;
|
|
3774
|
+
}
|
|
3775
|
+
|
|
3776
|
+
async function refreshCodexUsageLimitError(params: {
|
|
3777
|
+
client: CodexAppServerClient;
|
|
3778
|
+
source: CodexUsageLimitErrorSource;
|
|
3779
|
+
timeoutMs?: number;
|
|
3780
|
+
signal?: AbortSignal;
|
|
3781
|
+
}): Promise<CodexUsageLimitErrorResult | undefined> {
|
|
3782
|
+
const initialMessage = formatCodexUsageLimitErrorMessage(params.source);
|
|
3783
|
+
if (!shouldRefreshCodexRateLimitsForUsageLimitMessage(initialMessage)) {
|
|
3784
|
+
return initialMessage
|
|
3785
|
+
? {
|
|
3786
|
+
message: initialMessage,
|
|
3787
|
+
...(params.source.rateLimitsTrustedForProfile
|
|
3788
|
+
? { rateLimitsForProfile: params.source.rateLimits }
|
|
3789
|
+
: {}),
|
|
3790
|
+
}
|
|
3791
|
+
: undefined;
|
|
3792
|
+
}
|
|
3793
|
+
const rateLimits = await readCodexRateLimitsFromAppServerForUsageLimitError({
|
|
3794
|
+
client: params.client,
|
|
3795
|
+
timeoutMs: params.timeoutMs,
|
|
3796
|
+
signal: params.signal,
|
|
3797
|
+
});
|
|
3798
|
+
if (!rateLimits) {
|
|
3799
|
+
return initialMessage
|
|
3800
|
+
? {
|
|
3801
|
+
message: initialMessage,
|
|
3802
|
+
...(params.source.rateLimitsTrustedForProfile
|
|
3803
|
+
? { rateLimitsForProfile: params.source.rateLimits }
|
|
3804
|
+
: {}),
|
|
3805
|
+
}
|
|
3806
|
+
: undefined;
|
|
3807
|
+
}
|
|
3808
|
+
const refreshedMessage = formatCodexUsageLimitErrorMessage({
|
|
3809
|
+
message: params.source.message,
|
|
3810
|
+
codexErrorInfo: params.source.codexErrorInfo,
|
|
3811
|
+
rateLimits,
|
|
3812
|
+
});
|
|
3813
|
+
const message = refreshedMessage ?? initialMessage;
|
|
3814
|
+
return message ? { message, rateLimitsForProfile: rateLimits } : undefined;
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3817
|
+
async function readCodexRateLimitsFromAppServerForUsageLimitError(params: {
|
|
3818
|
+
client: CodexAppServerClient;
|
|
3819
|
+
timeoutMs?: number;
|
|
3820
|
+
signal?: AbortSignal;
|
|
3821
|
+
}): Promise<JsonValue | undefined> {
|
|
3822
|
+
if (params.signal?.aborted) {
|
|
3823
|
+
return undefined;
|
|
3824
|
+
}
|
|
3825
|
+
try {
|
|
3826
|
+
const rateLimits = await params.client.request(CODEX_CONTROL_METHODS.rateLimits, undefined, {
|
|
3827
|
+
timeoutMs: resolveCodexUsageLimitRateLimitRefreshTimeoutMs(params.timeoutMs),
|
|
3828
|
+
signal: params.signal,
|
|
3829
|
+
});
|
|
3830
|
+
rememberCodexRateLimits(rateLimits);
|
|
3831
|
+
return rateLimits;
|
|
3832
|
+
} catch (error) {
|
|
3833
|
+
embeddedAgentLog.debug("codex app-server rate-limit refresh failed after usage-limit error", {
|
|
3834
|
+
error: formatErrorMessage(error),
|
|
3835
|
+
});
|
|
3836
|
+
return undefined;
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3840
|
+
function resolveCodexUsageLimitRateLimitRefreshTimeoutMs(timeoutMs: number | undefined): number {
|
|
3841
|
+
if (timeoutMs === undefined || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
3842
|
+
return CODEX_USAGE_LIMIT_RATE_LIMIT_REFRESH_TIMEOUT_MS;
|
|
3843
|
+
}
|
|
3844
|
+
return Math.max(100, Math.min(timeoutMs, CODEX_USAGE_LIMIT_RATE_LIMIT_REFRESH_TIMEOUT_MS));
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
function readCodexTurnStartUsageLimitErrorSource(
|
|
3848
|
+
error: unknown,
|
|
3849
|
+
pendingNotifications: CodexServerNotification[],
|
|
3850
|
+
): CodexUsageLimitErrorSource {
|
|
3851
|
+
const notificationError = readLatestCodexErrorNotification(pendingNotifications);
|
|
3852
|
+
const notificationRateLimits = readLatestRateLimitNotificationPayload(pendingNotifications);
|
|
3853
|
+
const errorPayload = readCodexErrorPayload(error);
|
|
3854
|
+
const rateLimits =
|
|
3855
|
+
notificationRateLimits ?? errorPayload.rateLimits ?? readRecentCodexRateLimits();
|
|
3856
|
+
return {
|
|
3857
|
+
message: notificationError?.message ?? errorPayload.message ?? formatErrorMessage(error),
|
|
3858
|
+
codexErrorInfo: notificationError?.codexErrorInfo ?? errorPayload.codexErrorInfo,
|
|
3859
|
+
rateLimits,
|
|
3860
|
+
rateLimitsTrustedForProfile:
|
|
3861
|
+
notificationRateLimits !== undefined || errorPayload.rateLimits !== undefined,
|
|
3862
|
+
};
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3865
|
+
function readLatestRateLimitNotificationPayload(
|
|
3866
|
+
notifications: CodexServerNotification[],
|
|
3867
|
+
): JsonValue | undefined {
|
|
3868
|
+
for (let index = notifications.length - 1; index >= 0; index -= 1) {
|
|
3869
|
+
const notification = notifications[index];
|
|
3870
|
+
if (notification?.method === "account/rateLimits/updated") {
|
|
3871
|
+
rememberCodexRateLimits(notification.params);
|
|
3872
|
+
return notification.params;
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
return undefined;
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
function readLatestCodexErrorNotification(
|
|
3879
|
+
notifications: CodexServerNotification[],
|
|
3880
|
+
): { message?: string; codexErrorInfo?: JsonValue | null } | undefined {
|
|
3881
|
+
for (let index = notifications.length - 1; index >= 0; index -= 1) {
|
|
3882
|
+
const notification = notifications[index];
|
|
3883
|
+
if (notification?.method !== "error" || !isJsonObject(notification.params)) {
|
|
3884
|
+
continue;
|
|
3885
|
+
}
|
|
3886
|
+
const error = notification.params.error;
|
|
3887
|
+
if (!isJsonObject(error)) {
|
|
3888
|
+
continue;
|
|
3889
|
+
}
|
|
3890
|
+
return {
|
|
3891
|
+
message: readString(error, "message"),
|
|
3892
|
+
codexErrorInfo: error.codexErrorInfo,
|
|
3893
|
+
};
|
|
3894
|
+
}
|
|
3895
|
+
return undefined;
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3898
|
+
function readCodexErrorPayload(error: unknown): {
|
|
3899
|
+
message?: string;
|
|
3900
|
+
codexErrorInfo?: JsonValue | null;
|
|
3901
|
+
rateLimits?: JsonValue;
|
|
3902
|
+
} {
|
|
3903
|
+
const message = error instanceof Error ? error.message : undefined;
|
|
3904
|
+
if (!error || typeof error !== "object" || !("data" in error)) {
|
|
3905
|
+
return { message };
|
|
3906
|
+
}
|
|
3907
|
+
const data = (error as { data?: unknown }).data as JsonValue | undefined;
|
|
3908
|
+
if (!isJsonObject(data)) {
|
|
3909
|
+
return { message };
|
|
3910
|
+
}
|
|
3911
|
+
const nestedError = isJsonObject(data.error) ? data.error : data;
|
|
3912
|
+
const rateLimits = nestedError.rateLimits ?? data.rateLimits;
|
|
3913
|
+
if (rateLimits !== undefined) {
|
|
3914
|
+
rememberCodexRateLimits(rateLimits);
|
|
3915
|
+
}
|
|
3916
|
+
return {
|
|
3917
|
+
message: readString(nestedError, "message") ?? message,
|
|
3918
|
+
codexErrorInfo: nestedError.codexErrorInfo,
|
|
3919
|
+
rateLimits,
|
|
3920
|
+
};
|
|
3921
|
+
}
|
|
3922
|
+
|
|
3923
|
+
function isInvalidCodexImagePayloadError(message: unknown): boolean {
|
|
3924
|
+
if (typeof message !== "string" || !message.trim()) {
|
|
3925
|
+
return false;
|
|
3926
|
+
}
|
|
3927
|
+
const normalizedMessage = message.replace(/[_-]+/gu, " ");
|
|
3928
|
+
return (
|
|
3929
|
+
/\b(?:invalid|malformed)\b[\s\S]{0,120}\b(?:image|image url|base64)\b/iu.test(
|
|
3930
|
+
normalizedMessage,
|
|
3931
|
+
) ||
|
|
3932
|
+
/\b(?:image|image url|base64)\b[\s\S]{0,120}\b(?:invalid|malformed)\b/iu.test(normalizedMessage)
|
|
3933
|
+
);
|
|
3934
|
+
}
|
|
3935
|
+
|
|
3936
|
+
async function clearCodexBindingAfterInvalidImagePayload(
|
|
3937
|
+
sessionFile: string,
|
|
3938
|
+
fields: { phase: string; threadId?: string; turnId?: string; error?: string },
|
|
3939
|
+
): Promise<void> {
|
|
3940
|
+
const currentBinding = await readCodexAppServerBinding(sessionFile);
|
|
3941
|
+
if (fields.threadId && currentBinding && currentBinding.threadId !== fields.threadId) {
|
|
3942
|
+
embeddedAgentLog.warn(
|
|
3943
|
+
"codex app-server image payload error detected for unbound thread; preserving thread binding",
|
|
3944
|
+
{ ...fields, boundThreadId: currentBinding.threadId },
|
|
3945
|
+
);
|
|
3946
|
+
return;
|
|
3947
|
+
}
|
|
3948
|
+
embeddedAgentLog.warn(
|
|
3949
|
+
"codex app-server image payload error detected; clearing thread binding",
|
|
3950
|
+
fields,
|
|
3951
|
+
);
|
|
3952
|
+
await clearCodexAppServerBinding(sessionFile);
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3955
|
+
function describeNotificationActivity(
|
|
3956
|
+
notification: CodexServerNotification,
|
|
3957
|
+
): Record<string, unknown> | undefined {
|
|
3958
|
+
if (!isJsonObject(notification.params)) {
|
|
3959
|
+
return { lastNotificationMethod: notification.method };
|
|
3960
|
+
}
|
|
3961
|
+
if (notification.method !== "rawResponseItem/completed") {
|
|
3962
|
+
return { lastNotificationMethod: notification.method };
|
|
3963
|
+
}
|
|
3964
|
+
const item = isJsonObject(notification.params.item) ? notification.params.item : undefined;
|
|
3965
|
+
if (!item) {
|
|
3966
|
+
return { lastNotificationMethod: notification.method };
|
|
3967
|
+
}
|
|
3968
|
+
return {
|
|
3969
|
+
lastNotificationMethod: notification.method,
|
|
3970
|
+
lastNotificationItemId: readString(item, "id"),
|
|
3971
|
+
lastNotificationItemType: readString(item, "type"),
|
|
3972
|
+
lastNotificationItemRole: readString(item, "role"),
|
|
3973
|
+
lastAssistantTextPreview: readRawAssistantTextPreview(item),
|
|
3974
|
+
};
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
function updateActiveTurnItemIds(
|
|
3978
|
+
notification: CodexServerNotification,
|
|
3979
|
+
activeItemIds: Set<string>,
|
|
3980
|
+
): void {
|
|
3981
|
+
if (notification.method !== "item/started" && notification.method !== "item/completed") {
|
|
3982
|
+
return;
|
|
3983
|
+
}
|
|
3984
|
+
const itemId = readNotificationItemId(notification);
|
|
3985
|
+
if (!itemId) {
|
|
3986
|
+
return;
|
|
3987
|
+
}
|
|
3988
|
+
if (notification.method === "item/started") {
|
|
3989
|
+
activeItemIds.add(itemId);
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
3992
|
+
activeItemIds.delete(itemId);
|
|
3993
|
+
}
|
|
3994
|
+
|
|
3995
|
+
function isCompletedAssistantNotification(notification: CodexServerNotification): boolean {
|
|
3996
|
+
if (!isJsonObject(notification.params)) {
|
|
3997
|
+
return false;
|
|
3998
|
+
}
|
|
3999
|
+
if (notification.method !== "item/completed") {
|
|
4000
|
+
return false;
|
|
4001
|
+
}
|
|
4002
|
+
const item = isJsonObject(notification.params.item) ? notification.params.item : undefined;
|
|
4003
|
+
return Boolean(
|
|
4004
|
+
item &&
|
|
4005
|
+
readString(item, "type") === "agentMessage" &&
|
|
4006
|
+
readString(item, "phase") !== "commentary",
|
|
4007
|
+
);
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
function isAssistantCompletionReleaseNotification(
|
|
4011
|
+
notification: CodexServerNotification,
|
|
4012
|
+
turnCrossedToolHandoff: boolean,
|
|
4013
|
+
): boolean {
|
|
4014
|
+
if (isCompletedAssistantNotification(notification)) {
|
|
4015
|
+
return true;
|
|
4016
|
+
}
|
|
4017
|
+
return !turnCrossedToolHandoff && isRawAssistantCompletionNotification(notification);
|
|
4018
|
+
}
|
|
4019
|
+
|
|
4020
|
+
function shouldDisarmAssistantCompletionIdleWatch(notification: CodexServerNotification): boolean {
|
|
4021
|
+
if (!isJsonObject(notification.params)) {
|
|
4022
|
+
return false;
|
|
4023
|
+
}
|
|
4024
|
+
if (notification.method === "item/started") {
|
|
4025
|
+
return true;
|
|
4026
|
+
}
|
|
4027
|
+
if (notification.method === "item/agentMessage/delta") {
|
|
4028
|
+
return true;
|
|
4029
|
+
}
|
|
4030
|
+
return false;
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
function readNotificationItemId(notification: CodexServerNotification): string | undefined {
|
|
4034
|
+
if (!isJsonObject(notification.params)) {
|
|
4035
|
+
return undefined;
|
|
4036
|
+
}
|
|
4037
|
+
const item = isJsonObject(notification.params.item) ? notification.params.item : undefined;
|
|
4038
|
+
return (
|
|
4039
|
+
(item ? readString(item, "id") : undefined) ??
|
|
4040
|
+
readString(notification.params, "itemId") ??
|
|
4041
|
+
readString(notification.params, "id")
|
|
4042
|
+
);
|
|
4043
|
+
}
|
|
4044
|
+
|
|
4045
|
+
function isPendingAutoBotDynamicToolCompletionNotification(
|
|
4046
|
+
notification: CodexServerNotification,
|
|
4047
|
+
pendingAutoBotDynamicToolCompletionIds: ReadonlySet<string>,
|
|
4048
|
+
): boolean {
|
|
4049
|
+
if (notification.method !== "item/completed" || !isJsonObject(notification.params)) {
|
|
4050
|
+
return false;
|
|
4051
|
+
}
|
|
4052
|
+
const itemId = readNotificationItemId(notification);
|
|
4053
|
+
if (!itemId || !pendingAutoBotDynamicToolCompletionIds.has(itemId)) {
|
|
4054
|
+
return false;
|
|
4055
|
+
}
|
|
4056
|
+
const item = isJsonObject(notification.params.item) ? notification.params.item : undefined;
|
|
4057
|
+
const itemType = item ? readString(item, "type") : undefined;
|
|
4058
|
+
return itemType === undefined || itemType === "dynamicToolCall";
|
|
4059
|
+
}
|
|
4060
|
+
|
|
4061
|
+
function isRawToolOutputCompletionNotification(notification: CodexServerNotification): boolean {
|
|
4062
|
+
if (notification.method !== "rawResponseItem/completed" || !isJsonObject(notification.params)) {
|
|
4063
|
+
return false;
|
|
4064
|
+
}
|
|
4065
|
+
const item = isJsonObject(notification.params.item) ? notification.params.item : undefined;
|
|
4066
|
+
return item ? readString(item, "type") === "custom_tool_call_output" : false;
|
|
4067
|
+
}
|
|
4068
|
+
|
|
4069
|
+
function isNativeToolProgressNotification(notification: CodexServerNotification): boolean {
|
|
4070
|
+
if (
|
|
4071
|
+
notification.method !== "item/started" &&
|
|
4072
|
+
notification.method !== "item/completed" &&
|
|
4073
|
+
notification.method !== "item/updated"
|
|
4074
|
+
) {
|
|
4075
|
+
return false;
|
|
4076
|
+
}
|
|
4077
|
+
if (!isJsonObject(notification.params)) {
|
|
4078
|
+
return false;
|
|
4079
|
+
}
|
|
4080
|
+
const item = isJsonObject(notification.params.item) ? notification.params.item : undefined;
|
|
4081
|
+
switch (item ? readString(item, "type") : undefined) {
|
|
4082
|
+
case "commandExecution":
|
|
4083
|
+
case "fileChange":
|
|
4084
|
+
case "mcpToolCall":
|
|
4085
|
+
case "webSearch":
|
|
4086
|
+
return true;
|
|
4087
|
+
default:
|
|
4088
|
+
return false;
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
|
|
4092
|
+
function isRawAssistantCompletionNotification(notification: CodexServerNotification): boolean {
|
|
4093
|
+
if (notification.method !== "rawResponseItem/completed" || !isJsonObject(notification.params)) {
|
|
4094
|
+
return false;
|
|
4095
|
+
}
|
|
4096
|
+
const item = isJsonObject(notification.params.item) ? notification.params.item : undefined;
|
|
4097
|
+
return Boolean(
|
|
4098
|
+
item &&
|
|
4099
|
+
readString(item, "type") === "message" &&
|
|
4100
|
+
readString(item, "role") === "assistant" &&
|
|
4101
|
+
readString(item, "phase") !== "commentary" &&
|
|
4102
|
+
readRawAssistantTextPreview(item),
|
|
4103
|
+
);
|
|
4104
|
+
}
|
|
4105
|
+
|
|
4106
|
+
function readRawAssistantTextPreview(item: JsonObject): string | undefined {
|
|
4107
|
+
if (readString(item, "role") !== "assistant" || !Array.isArray(item.content)) {
|
|
4108
|
+
return undefined;
|
|
4109
|
+
}
|
|
4110
|
+
const text = item.content
|
|
4111
|
+
.flatMap((content) => {
|
|
4112
|
+
if (!isJsonObject(content)) {
|
|
4113
|
+
return [];
|
|
4114
|
+
}
|
|
4115
|
+
const contentText = readString(content, "text");
|
|
4116
|
+
return contentText ? [contentText] : [];
|
|
4117
|
+
})
|
|
4118
|
+
.join("\n")
|
|
4119
|
+
.trim();
|
|
4120
|
+
if (!text) {
|
|
4121
|
+
return undefined;
|
|
4122
|
+
}
|
|
4123
|
+
return text.length > 240 ? `${text.slice(0, 237)}...` : text;
|
|
4124
|
+
}
|
|
4125
|
+
|
|
4126
|
+
function isTurnNotification(
|
|
4127
|
+
value: JsonValue | undefined,
|
|
4128
|
+
threadId: string,
|
|
4129
|
+
turnId: string,
|
|
4130
|
+
): boolean {
|
|
4131
|
+
if (!isJsonObject(value)) {
|
|
4132
|
+
return false;
|
|
4133
|
+
}
|
|
4134
|
+
return readString(value, "threadId") === threadId && readNotificationTurnId(value) === turnId;
|
|
4135
|
+
}
|
|
4136
|
+
|
|
4137
|
+
function isCurrentThreadTurnRequestParams(
|
|
4138
|
+
value: JsonValue | undefined,
|
|
4139
|
+
threadId: string,
|
|
4140
|
+
turnId: string,
|
|
4141
|
+
): boolean {
|
|
4142
|
+
if (!isJsonObject(value)) {
|
|
4143
|
+
return false;
|
|
4144
|
+
}
|
|
4145
|
+
return readString(value, "threadId") === threadId && readString(value, "turnId") === turnId;
|
|
4146
|
+
}
|
|
4147
|
+
|
|
4148
|
+
function isCurrentApprovalTurnRequestParams(
|
|
4149
|
+
value: JsonValue | undefined,
|
|
4150
|
+
threadId: string,
|
|
4151
|
+
turnId: string,
|
|
4152
|
+
): boolean {
|
|
4153
|
+
if (!isJsonObject(value)) {
|
|
4154
|
+
return false;
|
|
4155
|
+
}
|
|
4156
|
+
const requestThreadId = readString(value, "threadId") ?? readString(value, "conversationId");
|
|
4157
|
+
return requestThreadId === threadId && readString(value, "turnId") === turnId;
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
function isCurrentThreadOptionalTurnRequestParams(
|
|
4161
|
+
value: JsonValue | undefined,
|
|
4162
|
+
threadId: string,
|
|
4163
|
+
turnId: string,
|
|
4164
|
+
): boolean {
|
|
4165
|
+
if (!isJsonObject(value) || readString(value, "threadId") !== threadId) {
|
|
4166
|
+
return false;
|
|
4167
|
+
}
|
|
4168
|
+
const requestTurnId = value.turnId;
|
|
4169
|
+
return requestTurnId === null || requestTurnId === undefined || requestTurnId === turnId;
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
function isRetryableErrorNotification(value: JsonValue | undefined): boolean {
|
|
4173
|
+
if (!isJsonObject(value)) {
|
|
4174
|
+
return false;
|
|
4175
|
+
}
|
|
4176
|
+
return readBoolean(value, "willRetry") === true || readBoolean(value, "will_retry") === true;
|
|
4177
|
+
}
|
|
4178
|
+
|
|
4179
|
+
function isTerminalTurnStatus(status: string | undefined): boolean {
|
|
4180
|
+
return status === "completed" || status === "interrupted" || status === "failed";
|
|
4181
|
+
}
|
|
4182
|
+
|
|
4183
|
+
function readNotificationTurnId(record: JsonObject): string | undefined {
|
|
4184
|
+
return readString(record, "turnId") ?? readNestedTurnId(record);
|
|
4185
|
+
}
|
|
4186
|
+
|
|
4187
|
+
function readNestedTurnId(record: JsonObject): string | undefined {
|
|
4188
|
+
const turn = record.turn;
|
|
4189
|
+
return isJsonObject(turn) ? readString(turn, "id") : undefined;
|
|
4190
|
+
}
|
|
4191
|
+
|
|
4192
|
+
const CODEX_TURN_ABORT_MARKER_START = "<turn_aborted>";
|
|
4193
|
+
const CODEX_TURN_ABORT_MARKER_END = "</turn_aborted>";
|
|
4194
|
+
const CODEX_INTERRUPTED_USER_GUIDANCE =
|
|
4195
|
+
"The user interrupted the previous turn on purpose. Any running unified exec processes may still be running in the background. If any tools/commands were aborted, they may have partially executed.";
|
|
4196
|
+
const CODEX_INTERRUPTED_DEVELOPER_GUIDANCE =
|
|
4197
|
+
"The previous turn was interrupted on purpose. Any running unified exec processes may still be running in the background. If any tools/commands were aborted, they may have partially executed.";
|
|
4198
|
+
|
|
4199
|
+
function isCodexTurnAbortMarkerNotification(
|
|
4200
|
+
notification: CodexServerNotification,
|
|
4201
|
+
options: { currentPromptText?: string; currentPromptTexts?: readonly string[] } = {},
|
|
4202
|
+
): boolean {
|
|
4203
|
+
if (notification.method !== "rawResponseItem/completed" || !isJsonObject(notification.params)) {
|
|
4204
|
+
return false;
|
|
4205
|
+
}
|
|
4206
|
+
const item = notification.params.item;
|
|
4207
|
+
const role = isJsonObject(item) ? readString(item, "role") : undefined;
|
|
4208
|
+
if (!isJsonObject(item) || (role !== "user" && role !== "developer")) {
|
|
4209
|
+
return false;
|
|
4210
|
+
}
|
|
4211
|
+
const text = extractRawResponseItemText(item).trim();
|
|
4212
|
+
const currentPromptTexts = [options.currentPromptText, ...(options.currentPromptTexts ?? [])]
|
|
4213
|
+
.filter(isNonEmptyString)
|
|
4214
|
+
.map((prompt) => prompt.trim());
|
|
4215
|
+
if (role === "user" && currentPromptTexts.includes(text)) {
|
|
4216
|
+
return false;
|
|
4217
|
+
}
|
|
4218
|
+
const markerBody = readCodexTurnAbortMarkerBody(text);
|
|
4219
|
+
return (
|
|
4220
|
+
markerBody === CODEX_INTERRUPTED_USER_GUIDANCE ||
|
|
4221
|
+
markerBody === CODEX_INTERRUPTED_DEVELOPER_GUIDANCE
|
|
4222
|
+
);
|
|
4223
|
+
}
|
|
4224
|
+
|
|
4225
|
+
function readCodexTurnAbortMarkerBody(text: string): string | undefined {
|
|
4226
|
+
if (
|
|
4227
|
+
!text.startsWith(CODEX_TURN_ABORT_MARKER_START) ||
|
|
4228
|
+
!text.endsWith(CODEX_TURN_ABORT_MARKER_END)
|
|
4229
|
+
) {
|
|
4230
|
+
return undefined;
|
|
4231
|
+
}
|
|
4232
|
+
return text
|
|
4233
|
+
.slice(CODEX_TURN_ABORT_MARKER_START.length, -CODEX_TURN_ABORT_MARKER_END.length)
|
|
4234
|
+
.trim();
|
|
4235
|
+
}
|
|
4236
|
+
|
|
4237
|
+
function extractRawResponseItemText(item: JsonObject): string {
|
|
4238
|
+
const content = item.content;
|
|
4239
|
+
if (!Array.isArray(content)) {
|
|
4240
|
+
return "";
|
|
4241
|
+
}
|
|
4242
|
+
return content
|
|
4243
|
+
.flatMap((entry) => {
|
|
4244
|
+
if (!isJsonObject(entry)) {
|
|
4245
|
+
return [];
|
|
4246
|
+
}
|
|
4247
|
+
const type = readString(entry, "type");
|
|
4248
|
+
if (type !== "input_text" && type !== "text") {
|
|
4249
|
+
return [];
|
|
4250
|
+
}
|
|
4251
|
+
const text = readString(entry, "text");
|
|
4252
|
+
return text ? [text] : [];
|
|
4253
|
+
})
|
|
4254
|
+
.join("");
|
|
4255
|
+
}
|
|
4256
|
+
|
|
4257
|
+
function readString(record: JsonObject, key: string): string | undefined {
|
|
4258
|
+
const value = record[key];
|
|
4259
|
+
return typeof value === "string" ? value : undefined;
|
|
4260
|
+
}
|
|
4261
|
+
|
|
4262
|
+
function readBoolean(record: JsonObject, key: string): boolean | undefined {
|
|
4263
|
+
const value = record[key];
|
|
4264
|
+
return typeof value === "boolean" ? value : undefined;
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
async function readMirroredSessionHistoryMessages(
|
|
4268
|
+
sessionFile: string,
|
|
4269
|
+
): Promise<AgentMessage[] | undefined> {
|
|
4270
|
+
const messages = await readCodexMirroredSessionHistoryMessages(sessionFile);
|
|
4271
|
+
if (!messages) {
|
|
4272
|
+
embeddedAgentLog.warn("failed to read mirrored session history for codex harness hooks", {
|
|
4273
|
+
sessionFile,
|
|
4274
|
+
});
|
|
4275
|
+
}
|
|
4276
|
+
return messages;
|
|
4277
|
+
}
|
|
4278
|
+
|
|
4279
|
+
async function buildCodexWorkspaceBootstrapContext(params: {
|
|
4280
|
+
params: EmbeddedRunAttemptParams;
|
|
4281
|
+
resolvedWorkspace: string;
|
|
4282
|
+
effectiveWorkspace: string;
|
|
4283
|
+
sessionKey: string;
|
|
4284
|
+
sessionAgentId: string;
|
|
4285
|
+
}): Promise<CodexWorkspaceBootstrapContext> {
|
|
4286
|
+
try {
|
|
4287
|
+
const bootstrapContext = await resolveBootstrapContextForRun({
|
|
4288
|
+
workspaceDir: params.resolvedWorkspace,
|
|
4289
|
+
config: params.params.config,
|
|
4290
|
+
sessionKey: params.sessionKey,
|
|
4291
|
+
sessionId: params.params.sessionId,
|
|
4292
|
+
agentId: params.params.agentId ?? params.sessionAgentId,
|
|
4293
|
+
warn: (message) => embeddedAgentLog.warn(message),
|
|
4294
|
+
contextMode: params.params.bootstrapContextMode,
|
|
4295
|
+
runKind: params.params.bootstrapContextRunKind,
|
|
4296
|
+
});
|
|
4297
|
+
const contextFiles = bootstrapContext.contextFiles.map((file) =>
|
|
4298
|
+
remapCodexContextFilePath({
|
|
4299
|
+
file,
|
|
4300
|
+
sourceWorkspaceDir: params.resolvedWorkspace,
|
|
4301
|
+
targetWorkspaceDir: params.effectiveWorkspace,
|
|
4302
|
+
}),
|
|
4303
|
+
);
|
|
4304
|
+
const promptContextFiles = selectCodexWorkspacePromptContextFiles(contextFiles);
|
|
4305
|
+
const developerInstructionFiles = shouldInjectCodexAutoBotPromptContext(params.params)
|
|
4306
|
+
? selectCodexWorkspaceDeveloperInstructionFiles(contextFiles)
|
|
4307
|
+
: [];
|
|
4308
|
+
const heartbeatReferenceFiles = selectCodexWorkspaceHeartbeatReferenceFiles(contextFiles);
|
|
4309
|
+
return {
|
|
4310
|
+
...bootstrapContext,
|
|
4311
|
+
contextFiles,
|
|
4312
|
+
promptContextFiles,
|
|
4313
|
+
developerInstructionFiles,
|
|
4314
|
+
heartbeatReferenceFiles,
|
|
4315
|
+
promptContext: renderCodexWorkspaceBootstrapPromptContext(promptContextFiles),
|
|
4316
|
+
developerInstructions: renderCodexWorkspaceDeveloperInstructions(developerInstructionFiles),
|
|
4317
|
+
heartbeatCollaborationInstructions:
|
|
4318
|
+
renderCodexWorkspaceHeartbeatReference(heartbeatReferenceFiles),
|
|
4319
|
+
};
|
|
4320
|
+
} catch (error) {
|
|
4321
|
+
embeddedAgentLog.warn("failed to load codex workspace bootstrap instructions", { error });
|
|
4322
|
+
return { bootstrapFiles: [], contextFiles: [] };
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
4325
|
+
|
|
4326
|
+
function buildCodexSystemPromptReport(params: {
|
|
4327
|
+
attempt: EmbeddedRunAttemptParams;
|
|
4328
|
+
sessionKey: string;
|
|
4329
|
+
workspaceDir: string;
|
|
4330
|
+
developerInstructions: string;
|
|
4331
|
+
workspaceBootstrapContext: CodexWorkspaceBootstrapContext;
|
|
4332
|
+
skillsPrompt: string;
|
|
4333
|
+
tools: CodexDynamicToolSpec[];
|
|
4334
|
+
}): CodexSystemPromptReport {
|
|
4335
|
+
const toolEntries = params.tools.map(buildCodexToolReportEntry);
|
|
4336
|
+
const schemaChars = toolEntries.reduce((sum, tool) => sum + tool.schemaChars, 0);
|
|
4337
|
+
const skillsPrompt = params.skillsPrompt.trim();
|
|
4338
|
+
const bootstrapMaxChars = readPositiveNumber(
|
|
4339
|
+
params.attempt.config?.agents?.defaults?.bootstrapMaxChars,
|
|
4340
|
+
);
|
|
4341
|
+
const bootstrapTotalMaxChars = readPositiveNumber(
|
|
4342
|
+
params.attempt.config?.agents?.defaults?.bootstrapTotalMaxChars,
|
|
4343
|
+
);
|
|
4344
|
+
return {
|
|
4345
|
+
source: "run",
|
|
4346
|
+
generatedAt: Date.now(),
|
|
4347
|
+
sessionId: params.attempt.sessionId,
|
|
4348
|
+
sessionKey: params.sessionKey,
|
|
4349
|
+
provider: params.attempt.provider,
|
|
4350
|
+
model: params.attempt.modelId,
|
|
4351
|
+
workspaceDir: params.workspaceDir,
|
|
4352
|
+
...(bootstrapMaxChars ? { bootstrapMaxChars } : {}),
|
|
4353
|
+
...(bootstrapTotalMaxChars ? { bootstrapTotalMaxChars } : {}),
|
|
4354
|
+
systemPrompt: {
|
|
4355
|
+
chars: params.developerInstructions.length,
|
|
4356
|
+
projectContextChars: 0,
|
|
4357
|
+
nonProjectContextChars: params.developerInstructions.length,
|
|
4358
|
+
},
|
|
4359
|
+
injectedWorkspaceFiles: buildCodexBootstrapInjectionStats({
|
|
4360
|
+
bootstrapFiles: params.workspaceBootstrapContext.bootstrapFiles,
|
|
4361
|
+
injectedFiles: params.workspaceBootstrapContext.promptContextFiles ?? [],
|
|
4362
|
+
developerInstructionFiles: params.workspaceBootstrapContext.developerInstructionFiles ?? [],
|
|
4363
|
+
}),
|
|
4364
|
+
skills: {
|
|
4365
|
+
promptChars: skillsPrompt.length,
|
|
4366
|
+
entries: buildCodexSkillReportEntries(skillsPrompt),
|
|
4367
|
+
},
|
|
4368
|
+
tools: {
|
|
4369
|
+
listChars: 0,
|
|
4370
|
+
schemaChars,
|
|
4371
|
+
entries: toolEntries,
|
|
4372
|
+
},
|
|
4373
|
+
};
|
|
4374
|
+
}
|
|
4375
|
+
|
|
4376
|
+
function buildCodexSkillReportEntries(
|
|
4377
|
+
skillsPrompt: string,
|
|
4378
|
+
): CodexSystemPromptReport["skills"]["entries"] {
|
|
4379
|
+
if (!skillsPrompt) {
|
|
4380
|
+
return [];
|
|
4381
|
+
}
|
|
4382
|
+
return Array.from(skillsPrompt.matchAll(/<skill>[\s\S]*?<\/skill>/gi))
|
|
4383
|
+
.map((match) => match[0] ?? "")
|
|
4384
|
+
.map((block) => ({
|
|
4385
|
+
name: block.match(/<name>\s*([^<]+?)\s*<\/name>/i)?.[1]?.trim() || "(unknown)",
|
|
4386
|
+
blockChars: block.length,
|
|
4387
|
+
}))
|
|
4388
|
+
.filter((entry) => entry.blockChars > 0);
|
|
4389
|
+
}
|
|
4390
|
+
|
|
4391
|
+
function buildCodexToolReportEntry(tool: CodexDynamicToolSpec): CodexToolReportEntry {
|
|
4392
|
+
const summary = tool.description.trim();
|
|
4393
|
+
if (tool.deferLoading === true) {
|
|
4394
|
+
return {
|
|
4395
|
+
name: tool.name,
|
|
4396
|
+
summaryChars: summary.length,
|
|
4397
|
+
schemaChars: 0,
|
|
4398
|
+
propertiesCount: null,
|
|
4399
|
+
};
|
|
4400
|
+
}
|
|
4401
|
+
return {
|
|
4402
|
+
name: tool.name,
|
|
4403
|
+
summaryChars: summary.length,
|
|
4404
|
+
...buildCodexToolSchemaStats(tool.inputSchema),
|
|
4405
|
+
};
|
|
4406
|
+
}
|
|
4407
|
+
|
|
4408
|
+
function buildCodexToolSchemaStats(
|
|
4409
|
+
schema: JsonValue,
|
|
4410
|
+
): Pick<CodexToolReportEntry, "schemaChars" | "propertiesCount"> {
|
|
4411
|
+
const schemaChars = (() => {
|
|
4412
|
+
try {
|
|
4413
|
+
return JSON.stringify(schema).length;
|
|
4414
|
+
} catch {
|
|
4415
|
+
return 0;
|
|
4416
|
+
}
|
|
4417
|
+
})();
|
|
4418
|
+
const properties =
|
|
4419
|
+
isJsonObject(schema) && isJsonObject(schema.properties) ? schema.properties : null;
|
|
4420
|
+
return {
|
|
4421
|
+
schemaChars,
|
|
4422
|
+
propertiesCount: properties ? Object.keys(properties).length : null,
|
|
4423
|
+
};
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4426
|
+
function buildCodexBootstrapInjectionStats(params: {
|
|
4427
|
+
bootstrapFiles: CodexBootstrapFile[];
|
|
4428
|
+
injectedFiles: EmbeddedContextFile[];
|
|
4429
|
+
developerInstructionFiles?: EmbeddedContextFile[];
|
|
4430
|
+
}): CodexSystemPromptReport["injectedWorkspaceFiles"] {
|
|
4431
|
+
const injectedIndex = indexCodexContextFileContent(params.injectedFiles);
|
|
4432
|
+
const developerInstructionIndex = indexCodexContextFileContent(
|
|
4433
|
+
params.developerInstructionFiles ?? [],
|
|
4434
|
+
);
|
|
4435
|
+
return params.bootstrapFiles.map((file) => {
|
|
4436
|
+
const pathValue = readNonEmptyString(file.path) ?? file.name;
|
|
4437
|
+
const baseName = getCodexContextFileBasename(pathValue || file.name);
|
|
4438
|
+
const rawChars = file.missing ? 0 : (file.content ?? "").trimEnd().length;
|
|
4439
|
+
const injected =
|
|
4440
|
+
readCodexIndexedContextFileContent(injectedIndex, pathValue, file.name) ??
|
|
4441
|
+
readCodexIndexedContextFileContent(developerInstructionIndex, pathValue, file.name);
|
|
4442
|
+
let injectedChars = injected?.length ?? 0;
|
|
4443
|
+
let truncated = !file.missing && injectedChars < rawChars;
|
|
4444
|
+
if (injected === undefined) {
|
|
4445
|
+
if (CODEX_NATIVE_PROJECT_DOC_BASENAMES.has(baseName)) {
|
|
4446
|
+
injectedChars = rawChars;
|
|
4447
|
+
truncated = false;
|
|
4448
|
+
} else if (baseName === CODEX_HEARTBEAT_CONTEXT_BASENAME) {
|
|
4449
|
+
injectedChars = 0;
|
|
4450
|
+
truncated = false;
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
return {
|
|
4454
|
+
name: file.name,
|
|
4455
|
+
path: pathValue,
|
|
4456
|
+
missing: file.missing,
|
|
4457
|
+
rawChars,
|
|
4458
|
+
injectedChars,
|
|
4459
|
+
truncated,
|
|
4460
|
+
};
|
|
4461
|
+
});
|
|
4462
|
+
}
|
|
4463
|
+
|
|
4464
|
+
function indexCodexContextFileContent(files: EmbeddedContextFile[]): {
|
|
4465
|
+
byPath: Map<string, string>;
|
|
4466
|
+
byBaseName: Map<string, string>;
|
|
4467
|
+
} {
|
|
4468
|
+
const byPath = new Map<string, string>();
|
|
4469
|
+
const byBaseName = new Map<string, string>();
|
|
4470
|
+
for (const file of files) {
|
|
4471
|
+
const pathValue = readNonEmptyString(file.path);
|
|
4472
|
+
if (!pathValue) {
|
|
4473
|
+
continue;
|
|
4474
|
+
}
|
|
4475
|
+
if (!byPath.has(pathValue)) {
|
|
4476
|
+
byPath.set(pathValue, file.content);
|
|
4477
|
+
}
|
|
4478
|
+
const baseName = getCodexContextFileBasename(pathValue);
|
|
4479
|
+
if (baseName && !byBaseName.has(baseName)) {
|
|
4480
|
+
byBaseName.set(baseName, file.content);
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
return { byPath, byBaseName };
|
|
4484
|
+
}
|
|
4485
|
+
|
|
4486
|
+
function readCodexIndexedContextFileContent(
|
|
4487
|
+
index: { byPath: Map<string, string>; byBaseName: Map<string, string> },
|
|
4488
|
+
pathValue: string,
|
|
4489
|
+
fileName: string,
|
|
4490
|
+
): string | undefined {
|
|
4491
|
+
return (
|
|
4492
|
+
index.byPath.get(pathValue) ??
|
|
4493
|
+
index.byPath.get(fileName) ??
|
|
4494
|
+
index.byBaseName.get(getCodexContextFileBasename(fileName))
|
|
4495
|
+
);
|
|
4496
|
+
}
|
|
4497
|
+
|
|
4498
|
+
function readPositiveNumber(value: unknown): number | undefined {
|
|
4499
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
4500
|
+
? Math.floor(value)
|
|
4501
|
+
: undefined;
|
|
4502
|
+
}
|
|
4503
|
+
|
|
4504
|
+
function readNonEmptyString(value: unknown): string | undefined {
|
|
4505
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
4506
|
+
}
|
|
4507
|
+
|
|
4508
|
+
function buildCodexAutoBotPromptContext(params: {
|
|
4509
|
+
params: EmbeddedRunAttemptParams;
|
|
4510
|
+
skillsPrompt?: string;
|
|
4511
|
+
workspacePromptContext?: string;
|
|
4512
|
+
}): string | undefined {
|
|
4513
|
+
if (!shouldInjectCodexAutoBotPromptContext(params.params)) {
|
|
4514
|
+
return undefined;
|
|
4515
|
+
}
|
|
4516
|
+
const sections = [
|
|
4517
|
+
params.skillsPrompt?.trim()
|
|
4518
|
+
? ["## AutoBot Skills", "", params.skillsPrompt.trim()].join("\n")
|
|
4519
|
+
: undefined,
|
|
4520
|
+
params.workspacePromptContext?.trim()
|
|
4521
|
+
? ["## AutoBot Workspace Context", "", params.workspacePromptContext.trim()].join("\n")
|
|
4522
|
+
: undefined,
|
|
4523
|
+
].filter(isNonEmptyString);
|
|
4524
|
+
if (sections.length === 0) {
|
|
4525
|
+
return undefined;
|
|
4526
|
+
}
|
|
4527
|
+
return [
|
|
4528
|
+
"AutoBot runtime context for this turn:",
|
|
4529
|
+
"Treat this AutoBot-provided context as supporting project/user reference for the current request.",
|
|
4530
|
+
"",
|
|
4531
|
+
...sections,
|
|
4532
|
+
].join("\n");
|
|
4533
|
+
}
|
|
4534
|
+
|
|
4535
|
+
function shouldInjectCodexAutoBotPromptContext(params: EmbeddedRunAttemptParams): boolean {
|
|
4536
|
+
// Lightweight cron runs are commonly exact commands. Keep the user input byte-for-byte
|
|
4537
|
+
// to avoid changing command intent while Codex keeps its native project-doc loader.
|
|
4538
|
+
return !(
|
|
4539
|
+
params.bootstrapContextMode === "lightweight" && params.bootstrapContextRunKind === "cron"
|
|
4540
|
+
);
|
|
4541
|
+
}
|
|
4542
|
+
|
|
4543
|
+
function prependCodexAutoBotPromptContext(prompt: string, context: string | undefined): string {
|
|
4544
|
+
if (!context?.trim()) {
|
|
4545
|
+
return prompt;
|
|
4546
|
+
}
|
|
4547
|
+
const promptSection = prompt.startsWith("AutoBot assembled context for this turn:")
|
|
4548
|
+
? prompt
|
|
4549
|
+
: ["Current user request:", prompt].join("\n");
|
|
4550
|
+
return [context.trim(), "", promptSection].join("\n");
|
|
4551
|
+
}
|
|
4552
|
+
|
|
4553
|
+
function renderCodexWorkspaceBootstrapPromptContext(
|
|
4554
|
+
contextFiles: EmbeddedContextFile[],
|
|
4555
|
+
): string | undefined {
|
|
4556
|
+
const files = selectCodexWorkspacePromptContextFiles(contextFiles);
|
|
4557
|
+
if (files.length === 0) {
|
|
4558
|
+
return undefined;
|
|
4559
|
+
}
|
|
4560
|
+
const lines = [
|
|
4561
|
+
"AutoBot loaded these user-editable workspace files for the current turn. Codex loads AGENTS.md natively. SOUL.md, IDENTITY.md, TOOLS.md, and USER.md are provided separately as Codex developer instructions. HEARTBEAT.md is handled by heartbeat collaboration-mode guidance. Those files are not repeated here.",
|
|
4562
|
+
"",
|
|
4563
|
+
"# Project Context",
|
|
4564
|
+
"",
|
|
4565
|
+
"The following project context files have been loaded:",
|
|
4566
|
+
];
|
|
4567
|
+
lines.push("");
|
|
4568
|
+
for (const file of files) {
|
|
4569
|
+
lines.push(`## ${file.path}`, "", file.content, "");
|
|
4570
|
+
}
|
|
4571
|
+
return lines.join("\n").trim();
|
|
4572
|
+
}
|
|
4573
|
+
|
|
4574
|
+
function selectCodexWorkspacePromptContextFiles(
|
|
4575
|
+
contextFiles: EmbeddedContextFile[],
|
|
4576
|
+
): EmbeddedContextFile[] {
|
|
4577
|
+
return contextFiles
|
|
4578
|
+
.filter((file) => {
|
|
4579
|
+
const baseName = getCodexContextFileBasename(file.path);
|
|
4580
|
+
return (
|
|
4581
|
+
baseName &&
|
|
4582
|
+
!CODEX_NATIVE_PROJECT_DOC_BASENAMES.has(baseName) &&
|
|
4583
|
+
!CODEX_WORKSPACE_DEVELOPER_CONTEXT_BASENAMES.has(baseName) &&
|
|
4584
|
+
baseName !== CODEX_HEARTBEAT_CONTEXT_BASENAME &&
|
|
4585
|
+
!isMissingCodexBootstrapContextFile(file)
|
|
4586
|
+
);
|
|
4587
|
+
})
|
|
4588
|
+
.toSorted(compareCodexContextFiles);
|
|
4589
|
+
}
|
|
4590
|
+
|
|
4591
|
+
function selectCodexWorkspaceDeveloperInstructionFiles(
|
|
4592
|
+
contextFiles: EmbeddedContextFile[],
|
|
4593
|
+
): EmbeddedContextFile[] {
|
|
4594
|
+
return contextFiles
|
|
4595
|
+
.filter((file) => {
|
|
4596
|
+
const baseName = getCodexContextFileBasename(file.path);
|
|
4597
|
+
return (
|
|
4598
|
+
baseName &&
|
|
4599
|
+
CODEX_WORKSPACE_DEVELOPER_CONTEXT_BASENAMES.has(baseName) &&
|
|
4600
|
+
!isMissingCodexBootstrapContextFile(file) &&
|
|
4601
|
+
file.content.trim().length > 0
|
|
4602
|
+
);
|
|
4603
|
+
})
|
|
4604
|
+
.toSorted(compareCodexContextFiles);
|
|
4605
|
+
}
|
|
4606
|
+
|
|
4607
|
+
function renderCodexWorkspaceDeveloperInstructions(
|
|
4608
|
+
files: EmbeddedContextFile[],
|
|
4609
|
+
): string | undefined {
|
|
4610
|
+
if (files.length === 0) {
|
|
4611
|
+
return undefined;
|
|
4612
|
+
}
|
|
4613
|
+
const lines = [
|
|
4614
|
+
"## AutoBot Agent Soul",
|
|
4615
|
+
"",
|
|
4616
|
+
"AutoBot loaded these workspace instruction files from the active agent workspace. They define who you are, how you work, what tools are available, and the human you work alongside. Internalize and follow them accordingly.",
|
|
4617
|
+
"",
|
|
4618
|
+
];
|
|
4619
|
+
for (const file of files) {
|
|
4620
|
+
lines.push(`### ${file.path}`, "", file.content, "");
|
|
4621
|
+
}
|
|
4622
|
+
return lines.join("\n").trim();
|
|
4623
|
+
}
|
|
4624
|
+
|
|
4625
|
+
function selectCodexWorkspaceHeartbeatReferenceFiles(
|
|
4626
|
+
contextFiles: EmbeddedContextFile[],
|
|
4627
|
+
): EmbeddedContextFile[] {
|
|
4628
|
+
return contextFiles
|
|
4629
|
+
.filter((file) => {
|
|
4630
|
+
const baseName = getCodexContextFileBasename(file.path);
|
|
4631
|
+
return (
|
|
4632
|
+
baseName === CODEX_HEARTBEAT_CONTEXT_BASENAME &&
|
|
4633
|
+
!isMissingCodexBootstrapContextFile(file) &&
|
|
4634
|
+
file.content.trim().length > 0
|
|
4635
|
+
);
|
|
4636
|
+
})
|
|
4637
|
+
.toSorted(compareCodexContextFiles);
|
|
4638
|
+
}
|
|
4639
|
+
|
|
4640
|
+
function renderCodexWorkspaceHeartbeatReference(files: EmbeddedContextFile[]): string | undefined {
|
|
4641
|
+
if (files.length === 0) {
|
|
4642
|
+
return undefined;
|
|
4643
|
+
}
|
|
4644
|
+
const lines = [
|
|
4645
|
+
"## AutoBot Heartbeat Workspace",
|
|
4646
|
+
"",
|
|
4647
|
+
"HEARTBEAT.md exists in the active agent workspace. Read it before proceeding with this heartbeat, then decide what action is appropriate.",
|
|
4648
|
+
"",
|
|
4649
|
+
];
|
|
4650
|
+
for (const file of files) {
|
|
4651
|
+
lines.push(`- ${file.path}`);
|
|
4652
|
+
}
|
|
4653
|
+
return lines.join("\n").trim();
|
|
4654
|
+
}
|
|
4655
|
+
|
|
4656
|
+
function isMissingCodexBootstrapContextFile(file: EmbeddedContextFile): boolean {
|
|
4657
|
+
return file.content.trimStart().startsWith("[MISSING] Expected at:");
|
|
4658
|
+
}
|
|
4659
|
+
|
|
4660
|
+
function remapCodexContextFilePath(params: {
|
|
4661
|
+
file: EmbeddedContextFile;
|
|
4662
|
+
sourceWorkspaceDir: string;
|
|
4663
|
+
targetWorkspaceDir: string;
|
|
4664
|
+
}): EmbeddedContextFile {
|
|
4665
|
+
const relativePath = path.relative(params.sourceWorkspaceDir, params.file.path);
|
|
4666
|
+
if (
|
|
4667
|
+
!relativePath ||
|
|
4668
|
+
relativePath === ".." ||
|
|
4669
|
+
relativePath.startsWith(`..${path.sep}`) ||
|
|
4670
|
+
path.isAbsolute(relativePath) ||
|
|
4671
|
+
params.sourceWorkspaceDir === params.targetWorkspaceDir
|
|
4672
|
+
) {
|
|
4673
|
+
return params.file;
|
|
4674
|
+
}
|
|
4675
|
+
return {
|
|
4676
|
+
...params.file,
|
|
4677
|
+
path: path.join(params.targetWorkspaceDir, relativePath),
|
|
4678
|
+
};
|
|
4679
|
+
}
|
|
4680
|
+
|
|
4681
|
+
function compareCodexContextFiles(left: EmbeddedContextFile, right: EmbeddedContextFile): number {
|
|
4682
|
+
const leftPath = normalizeCodexContextFilePath(left.path);
|
|
4683
|
+
const rightPath = normalizeCodexContextFilePath(right.path);
|
|
4684
|
+
const leftBase = getCodexContextFileBasename(left.path);
|
|
4685
|
+
const rightBase = getCodexContextFileBasename(right.path);
|
|
4686
|
+
const leftOrder = CODEX_BOOTSTRAP_CONTEXT_ORDER.get(leftBase) ?? Number.MAX_SAFE_INTEGER;
|
|
4687
|
+
const rightOrder = CODEX_BOOTSTRAP_CONTEXT_ORDER.get(rightBase) ?? Number.MAX_SAFE_INTEGER;
|
|
4688
|
+
if (leftOrder !== rightOrder) {
|
|
4689
|
+
return leftOrder - rightOrder;
|
|
4690
|
+
}
|
|
4691
|
+
if (leftBase !== rightBase) {
|
|
4692
|
+
return leftBase.localeCompare(rightBase);
|
|
4693
|
+
}
|
|
4694
|
+
return leftPath.localeCompare(rightPath);
|
|
4695
|
+
}
|
|
4696
|
+
|
|
4697
|
+
function normalizeCodexContextFilePath(filePath: string): string {
|
|
4698
|
+
return filePath.trim().replaceAll("\\", "/").toLowerCase();
|
|
4699
|
+
}
|
|
4700
|
+
|
|
4701
|
+
function getCodexContextFileBasename(filePath: string): string {
|
|
4702
|
+
return normalizeCodexContextFilePath(filePath).split("/").pop() ?? "";
|
|
4703
|
+
}
|
|
4704
|
+
|
|
4705
|
+
async function mirrorTranscriptBestEffort(params: {
|
|
4706
|
+
params: EmbeddedRunAttemptParams;
|
|
4707
|
+
agentId?: string;
|
|
4708
|
+
result: EmbeddedRunAttemptResult;
|
|
4709
|
+
sessionKey?: string;
|
|
4710
|
+
threadId: string;
|
|
4711
|
+
turnId: string;
|
|
4712
|
+
}): Promise<void> {
|
|
4713
|
+
try {
|
|
4714
|
+
await mirrorCodexAppServerTranscript({
|
|
4715
|
+
sessionFile: params.params.sessionFile,
|
|
4716
|
+
agentId: params.agentId,
|
|
4717
|
+
sessionKey: params.sessionKey,
|
|
4718
|
+
messages: params.result.messagesSnapshot,
|
|
4719
|
+
// Scope is thread-stable. Each entry in `messagesSnapshot` is tagged
|
|
4720
|
+
// with a per-turn `attachCodexMirrorIdentity` value carrying its own
|
|
4721
|
+
// turnId, so distinct turns produce distinct dedupe keys via the
|
|
4722
|
+
// identity (not via the scope). Dropping `turnId` from the scope
|
|
4723
|
+
// here is what lets a re-emitted prior-turn entry — which still
|
|
4724
|
+
// carries its original `${turnId}:${kind}` identity — collide with
|
|
4725
|
+
// its existing on-disk key and be a true no-op.
|
|
4726
|
+
idempotencyScope: `codex-app-server:${params.threadId}`,
|
|
4727
|
+
config: params.params.config,
|
|
4728
|
+
});
|
|
4729
|
+
} catch (error) {
|
|
4730
|
+
embeddedAgentLog.warn("failed to mirror codex app-server transcript", { error });
|
|
4731
|
+
}
|
|
4732
|
+
}
|
|
4733
|
+
|
|
4734
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
4735
|
+
return typeof value === "string" && value.length > 0;
|
|
4736
|
+
}
|
|
4737
|
+
|
|
4738
|
+
function shouldRetryContextEngineTurnOnFreshCodexThread(params: {
|
|
4739
|
+
error: unknown;
|
|
4740
|
+
contextEngineActive: boolean;
|
|
4741
|
+
thread: CodexAppServerThreadLifecycleBinding;
|
|
4742
|
+
}): boolean {
|
|
4743
|
+
if (!params.contextEngineActive || params.thread.lifecycle.action !== "resumed") {
|
|
4744
|
+
return false;
|
|
4745
|
+
}
|
|
4746
|
+
return isCodexContextWindowError(params.error);
|
|
4747
|
+
}
|
|
4748
|
+
|
|
4749
|
+
function isCodexContextWindowError(error: unknown): boolean {
|
|
4750
|
+
const message = formatErrorMessage(error);
|
|
4751
|
+
return (
|
|
4752
|
+
/ran out of room in the model'?s context window/iu.test(message) ||
|
|
4753
|
+
/context window/iu.test(message) ||
|
|
4754
|
+
/context length/iu.test(message) ||
|
|
4755
|
+
/maximum context/iu.test(message) ||
|
|
4756
|
+
/too many tokens/iu.test(message)
|
|
4757
|
+
);
|
|
4758
|
+
}
|
|
4759
|
+
|
|
4760
|
+
function readCodexNotificationItem(params: JsonValue | undefined): CodexThreadItem | undefined {
|
|
4761
|
+
if (!isJsonObject(params) || !isJsonObject(params.item)) {
|
|
4762
|
+
return undefined;
|
|
4763
|
+
}
|
|
4764
|
+
const item = params.item;
|
|
4765
|
+
return typeof item.id === "string" && typeof item.type === "string"
|
|
4766
|
+
? (item as CodexThreadItem)
|
|
4767
|
+
: undefined;
|
|
4768
|
+
}
|
|
4769
|
+
|
|
4770
|
+
function codexExecutionToolName(item: CodexThreadItem): string | undefined {
|
|
4771
|
+
if (item.type === "dynamicToolCall" && typeof item.tool === "string") {
|
|
4772
|
+
return item.tool;
|
|
4773
|
+
}
|
|
4774
|
+
if (item.type === "mcpToolCall" && typeof item.tool === "string") {
|
|
4775
|
+
const server = typeof item.server === "string" && item.server ? item.server : undefined;
|
|
4776
|
+
return server ? `${server}.${item.tool}` : item.tool;
|
|
4777
|
+
}
|
|
4778
|
+
if (item.type === "commandExecution") {
|
|
4779
|
+
return "bash";
|
|
4780
|
+
}
|
|
4781
|
+
if (item.type === "fileChange") {
|
|
4782
|
+
return "apply_patch";
|
|
4783
|
+
}
|
|
4784
|
+
if (item.type === "webSearch") {
|
|
4785
|
+
return "web_search";
|
|
4786
|
+
}
|
|
4787
|
+
return undefined;
|
|
4788
|
+
}
|
|
4789
|
+
|
|
4790
|
+
function joinPresentSections(...sections: Array<string | undefined>): string {
|
|
4791
|
+
return sections.filter((section): section is string => Boolean(section?.trim())).join("\n\n");
|
|
4792
|
+
}
|
|
4793
|
+
|
|
4794
|
+
function prependCurrentInboundContext(
|
|
4795
|
+
prompt: string,
|
|
4796
|
+
context: EmbeddedRunAttemptParams["currentInboundContext"],
|
|
4797
|
+
): string {
|
|
4798
|
+
const text = context?.text.trim();
|
|
4799
|
+
return text ? [text, prompt].filter(Boolean).join("\n\n") : prompt;
|
|
4800
|
+
}
|
|
4801
|
+
|
|
4802
|
+
function waitForCodexNotificationDispatchTurn(): Promise<void> {
|
|
4803
|
+
return new Promise((resolve) => {
|
|
4804
|
+
setImmediate(resolve);
|
|
4805
|
+
});
|
|
4806
|
+
}
|
|
4807
|
+
|
|
4808
|
+
function handleApprovalRequest(params: {
|
|
4809
|
+
method: string;
|
|
4810
|
+
params: JsonValue | undefined;
|
|
4811
|
+
paramsForRun: EmbeddedRunAttemptParams;
|
|
4812
|
+
threadId: string;
|
|
4813
|
+
turnId: string;
|
|
4814
|
+
nativeHookRelay?: NativeHookRelayRegistrationHandle;
|
|
4815
|
+
signal?: AbortSignal;
|
|
4816
|
+
}): Promise<JsonValue | undefined> {
|
|
4817
|
+
return handleCodexAppServerApprovalRequest({
|
|
4818
|
+
method: params.method,
|
|
4819
|
+
requestParams: params.params,
|
|
4820
|
+
paramsForRun: params.paramsForRun,
|
|
4821
|
+
threadId: params.threadId,
|
|
4822
|
+
turnId: params.turnId,
|
|
4823
|
+
nativeHookRelay: params.nativeHookRelay,
|
|
4824
|
+
signal: params.signal,
|
|
4825
|
+
});
|
|
4826
|
+
}
|
|
4827
|
+
|
|
4828
|
+
export const testing = {
|
|
4829
|
+
CODEX_DYNAMIC_TOOL_TIMEOUT_MS,
|
|
4830
|
+
CODEX_DYNAMIC_TOOL_MAX_TIMEOUT_MS,
|
|
4831
|
+
CODEX_DYNAMIC_IMAGE_TOOL_TIMEOUT_MS,
|
|
4832
|
+
CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS,
|
|
4833
|
+
CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS,
|
|
4834
|
+
createCodexSteeringQueue,
|
|
4835
|
+
buildCodexNativeHookRelayId,
|
|
4836
|
+
buildDeveloperInstructions,
|
|
4837
|
+
filterCodexDynamicTools,
|
|
4838
|
+
buildDynamicTools,
|
|
4839
|
+
addSandboxShellDynamicToolsIfAvailable,
|
|
4840
|
+
filterCodexDynamicToolsForAllowlist,
|
|
4841
|
+
filterToolsForVisionInputs,
|
|
4842
|
+
hasWildcardCodexToolsAllow,
|
|
4843
|
+
handleDynamicToolCallWithTimeout,
|
|
4844
|
+
isInvalidCodexImagePayloadError,
|
|
4845
|
+
remapCodexContextFilePath,
|
|
4846
|
+
resolveDynamicToolCallTimeoutMs,
|
|
4847
|
+
resolveCodexDynamicToolsLoading,
|
|
4848
|
+
rotateOversizedCodexAppServerStartupBinding,
|
|
4849
|
+
resolveCodexAppServerSandboxPolicyForAutoBotSandbox,
|
|
4850
|
+
resolveCodexAppServerForAutoBotToolPolicy,
|
|
4851
|
+
resolveAutoBotCodingToolsSessionKeys,
|
|
4852
|
+
shouldEnableCodexAppServerNativeToolSurface,
|
|
4853
|
+
shouldForceMessageTool,
|
|
4854
|
+
buildCodexPluginThreadConfigEligibilityLogData,
|
|
4855
|
+
setAutoBotCodingToolsFactoryForTests(factory: AutoBotCodingToolsFactory): void {
|
|
4856
|
+
autoBotCodingToolsFactoryForTests = factory;
|
|
4857
|
+
},
|
|
4858
|
+
resetAutoBotCodingToolsFactoryForTests(): void {
|
|
4859
|
+
autoBotCodingToolsFactoryForTests = undefined;
|
|
4860
|
+
},
|
|
4861
|
+
} as const;
|
|
4862
|
+
export { testing as __testing };
|