@openclaw/codex 2026.6.11 → 2026.7.1-1

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.
Files changed (42) hide show
  1. package/dist/{app-server-policy-BPTiVNsW.js → app-server-policy-C968Kgin.js} +5 -5
  2. package/dist/{attempt-notifications-BYnndyFl.js → attempt-notifications-BGsEIIDI.js} +29 -40
  3. package/dist/{command-handlers-DEPTJYk0.js → command-handlers-Dv-pzAmg.js} +278 -237
  4. package/dist/command-rpc-eLM_mtC7.js +784 -0
  5. package/dist/compact-4mVBriKT.js +603 -0
  6. package/dist/{computer-use-ClrJwoFp.js → computer-use-Bmaz333N.js} +3 -3
  7. package/dist/{config-CszD0vP3.js → config-CYEDnLJ2.js} +44 -18
  8. package/dist/conversation-binding-data-BHRpYbX3.js +121 -0
  9. package/dist/doctor-contract-api.js +590 -22
  10. package/dist/harness.js +29 -10
  11. package/dist/index.js +1134 -35
  12. package/dist/{media-understanding-provider-ZUQHN7gT.js → media-understanding-provider-BHOu9-Gg.js} +13 -8
  13. package/dist/media-understanding-provider.js +1 -1
  14. package/dist/{models-CAmfBWRl.js → models-DDmO1zwd.js} +2 -2
  15. package/dist/notification-correlation-Bo7KB3ks.js +35 -0
  16. package/dist/{plugin-app-cache-key-C9ILt7bl.js → plugin-app-cache-key-BrhVdeEf.js} +13 -5
  17. package/dist/{protocol-dh-ETiNd.js → protocol-2POPqAY4.js} +2 -1
  18. package/dist/{protocol-validators-B19q5BIX.js → protocol-validators-dZQ-UTOa.js} +205 -56
  19. package/dist/provider-capabilities-DzI8fgEu.js +6172 -0
  20. package/dist/provider-catalog.js +4 -2
  21. package/dist/{provider-B-OHpbD3.js → provider-zjPfx5Fs.js} +94 -23
  22. package/dist/provider.js +2 -2
  23. package/dist/{request-Dcd452Nk.js → request-BcJyl8KL.js} +2 -2
  24. package/dist/{run-attempt-CXxtFC9d.js → run-attempt-DNSNk_G8.js} +1250 -2992
  25. package/dist/{sandbox-guard-3tnjhjFb.js → sandbox-guard-DA2TQfZW.js} +5 -3
  26. package/dist/session-binding-C1ZXdP-x.js +627 -0
  27. package/dist/session-binding-meta-B7aEMU7g.js +6 -0
  28. package/dist/shared-client-4ICy3U6d.js +1853 -0
  29. package/dist/{side-question-C-DhgJOd.js → side-question-CMOpEhvY.js} +134 -142
  30. package/dist/{thread-lifecycle-DtT3-ehU.js → thread-lifecycle-qWE88Dn2.js} +803 -481
  31. package/dist/{web-search-provider.runtime-CkwxrXQj.js → web-search-provider.runtime-BSlriav6.js} +3 -3
  32. package/npm-shrinkwrap.json +34 -34
  33. package/openclaw.plugin.json +23 -3
  34. package/package.json +9 -6
  35. package/dist/client-CLrtWgrD.js +0 -729
  36. package/dist/client-factory-Bm6HsGob.js +0 -18
  37. package/dist/compact-aQJ0ZKhe.js +0 -278
  38. package/dist/conversation-binding-t0XMLz-j.js +0 -1557
  39. package/dist/provider-capabilities-D7hnTO76.js +0 -3201
  40. package/dist/rate-limit-cache-C7qmZ0Jh.js +0 -26
  41. package/dist/session-binding-HOuPt-E0.js +0 -338
  42. package/dist/shared-client-Dtx5i7Ez.js +0 -1013
@@ -1,79 +1,23 @@
1
- import { n as isJsonObject, t as flattenCodexDynamicToolFunctions } from "./protocol-dh-ETiNd.js";
2
- import { i as codexSandboxPolicyForTurn, m as resolveCodexPluginsPolicy, t as CODEX_PLUGINS_MARKETPLACE_NAME } from "./config-CszD0vP3.js";
3
- import { n as assertCodexThreadResumeResponse, r as assertCodexThreadStartResponse } from "./protocol-validators-B19q5BIX.js";
1
+ import { n as flattenCodexDynamicToolFunctions, r as isJsonObject } from "./protocol-2POPqAY4.js";
2
+ import { h as resolveCodexPluginsPolicy, i as codexSandboxPolicyForTurn, t as CODEX_PLUGINS_MARKETPLACE_NAME } from "./config-CYEDnLJ2.js";
3
+ import { n as assertCodexThreadResumeResponse, r as assertCodexThreadStartResponse } from "./protocol-validators-dZQ-UTOa.js";
4
4
  import { CODEX_GPT5_HEARTBEAT_PROMPT_OVERLAY } from "./prompt-overlay.js";
5
- import { r as isModernCodexModel } from "./provider-B-OHpbD3.js";
6
- import { a as isCodexAppServerConnectionClosedError, n as CodexAppServerRpcError } from "./client-CLrtWgrD.js";
7
- import { i as isCodexAppServerNativeAuthProfile, l as writeCodexAppServerBinding, n as clearCodexAppServerBinding, o as readCodexAppServerBinding } from "./session-binding-HOuPt-E0.js";
5
+ import { a as readCodexSupportedReasoningEfforts, i as isModernCodexModel, o as resolveCodexFallbackReasoningEfforts, r as isMaxReasoningCodexModel, s as resolveCodexSupportedReasoningEffort } from "./provider-zjPfx5Fs.js";
6
+ import { f as isCodexAppServerConnectionClosedError, n as clearSharedCodexAppServerClientIfCurrentAndUnclaimed, s as retireSharedCodexAppServerClientIfCurrent, t as clearSharedCodexAppServerClientIfCurrent, u as CodexAppServerRpcError } from "./shared-client-4ICy3U6d.js";
7
+ import { a as normalizeCodexAppServerBindingModelProvider, i as isCodexAppServerNativeAuthProfile, o as reclaimCurrentCodexSessionGeneration, r as hashCodexAppServerBindingFingerprint, s as sessionBindingIdentity } from "./session-binding-C1ZXdP-x.js";
8
8
  import "node:fs/promises";
9
9
  import "node:path";
10
10
  import { isFutureDateTimestampMs, resolveDateTimestampMs, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
11
- import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
11
+ import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
12
12
  import crypto from "node:crypto";
13
+ import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
13
14
  import { SKILL_WORKSHOP_TOOL_NAME, buildSkillWorkshopPromptSection, embeddedAgentLog, formatErrorMessage, isActiveHarnessContextEngine } from "openclaw/plugin-sdk/agent-harness-runtime";
14
15
  import { buildCodexUserMcpServersThreadConfigPatch } from "openclaw/plugin-sdk/codex-mcp-projection";
15
16
  import { listRegisteredPluginAgentPromptGuidance } from "openclaw/plugin-sdk/plugin-runtime";
16
17
  import { redactSensitiveFieldValue, redactToolPayloadText } from "openclaw/plugin-sdk/logging-core";
17
18
  import { INLINE_IMAGE_DATA_URL_PREFIX, sanitizeInlineImageDataUrl } from "openclaw/plugin-sdk/inline-image-data-url-runtime";
18
19
  import { isDiagnosticFlagEnabled } from "openclaw/plugin-sdk/diagnostic-runtime";
19
- //#region extensions/codex/src/app-server/notification-correlation.ts
20
- /**
21
- * Correlates Codex app-server notifications with the active thread/turn so
22
- * projectors can ignore global or stale events without losing diagnostics.
23
- */
24
- /** Returns true when a notification payload belongs to the exact active thread and turn. */
25
- function isCodexNotificationForTurn(value, threadId, turnId) {
26
- if (!isJsonObject(value)) return false;
27
- return readCodexNotificationThreadId(value) === threadId && readCodexNotificationTurnId(value) === turnId;
28
- }
29
- /** Reads a thread id from either top-level notification params or nested turn payloads. */
30
- function readCodexNotificationThreadId(record) {
31
- return readNestedTurnThreadId(record) ?? readString(record, "threadId");
32
- }
33
- /** Reads a turn id from either top-level notification params or nested turn payloads. */
34
- function readCodexNotificationTurnId(record) {
35
- return readNestedTurnId(record) ?? readString(record, "turnId");
36
- }
37
- /** Builds structured correlation details for logs when notification routing is ambiguous. */
38
- function describeCodexNotificationCorrelation(notification, active) {
39
- const params = isJsonObject(notification.params) ? notification.params : void 0;
40
- const turn = params && isJsonObject(params.turn) ? params.turn : void 0;
41
- const threadId = params ? readString(params, "threadId") : void 0;
42
- const turnId = params ? readString(params, "turnId") : void 0;
43
- const nestedTurnThreadId = turn ? readString(turn, "threadId") : void 0;
44
- const nestedTurnId = turn ? readString(turn, "id") : void 0;
45
- const resolvedThreadId = params ? readCodexNotificationThreadId(params) : void 0;
46
- const resolvedTurnId = params ? readCodexNotificationTurnId(params) : void 0;
47
- const matchesActiveThread = resolvedThreadId === active.threadId;
48
- const matchesActiveTurn = active.turnId ? matchesActiveThread && resolvedTurnId === active.turnId : void 0;
49
- const items = turn?.items;
50
- return {
51
- method: notification.method,
52
- ...params ? { paramsKeys: Object.keys(params).toSorted() } : {},
53
- activeThreadId: active.threadId,
54
- ...active.turnId ? { activeTurnId: active.turnId } : {},
55
- ...threadId ? { threadId } : {},
56
- ...turnId ? { turnId } : {},
57
- ...nestedTurnThreadId ? { nestedTurnThreadId } : {},
58
- ...nestedTurnId ? { nestedTurnId } : {},
59
- ...turn ? { turnStatus: readString(turn, "status") } : {},
60
- ...Array.isArray(items) ? { turnItemCount: items.length } : {},
61
- matchesActiveThread,
62
- ...matchesActiveTurn === void 0 ? {} : { matchesActiveTurn }
63
- };
64
- }
65
- function readNestedTurnId(record) {
66
- const turn = record.turn;
67
- return isJsonObject(turn) ? readString(turn, "id") : void 0;
68
- }
69
- function readNestedTurnThreadId(record) {
70
- const turn = record.turn;
71
- return isJsonObject(turn) ? readString(turn, "threadId") : void 0;
72
- }
73
- function readString(record, key) {
74
- const value = record[key];
75
- return typeof value === "string" && value.trim() ? value.trim() : void 0;
76
- }
20
+ const CODEX_TARGETED_APP_INVENTORY_LIMIT = 1e3;
77
21
  const MAX_SERIALIZED_ERROR_MESSAGE_LENGTH = 500;
78
22
  /** In-memory app inventory cache with coalesced refreshes per key. */
79
23
  var CodexAppInventoryCache = class {
@@ -212,9 +156,11 @@ function serializeCodexAppInventoryError(error) {
212
156
  }
213
157
  /** Shared app inventory cache used by Codex app-server runtime paths. */
214
158
  const defaultCodexAppInventoryCache = new CodexAppInventoryCache();
215
- /** Builds a stable cache key from runtime identity fields. */
216
- function buildCodexAppInventoryCacheKey(input) {
159
+ /** Builds a stable cache key from build versions and runtime identity fields. */
160
+ function buildCodexAppInventoryCacheKey(input, openClawVersion, codexPluginVersion) {
217
161
  return JSON.stringify({
162
+ openClawVersion,
163
+ codexPluginVersion,
218
164
  codexHome: input.codexHome ?? null,
219
165
  endpoint: input.endpoint ?? null,
220
166
  runtimeIdentity: normalizeRuntimeIdentityForCacheKey(input.runtimeIdentity),
@@ -235,18 +181,21 @@ function normalizeRuntimeIdentityForCacheKey(value) {
235
181
  async function listAllApps(request, forceRefetch, targetAppIds = []) {
236
182
  const apps = [];
237
183
  const targetIds = new Set(targetAppIds.filter(Boolean));
238
- const foundTargetIds = /* @__PURE__ */ new Set();
184
+ const remainingTargetIds = new Set(targetIds);
185
+ const seenCursors = /* @__PURE__ */ new Set();
239
186
  let cursor;
240
187
  do {
241
188
  const response = await request("app/list", {
242
189
  cursor,
243
- limit: 100,
190
+ limit: targetIds.size > 0 ? CODEX_TARGETED_APP_INVENTORY_LIMIT : 100,
244
191
  forceRefetch
245
192
  });
246
193
  apps.push(...response.data);
247
- for (const app of response.data) if (targetIds.has(app.id)) foundTargetIds.add(app.id);
194
+ for (const app of response.data) remainingTargetIds.delete(app.id);
195
+ if (targetIds.size > 0 && remainingTargetIds.size === 0) break;
248
196
  cursor = response.nextCursor;
249
- if (targetIds.size > 0 && foundTargetIds.size === targetIds.size) break;
197
+ if (cursor && seenCursors.has(cursor)) throw new Error(`app/list returned repeated cursor ${cursor}`);
198
+ if (cursor) seenCursors.add(cursor);
250
199
  } while (cursor);
251
200
  return apps;
252
201
  }
@@ -269,7 +218,7 @@ function redactErrorData(value, depth = 0) {
269
218
  for (const [key, entry] of Object.entries(value)) redacted[key] = isSensitiveErrorDataKey(key) ? "<redacted>" : redactErrorData(entry, depth + 1) ?? null;
270
219
  return redacted;
271
220
  }
272
- if (typeof value === "string" && value.length > 500) return `${value.slice(0, 500)}...`;
221
+ if (typeof value === "string" && value.length > 500) return `${truncateUtf16Safe(value, 500)}...`;
273
222
  if (typeof value === "string") return value;
274
223
  if (typeof value === "bigint") return value.toString();
275
224
  if (typeof value === "symbol") return value.description ? `Symbol(${value.description})` : "Symbol()";
@@ -290,6 +239,7 @@ function isSensitiveErrorDataKey(key) {
290
239
  * Reads Codex plugin marketplace state and app inventory to decide which
291
240
  * plugin-owned apps can be exposed to a native Codex thread.
292
241
  */
242
+ const CODEX_PLUGINS_REMOTE_MARKETPLACE_NAME = `${CODEX_PLUGINS_MARKETPLACE_NAME}-remote`;
293
243
  /** Reads configured Codex plugin state and maps owned apps to readiness diagnostics. */
294
244
  async function readCodexPluginInventory(params) {
295
245
  const policy = params.policy ?? resolveCodexPluginsPolicy(params.pluginConfig);
@@ -302,7 +252,8 @@ async function readCodexPluginInventory(params) {
302
252
  }]
303
253
  };
304
254
  const appInventory = readCachedAppInventory(params);
305
- const marketplaceEntry = (await params.request("plugin/list", { cwds: [] })).marketplaces.find((marketplace) => marketplace.name === CODEX_PLUGINS_MARKETPLACE_NAME);
255
+ const listed = await params.request("plugin/list", { cwds: [] });
256
+ const marketplaceEntry = listed.marketplaces.find(isOpenAiCuratedMarketplace);
306
257
  if (!marketplaceEntry) return {
307
258
  policy,
308
259
  records: [],
@@ -313,7 +264,7 @@ async function readCodexPluginInventory(params) {
313
264
  })),
314
265
  ...appInventory ? { appInventory } : {}
315
266
  };
316
- const marketplace = marketplaceRef(marketplaceEntry);
267
+ let marketplace = marketplaceRef(marketplaceEntry);
317
268
  const diagnostics = [];
318
269
  const records = [];
319
270
  if (appInventory?.state === "missing") diagnostics.push({
@@ -326,8 +277,8 @@ async function readCodexPluginInventory(params) {
326
277
  });
327
278
  for (const pluginPolicy of policy.pluginPolicies) {
328
279
  if (!pluginPolicy.enabled) continue;
329
- const summary = findPluginSummary(marketplaceEntry, pluginPolicy.pluginName);
330
- if (!summary) {
280
+ const resolvedPlugin = findOpenAiCuratedMarketplacePlugin(listed, pluginPolicy.pluginName);
281
+ if (!resolvedPlugin) {
331
282
  diagnostics.push({
332
283
  code: "plugin_missing",
333
284
  plugin: pluginPolicy,
@@ -335,7 +286,10 @@ async function readCodexPluginInventory(params) {
335
286
  });
336
287
  continue;
337
288
  }
338
- const detail = await readPluginDetail(params, marketplace, pluginPolicy, diagnostics);
289
+ const { summary } = resolvedPlugin;
290
+ const pluginMarketplace = marketplaceRef(resolvedPlugin.marketplace);
291
+ if (records.length === 0) marketplace = pluginMarketplace;
292
+ const detail = await readPluginDetail(params, pluginMarketplace, pluginPolicy, summary, diagnostics);
339
293
  const ownedAppIds = detail?.apps.map((app) => app.id).filter(Boolean).toSorted() ?? [];
340
294
  const appOwnership = resolveAppOwnership({
341
295
  detail,
@@ -378,12 +332,10 @@ async function readCodexPluginInventory(params) {
378
332
  }
379
333
  /** Finds one plugin summary in the OpenAI curated marketplace response. */
380
334
  function findOpenAiCuratedPluginSummary(listed, pluginName) {
381
- const marketplaceEntry = listed.marketplaces.find((marketplace) => marketplace.name === CODEX_PLUGINS_MARKETPLACE_NAME);
382
- if (!marketplaceEntry) return;
383
- const summary = findPluginSummary(marketplaceEntry, pluginName);
384
- return summary ? {
385
- marketplace: marketplaceRef(marketplaceEntry),
386
- summary
335
+ const resolved = findOpenAiCuratedMarketplacePlugin(listed, pluginName);
336
+ return resolved ? {
337
+ marketplace: marketplaceRef(resolved.marketplace),
338
+ summary: resolved.summary
387
339
  } : void 0;
388
340
  }
389
341
  /** Builds plugin/read or plugin/install params from a marketplace reference. */
@@ -404,10 +356,10 @@ function readCachedAppInventory(params) {
404
356
  suppressRefresh: params.suppressAppInventoryRefresh
405
357
  });
406
358
  }
407
- async function readPluginDetail(params, marketplace, pluginPolicy, diagnostics) {
359
+ async function readPluginDetail(params, marketplace, pluginPolicy, summary, diagnostics) {
408
360
  if (params.readPluginDetails === false) return;
409
361
  try {
410
- return (await params.request("plugin/read", pluginReadParams(marketplace, pluginPolicy.pluginName))).plugin;
362
+ return (await params.request("plugin/read", pluginReadParams(marketplace, marketplace.remoteMarketplaceName && summary.remotePluginId ? summary.remotePluginId : pluginPolicy.pluginName))).plugin;
411
363
  } catch (error) {
412
364
  diagnostics.push({
413
365
  code: "plugin_detail_unavailable",
@@ -454,6 +406,16 @@ function resolveOwnedApps(params) {
454
406
  function findPluginSummary(marketplace, pluginName) {
455
407
  return marketplace.plugins.find((plugin) => plugin.name === pluginName || plugin.id === pluginName || plugin.id === `${pluginName}@${marketplace.name}` || pluginNameFromPluginId(plugin.id, marketplace.name) === pluginName);
456
408
  }
409
+ function findOpenAiCuratedMarketplacePlugin(listed, pluginName) {
410
+ for (const marketplace of listed.marketplaces) {
411
+ if (!isOpenAiCuratedMarketplace(marketplace)) continue;
412
+ const summary = findPluginSummary(marketplace, pluginName);
413
+ if (summary) return {
414
+ marketplace,
415
+ summary
416
+ };
417
+ }
418
+ }
457
419
  function pluginNameFromPluginId(pluginId, marketplaceName) {
458
420
  const trimmed = pluginId.trim();
459
421
  if (!trimmed) return;
@@ -467,6 +429,9 @@ function marketplaceRef(marketplace) {
467
429
  ...!marketplace.path ? { remoteMarketplaceName: marketplace.name } : {}
468
430
  };
469
431
  }
432
+ function isOpenAiCuratedMarketplace(marketplace) {
433
+ return marketplace.name === "openai-curated" || marketplace.name === CODEX_PLUGINS_REMOTE_MARKETPLACE_NAME;
434
+ }
470
435
  //#endregion
471
436
  //#region extensions/codex/src/app-server/plugin-activation.ts
472
437
  /** Installs/enables a configured Codex plugin and refreshes plugin/app state. */
@@ -486,7 +451,7 @@ async function ensureCodexPluginActivation(params) {
486
451
  marketplace: resolved.marketplace,
487
452
  diagnostics: []
488
453
  };
489
- const installResponse = await params.request("plugin/install", pluginReadParams(resolved.marketplace, params.identity.pluginName));
454
+ const installResponse = await params.request("plugin/install", pluginReadParams(resolved.marketplace, resolved.marketplace.remoteMarketplaceName && resolved.summary.remotePluginId ? resolved.summary.remotePluginId : params.identity.pluginName));
490
455
  const refreshDiagnostics = [];
491
456
  let refreshFailed = false;
492
457
  try {
@@ -554,20 +519,21 @@ function activationFailure(identity, reason, diagnostic, extraDiagnostics = [])
554
519
  //#endregion
555
520
  //#region extensions/codex/src/app-server/plugin-thread-config.ts
556
521
  /**
557
- * Builds Codex thread config patches that expose only policy-approved
558
- * plugin-owned apps for native Codex turns.
522
+ * Builds Codex thread config patches that expose only policy-approved apps
523
+ * for native Codex turns.
559
524
  */
560
- const CODEX_PLUGIN_THREAD_CONFIG_INPUT_FINGERPRINT_VERSION = 2;
561
- const CODEX_PLUGIN_THREAD_CONFIG_FINGERPRINT_VERSION = 1;
525
+ const CODEX_PLUGIN_THREAD_CONFIG_INPUT_FINGERPRINT_VERSION = 3;
526
+ const CODEX_PLUGIN_THREAD_CONFIG_FINGERPRINT_VERSION = 2;
562
527
  /** Returns true when plugin config exists and thread config may need app patches. */
563
528
  function shouldBuildCodexPluginThreadConfig(pluginConfig) {
564
529
  return resolveCodexPluginsPolicy(pluginConfig).configured;
565
530
  }
566
531
  /** Fingerprints policy and app-cache identity before runtime inventory is read. */
567
532
  function buildCodexPluginThreadConfigInputFingerprint(params) {
533
+ const policy = resolveCodexPluginsPolicy(params.pluginConfig);
568
534
  return fingerprintJson({
569
535
  version: CODEX_PLUGIN_THREAD_CONFIG_INPUT_FINGERPRINT_VERSION,
570
- policy: policyFingerprint(resolveCodexPluginsPolicy(params.pluginConfig)),
536
+ policy: policyFingerprint(policy),
571
537
  appCacheKey: params.appCacheKey ?? null
572
538
  });
573
539
  }
@@ -584,7 +550,7 @@ async function buildCodexPluginThreadConfig(params) {
584
550
  inputFingerprint,
585
551
  configPatch: buildDisabledAppsConfigPatch()
586
552
  });
587
- let inventory = await readCodexPluginInventory({
553
+ let inventory = policy.pluginPolicies.length > 0 ? await readCodexPluginInventory({
588
554
  pluginConfig: params.pluginConfig,
589
555
  policy,
590
556
  request: params.request,
@@ -592,10 +558,11 @@ async function buildCodexPluginThreadConfig(params) {
592
558
  appCacheKey: params.appCacheKey,
593
559
  nowMs: params.nowMs,
594
560
  suppressAppInventoryRefresh: true
595
- });
561
+ }) : emptyCodexPluginInventory(policy);
562
+ const appInventoryRefreshDeferredForActivation = inventory.records.some((record) => record.activationRequired) && shouldRefreshMissingAppInventory(params, policy, inventory);
596
563
  if (shouldWaitForInitialAppInventory(params, policy, inventory)) {
597
564
  await refreshAppInventoryNow(params, appCache, {
598
- forceRefetch: true,
565
+ forceRefetch: false,
599
566
  reason: "initial_missing",
600
567
  targetAppIds: collectInventoryOwnedAppIds(inventory)
601
568
  });
@@ -630,10 +597,12 @@ async function buildCodexPluginThreadConfig(params) {
630
597
  message: activation.diagnostics.map((item) => item.message).join(" ") || activation.reason
631
598
  });
632
599
  }
633
- if (activationResults.some((activation) => activation.ok && activation.installAttempted)) {
600
+ const postInstallRefreshRequired = activationResults.some((activation) => activation.ok && activation.installAttempted);
601
+ const deferredMissingRefreshRequired = appInventoryRefreshDeferredForActivation && !postInstallRefreshRequired && shouldRefreshMissingAppInventory(params, policy, inventory);
602
+ if (postInstallRefreshRequired || deferredMissingRefreshRequired) {
634
603
  await refreshAppInventoryNow(params, appCache, {
635
604
  forceRefetch: true,
636
- reason: "post_install",
605
+ reason: postInstallRefreshRequired ? "post_install" : "deferred_missing",
637
606
  targetAppIds: collectInventoryOwnedAppIds(inventory)
638
607
  });
639
608
  inventory = await readCodexPluginInventory({
@@ -668,7 +637,12 @@ async function buildCodexPluginThreadConfig(params) {
668
637
  appCacheKey: params.appCacheKey
669
638
  });
670
639
  }
671
- const diagnostics = [...inventory.diagnostics, ...activationDiagnostics];
640
+ const accountAppsResult = policy.allowAllPlugins ? await readAccessibleAccountApps(params, appCache) : { apps: [] };
641
+ const diagnostics = [
642
+ ...inventory.diagnostics,
643
+ ...activationDiagnostics,
644
+ ...accountAppsResult.diagnostic ? [accountAppsResult.diagnostic] : []
645
+ ];
672
646
  const apps = { _default: {
673
647
  enabled: false,
674
648
  destructive_enabled: false,
@@ -676,31 +650,32 @@ async function buildCodexPluginThreadConfig(params) {
676
650
  } };
677
651
  const policyApps = {};
678
652
  const pluginAppIds = {};
653
+ const pluginOwnedAppIds = new Set(inventory.records.flatMap((record) => record.appOwnership === "proven" ? record.ownedAppIds : []));
679
654
  for (const record of inventory.records) {
680
- if (record.activationRequired) {
681
- if (!activationResults.find((item) => item.identity.configKey === record.policy.configKey)?.ok) continue;
682
- }
655
+ const activation = activationResults.find((item) => item.identity.configKey === record.policy.configKey);
656
+ if (activation?.ok === false || record.activationRequired && !activation?.ok) continue;
683
657
  if (record.appOwnership !== "proven") continue;
684
658
  pluginAppIds[record.policy.configKey] = [...record.ownedAppIds].toSorted();
685
659
  for (const app of resolveThreadConfigAppsForRecord({
686
660
  record,
687
661
  inventory
688
662
  })) {
689
- if (!app.accessible || !app.enabled) {
663
+ if (!isPluginAppReadyForThreadStart(app)) {
690
664
  diagnostics.push({
691
665
  code: "app_not_ready",
692
666
  plugin: record.policy,
693
- message: `${app.id} is not accessible or enabled for ${record.policy.pluginName}.`
667
+ message: `${app.id} is not accessible for ${record.policy.pluginName}.`
694
668
  });
695
669
  continue;
696
670
  }
697
- const appConfig = {
698
- enabled: true,
699
- destructive_enabled: record.policy.allowDestructiveActions,
700
- open_world_enabled: true,
701
- default_tools_approval_mode: "auto"
702
- };
703
- apps[app.id] = appConfig;
671
+ if (record.policy.destructiveApprovalMode === "ask" && !await clearPersistedAppToolApprovalOverrides({
672
+ request: params.request,
673
+ configCwd: params.configCwd,
674
+ plugin: record.policy,
675
+ app,
676
+ diagnostics
677
+ })) continue;
678
+ apps[app.id] = buildEnabledAppConfig(record.policy);
704
679
  policyApps[app.id] = {
705
680
  configKey: record.policy.configKey,
706
681
  marketplaceName: record.policy.marketplaceName,
@@ -711,6 +686,24 @@ async function buildCodexPluginThreadConfig(params) {
711
686
  };
712
687
  }
713
688
  }
689
+ for (const app of accountAppsResult.apps) {
690
+ if (pluginOwnedAppIds.has(app.id)) continue;
691
+ const accountApp = toOwnedAccountApp(app);
692
+ if (policy.destructiveApprovalMode === "ask" && !await clearPersistedAppToolApprovalOverrides({
693
+ request: params.request,
694
+ configCwd: params.configCwd,
695
+ app: accountApp,
696
+ diagnostics
697
+ })) continue;
698
+ apps[app.id] = buildEnabledAppConfig(policy);
699
+ policyApps[app.id] = {
700
+ source: "account",
701
+ appName: app.name,
702
+ allowDestructiveActions: policy.allowDestructiveActions,
703
+ destructiveApprovalMode: policy.destructiveApprovalMode,
704
+ mcpServerNames: []
705
+ };
706
+ }
714
707
  const configPatch = { apps };
715
708
  const policyContext = buildPluginAppPolicyContext(policyApps, pluginAppIds);
716
709
  return {
@@ -766,10 +759,35 @@ function buildDisabledAppsConfigPatch() {
766
759
  open_world_enabled: false
767
760
  } } };
768
761
  }
762
+ function buildEnabledAppConfig(policy) {
763
+ return {
764
+ enabled: true,
765
+ destructive_enabled: policy.allowDestructiveActions,
766
+ open_world_enabled: true,
767
+ default_tools_approval_mode: "auto",
768
+ ...policy.destructiveApprovalMode === "ask" ? { approvals_reviewer: "user" } : {}
769
+ };
770
+ }
771
+ /** Rebuilds the safe per-thread apps patch persisted with a Codex thread binding. */
772
+ function buildCodexPluginAppsConfigPatchFromPolicyContext(policyContext) {
773
+ const apps = { _default: {
774
+ enabled: false,
775
+ destructive_enabled: false,
776
+ open_world_enabled: false
777
+ } };
778
+ for (const [appId, policy] of Object.entries(policyContext.apps).toSorted(([left], [right]) => left.localeCompare(right))) apps[appId] = {
779
+ enabled: true,
780
+ destructive_enabled: policy.allowDestructiveActions,
781
+ open_world_enabled: true,
782
+ default_tools_approval_mode: "auto",
783
+ ...policy.destructiveApprovalMode === "ask" ? { approvals_reviewer: "user" } : {}
784
+ };
785
+ return { apps };
786
+ }
769
787
  function buildPluginAppPolicyContext(apps, pluginAppIds) {
770
788
  return {
771
789
  fingerprint: fingerprintJson({
772
- version: 1,
790
+ version: 2,
773
791
  apps,
774
792
  pluginAppIds
775
793
  }),
@@ -777,8 +795,53 @@ function buildPluginAppPolicyContext(apps, pluginAppIds) {
777
795
  pluginAppIds
778
796
  };
779
797
  }
798
+ async function clearPersistedAppToolApprovalOverrides(params) {
799
+ try {
800
+ const overrideNames = await readPersistedAppToolApprovalOverrideNames(params);
801
+ for (const toolName of overrideNames) if (isOverriddenConfigWriteResponse(await params.request("config/value/write", {
802
+ keyPath: `apps.${quoteConfigKeyPathSegment(params.app.id)}.tools.${quoteConfigKeyPathSegment(toolName)}.approval_mode`,
803
+ value: null,
804
+ mergeStrategy: "replace"
805
+ }))) throw new Error(`approval override for ${toolName} is controlled by another config layer`);
806
+ const remainingOverrideNames = await readPersistedAppToolApprovalOverrideNames(params);
807
+ if (remainingOverrideNames.length > 0) throw new Error(`effective approval overrides remain for ${remainingOverrideNames.join(", ")}`);
808
+ return true;
809
+ } catch (error) {
810
+ params.diagnostics.push({
811
+ code: "approval_overrides_clear_failed",
812
+ ...params.plugin ? { plugin: params.plugin } : {},
813
+ message: `Could not clear durable Codex app approval overrides for ${params.app.id}: ${error instanceof Error ? error.message : String(error)}`
814
+ });
815
+ return false;
816
+ }
817
+ }
818
+ async function readPersistedAppToolApprovalOverrideNames(params) {
819
+ const response = await params.request("config/read", {
820
+ includeLayers: false,
821
+ ...params.configCwd ? { cwd: params.configCwd } : {}
822
+ });
823
+ const config = isJsonObject(response) ? response.config : void 0;
824
+ const appsRoot = isJsonObject(config) ? config.apps : void 0;
825
+ const nestedApps = isJsonObject(appsRoot) ? appsRoot.apps : void 0;
826
+ const appConfig = isJsonObject(appsRoot) ? appsRoot[params.app.id] ?? (isJsonObject(nestedApps) ? nestedApps[params.app.id] : void 0) : void 0;
827
+ const tools = isJsonObject(appConfig) ? appConfig.tools : void 0;
828
+ if (!isJsonObject(tools)) return [];
829
+ return Object.entries(tools).filter(([, value]) => hasPersistedToolApprovalOverride(value)).map(([toolName]) => toolName).toSorted();
830
+ }
831
+ function hasPersistedToolApprovalOverride(value) {
832
+ return isJsonObject(value) && (value.approval_mode !== void 0 || value.approvalMode !== void 0);
833
+ }
834
+ function isOverriddenConfigWriteResponse(response) {
835
+ return isJsonObject(response) && response.status === "okOverridden";
836
+ }
837
+ function quoteConfigKeyPathSegment(segment) {
838
+ return `"${segment.replace(/["\\]/g, (char) => `\\${char}`)}"`;
839
+ }
780
840
  function shouldWaitForInitialAppInventory(params, policy, inventory) {
781
841
  if (inventory.records.some((record) => record.activationRequired)) return false;
842
+ return shouldRefreshMissingAppInventory(params, policy, inventory);
843
+ }
844
+ function shouldRefreshMissingAppInventory(params, policy, inventory) {
782
845
  return Boolean(params.appCacheKey && policy.pluginPolicies.some((plugin) => plugin.enabled) && inventory.appInventory?.state === "missing");
783
846
  }
784
847
  async function refreshAppInventoryNow(params, appCache, options = {}) {
@@ -805,18 +868,53 @@ async function refreshAppInventoryNow(params, appCache, options = {}) {
805
868
  function collectInventoryOwnedAppIds(inventory) {
806
869
  return Array.from(new Set(inventory.records.flatMap((record) => record.ownedAppIds).filter(Boolean))).toSorted();
807
870
  }
871
+ function emptyCodexPluginInventory(policy) {
872
+ return {
873
+ policy,
874
+ records: [],
875
+ diagnostics: []
876
+ };
877
+ }
878
+ async function readAccessibleAccountApps(params, appCache) {
879
+ const snapshot = await refreshAppInventoryNow(params, appCache, {
880
+ forceRefetch: false,
881
+ reason: "account_apps_all",
882
+ targetAppIds: []
883
+ });
884
+ if (!snapshot) return {
885
+ apps: [],
886
+ diagnostic: {
887
+ code: "account_app_inventory_unavailable",
888
+ message: "Codex account app inventory was unavailable; account apps were not exposed."
889
+ }
890
+ };
891
+ return { apps: snapshot.apps.filter((app) => app.isAccessible).toSorted((left, right) => left.id.localeCompare(right.id)) };
892
+ }
893
+ function toOwnedAccountApp(app) {
894
+ return {
895
+ id: app.id,
896
+ name: app.name,
897
+ accessible: app.isAccessible,
898
+ enabled: app.isEnabled,
899
+ needsAuth: !app.isAccessible
900
+ };
901
+ }
808
902
  function resolveThreadConfigAppsForRecord(params) {
809
903
  if (params.inventory.appInventory?.state === "missing") return [];
810
904
  return params.record.apps;
811
905
  }
906
+ function isPluginAppReadyForThreadStart(app) {
907
+ return app.accessible;
908
+ }
812
909
  function shouldForceRefreshForNotReadyPluginApps(params, policy, inventory) {
813
910
  if (!params.appCacheKey || !policy.pluginPolicies.some((plugin) => plugin.enabled)) return false;
814
911
  if (inventory.appInventory?.state === "missing") return false;
815
- return inventory.records.some((record) => record.appOwnership === "proven" && record.ownedAppIds.length > 0 && (record.apps.length === 0 || record.apps.some((app) => !app.accessible || !app.enabled)));
912
+ return inventory.records.some((record) => record.appOwnership === "proven" && record.ownedAppIds.length > 0 && (record.apps.length === 0 || record.apps.some((app) => !app.accessible)));
816
913
  }
817
914
  function policyFingerprint(policy) {
818
915
  return {
819
916
  enabled: policy.enabled,
917
+ allowAllPlugins: policy.allowAllPlugins,
820
918
  allowDestructiveActions: policy.allowDestructiveActions,
821
919
  destructiveApprovalMode: policy.destructiveApprovalMode,
822
920
  plugins: policy.pluginPolicies.map((plugin) => ({
@@ -849,6 +947,127 @@ function stableStringify(value) {
849
947
  return JSON.stringify(value);
850
948
  }
851
949
  //#endregion
950
+ //#region extensions/codex/src/app-server/attempt-client-cleanup.ts
951
+ /**
952
+ * Best-effort cleanup helpers for Codex app-server startup attempts and turns.
953
+ */
954
+ /** Timeout for best-effort app-server turn interruption during cleanup. */
955
+ const CODEX_APP_SERVER_INTERRUPT_TIMEOUT_MS = 5e3;
956
+ /** Timeout for best-effort thread unsubscribe during cleanup. */
957
+ const CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS = 5e3;
958
+ /** Raised when a thread subscription may be live on a client OpenClaw no longer controls. */
959
+ var CodexAppServerUnsafeSubscriptionError = class extends Error {
960
+ constructor(message, options) {
961
+ super(message, options);
962
+ this.name = "CodexAppServerUnsafeSubscriptionError";
963
+ }
964
+ };
965
+ function isCodexAppServerUnsafeSubscriptionError(error) {
966
+ return error instanceof CodexAppServerUnsafeSubscriptionError;
967
+ }
968
+ /** Asserts Codex resumed the exact thread this attempt subscribed to. */
969
+ function assertCodexThreadResumeSubscription(requestedThreadId, returnedThreadId) {
970
+ if (returnedThreadId !== requestedThreadId) throw new CodexAppServerUnsafeSubscriptionError(`Codex thread/resume returned ${returnedThreadId} for ${requestedThreadId}`);
971
+ }
972
+ async function closeClientAndWaitIfAvailable(client) {
973
+ const closeable = client;
974
+ if (typeof closeable.closeAndWait === "function") {
975
+ await closeable.closeAndWait();
976
+ return;
977
+ }
978
+ closeable.close?.();
979
+ }
980
+ async function closeCodexStartupClientBestEffort(client) {
981
+ if (!client) return;
982
+ const unclaimedSharedClient = clearSharedCodexAppServerClientIfCurrentAndUnclaimed(client);
983
+ if (unclaimedSharedClient.closed) {
984
+ await closeClientAndWaitIfAvailable(client);
985
+ return;
986
+ }
987
+ if (unclaimedSharedClient.found) {
988
+ if (retireSharedCodexAppServerClientIfCurrent(client)?.closed) await closeClientAndWaitIfAvailable(client);
989
+ return;
990
+ }
991
+ const retiredSharedClient = retireSharedCodexAppServerClientIfCurrent(client);
992
+ if (retiredSharedClient) {
993
+ if (retiredSharedClient.closed) await closeClientAndWaitIfAvailable(client);
994
+ return;
995
+ }
996
+ if (clearSharedCodexAppServerClientIfCurrent(client)) {
997
+ await closeClientAndWaitIfAvailable(client);
998
+ return;
999
+ }
1000
+ await closeClientAndWaitIfAvailable(client);
1001
+ }
1002
+ /** Sends a turn interrupt without blocking abort cleanup on app-server errors. */
1003
+ function interruptCodexTurnBestEffort(client, params) {
1004
+ const requestOptions = params.timeoutMs && Number.isFinite(params.timeoutMs) && params.timeoutMs > 0 ? { timeoutMs: params.timeoutMs } : void 0;
1005
+ const requestParams = {
1006
+ threadId: params.threadId,
1007
+ turnId: params.turnId
1008
+ };
1009
+ try {
1010
+ const interrupt = requestOptions ? client.request("turn/interrupt", requestParams, requestOptions) : client.request("turn/interrupt", requestParams);
1011
+ Promise.resolve(interrupt).catch((error) => {
1012
+ embeddedAgentLog.debug("codex app-server turn interrupt failed during abort", { error });
1013
+ });
1014
+ } catch (error) {
1015
+ embeddedAgentLog.debug("codex app-server turn interrupt failed during abort", { error });
1016
+ }
1017
+ }
1018
+ /** Unsubscribes from a thread while swallowing cleanup-only failures. */
1019
+ async function unsubscribeCodexThreadBestEffort(client, params) {
1020
+ try {
1021
+ await client.request("thread/unsubscribe", { threadId: params.threadId }, { timeoutMs: params.timeoutMs });
1022
+ return true;
1023
+ } catch (error) {
1024
+ embeddedAgentLog.debug("codex app-server thread unsubscribe cleanup failed", {
1025
+ threadId: params.threadId,
1026
+ error
1027
+ });
1028
+ return false;
1029
+ }
1030
+ }
1031
+ /**
1032
+ * Retires the shared client after a timed-out turn so later runs do not reuse a
1033
+ * potentially wedged app-server connection.
1034
+ */
1035
+ async function retireCodexAppServerClientAfterTimedOutTurn(client, params) {
1036
+ const retiredSharedClient = retireSharedCodexAppServerClientIfCurrent(client);
1037
+ const detachedSharedClient = Boolean(retiredSharedClient);
1038
+ interruptCodexTurnBestEffort(client, {
1039
+ threadId: params.threadId,
1040
+ turnId: params.turnId,
1041
+ timeoutMs: CODEX_APP_SERVER_INTERRUPT_TIMEOUT_MS
1042
+ });
1043
+ await unsubscribeCodexThreadBestEffort(client, {
1044
+ threadId: params.threadId,
1045
+ timeoutMs: CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS
1046
+ });
1047
+ let closedClient = retiredSharedClient?.closed ?? false;
1048
+ if (!detachedSharedClient) {
1049
+ const close = client.close;
1050
+ if (typeof close === "function") try {
1051
+ close.call(client);
1052
+ closedClient = true;
1053
+ } catch (error) {
1054
+ embeddedAgentLog.debug("codex app-server client close failed during timeout cleanup", {
1055
+ threadId: params.threadId,
1056
+ turnId: params.turnId,
1057
+ error
1058
+ });
1059
+ }
1060
+ }
1061
+ embeddedAgentLog.warn("codex app-server client retired after timed-out turn", {
1062
+ threadId: params.threadId,
1063
+ turnId: params.turnId,
1064
+ reason: params.reason,
1065
+ detachedSharedClient,
1066
+ closedClient,
1067
+ activeSharedClientLeases: retiredSharedClient?.activeLeases ?? 0
1068
+ });
1069
+ }
1070
+ //#endregion
852
1071
  //#region extensions/codex/src/app-server/context-engine-projection.ts
853
1072
  const CONTEXT_HEADER = "OpenClaw assembled context for this turn:";
854
1073
  const CONTEXT_OPEN = "<conversation_context>";
@@ -1015,7 +1234,7 @@ function renderToolResultPayload(record) {
1015
1234
  }
1016
1235
  return payload;
1017
1236
  }
1018
- const TOOL_PAYLOAD_METADATA_KEYS = new Set([
1237
+ const TOOL_PAYLOAD_METADATA_KEYS = /* @__PURE__ */ new Set([
1019
1238
  "type",
1020
1239
  "name",
1021
1240
  "id",
@@ -1249,6 +1468,31 @@ function isCodexAppServerProfilerEnabled(config, env = process.env) {
1249
1468
  return PROFILER_FLAGS.some((flag) => isDiagnosticFlagEnabled(flag, config, env));
1250
1469
  }
1251
1470
  //#endregion
1471
+ //#region extensions/codex/src/app-server/thread-resume.ts
1472
+ /** Owns Codex thread/resume subscription safety. */
1473
+ /** Resumes one thread and retires the physical client when acceptance is indeterminate. */
1474
+ async function resumeCodexAppServerThread(params) {
1475
+ const threadId = params.request.threadId;
1476
+ let response;
1477
+ try {
1478
+ response = assertCodexThreadResumeResponse(await params.client.request("thread/resume", params.request, {
1479
+ ...params.timeoutMs !== void 0 ? { timeoutMs: params.timeoutMs } : {},
1480
+ ...params.signal ? { signal: params.signal } : {}
1481
+ }));
1482
+ assertCodexThreadResumeSubscription(threadId, response.thread.id);
1483
+ } catch (error) {
1484
+ if (error instanceof CodexAppServerRpcError) throw error;
1485
+ try {
1486
+ await params.abandonClient();
1487
+ } catch (abandonError) {
1488
+ throw new CodexAppServerUnsafeSubscriptionError(`Codex thread/resume client could not be retired for ${threadId}`, { cause: abandonError });
1489
+ }
1490
+ if (error instanceof CodexAppServerUnsafeSubscriptionError) throw error;
1491
+ throw new CodexAppServerUnsafeSubscriptionError(error instanceof Error ? error.message : `Codex thread/resume outcome is indeterminate for ${threadId}`, { cause: error });
1492
+ }
1493
+ return response;
1494
+ }
1495
+ //#endregion
1252
1496
  //#region extensions/codex/src/app-server/web-search.ts
1253
1497
  const CODEX_NATIVE_WEB_SEARCH_DISABLED_CONFIG = {
1254
1498
  "features.standalone_web_search": false,
@@ -1323,6 +1567,12 @@ var CodexThreadStartRequestError = class extends Error {
1323
1567
  this.name = "CodexThreadStartRequestError";
1324
1568
  }
1325
1569
  };
1570
+ var CodexThreadBindingConflictError = class extends Error {
1571
+ constructor(threadId, operation) {
1572
+ super(`Codex thread binding changed while ${operation}: ${threadId}`);
1573
+ this.name = "CodexThreadBindingConflictError";
1574
+ }
1575
+ };
1326
1576
  const CODEX_NATIVE_PERSONALITY_NONE = "none";
1327
1577
  const CODEX_CODE_MODE_THREAD_CONFIG = {
1328
1578
  "features.code_mode": true,
@@ -1423,227 +1673,383 @@ function createCodexThreadLifecycleTimingTracker(options = {}) {
1423
1673
  };
1424
1674
  }
1425
1675
  async function startOrResumeThread(params) {
1426
- const lifecycleTiming = createCodexThreadLifecycleTimingTracker({
1427
- ...params.timing,
1428
- enabled: params.timing?.enabled ?? isCodexAppServerProfilerEnabled(params.params.config)
1429
- });
1430
- const dynamicToolsFingerprint = lifecycleTiming.measureSync("dynamic-tools-fingerprint", () => fingerprintDynamicTools(params.dynamicTools));
1431
- const dynamicToolsContainDeferred = flattenCodexDynamicToolFunctions(params.dynamicTools).some((tool) => tool.deferLoading === true);
1432
- const webSearchThreadConfigFingerprint = fingerprintJsonObject(lifecycleTiming.measureSync("web-search-plan", () => resolveCodexWebSearchPlan({
1433
- config: params.params.config,
1434
- disableTools: params.params.disableTools,
1435
- nativeToolSurfaceEnabled: params.nativeCodeModeEnabled,
1436
- nativeProviderWebSearchSupport: params.nativeProviderWebSearchSupport,
1437
- webSearchAllowed: params.webSearchAllowed
1438
- })).threadConfig);
1439
- const networkProxyConfigFingerprint = params.appServer.networkProxy?.configFingerprint;
1440
- const contextEngineBinding = lifecycleTiming.measureSync("context-engine-binding", () => buildContextEngineBinding(params.params, params.contextEngineProjection));
1441
- const userMcpServersConfigPatch = params.userMcpServersEnabled === false ? void 0 : buildCodexUserMcpServersThreadConfigPatch(params.params.config, { agentId: params.agentId ?? params.params.agentId });
1442
- const userMcpServersFingerprint = fingerprintUserMcpServersConfigPatch(userMcpServersConfigPatch);
1443
- const environmentSelectionFingerprint = fingerprintEnvironmentSelection(params.environmentSelection);
1444
- let binding = await lifecycleTiming.measure("read-binding", () => readCodexAppServerBinding(params.params.sessionFile, {
1445
- authProfileStore: params.params.authProfileStore,
1446
- agentDir: params.params.agentDir,
1447
- config: params.params.config
1448
- }));
1449
- if (binding?.threadId && shouldRotateCodexAppServerBindingForRuntime({
1450
- connectionClass: params.appServer.connectionClass,
1451
- current: params.appServerRuntimeFingerprint,
1452
- binding: binding.appServerRuntimeFingerprint
1453
- })) {
1454
- embeddedAgentLog.debug("codex app-server runtime identity changed; starting a new thread", {
1455
- threadId: binding.threadId,
1456
- connectionClass: params.appServer.connectionClass
1457
- });
1458
- await clearCodexAppServerBinding(params.params.sessionFile);
1459
- binding = void 0;
1460
- }
1461
- const startModelSelection = resolveCodexAppServerThreadModelSelection({
1462
- provider: params.params.provider,
1463
- model: params.params.modelId,
1464
- binding,
1465
- authProfileId: params.params.authProfileId,
1466
- authProfileStore: params.params.authProfileStore,
1467
- agentDir: params.params.agentDir,
1676
+ const bindingIdentity = sessionBindingIdentity({
1677
+ sessionId: params.params.sessionId,
1678
+ sessionKey: params.params.sessionKey,
1679
+ agentId: params.agentId ?? params.params.agentId,
1468
1680
  config: params.params.config
1469
1681
  });
1470
- const startModelProvider = startModelSelection.modelProvider;
1471
- let preserveExistingBinding = params.nativeProviderWebSearchSupport === "unknown" && !binding?.threadId;
1472
- let rotatedContextEngineBinding = false;
1473
- let prebuiltPluginThreadConfig;
1474
- const throwIfAborted = () => {
1475
- if (!params.signal?.aborted) return;
1476
- const reason = params.signal.reason;
1477
- if (reason instanceof Error) throw reason;
1478
- const error = new Error(typeof reason === "string" && reason.length > 0 ? reason : "codex app-server thread lifecycle aborted");
1479
- error.name = "AbortError";
1480
- throw error;
1481
- };
1482
- const webSearchBindingChanged = binding?.threadId && binding.webSearchThreadConfigFingerprint !== webSearchThreadConfigFingerprint;
1483
- const persistentWebSearchRestriction = params.webSearchAllowed === false && params.persistentWebSearchAllowed === false;
1484
- const transientNativeToolRestriction = params.nativeCodeModeEnabled === false && !persistentWebSearchRestriction;
1485
- const transientWebSearchRestriction = isTransientWebSearchRestriction(params);
1486
- const explicitTransientWebSearchRestriction = params.webSearchAllowed === false && params.persistentWebSearchAllowed !== false && transientWebSearchRestriction;
1487
- const unknownProviderWebSearchSupport = params.nativeProviderWebSearchSupport === "unknown";
1488
- if (binding?.threadId && params.mcpServersFingerprintEvaluated === true && binding.mcpServersFingerprint !== params.mcpServersFingerprint) {
1489
- if (transientNativeToolRestriction || webSearchBindingChanged && (explicitTransientWebSearchRestriction || unknownProviderWebSearchSupport)) {
1490
- embeddedAgentLog.debug("codex app-server MCP config changed during transient restricted turn; starting transient thread", { threadId: binding.threadId });
1491
- preserveExistingBinding = true;
1492
- } else {
1493
- embeddedAgentLog.debug("codex app-server MCP config changed; starting a new thread", { threadId: binding.threadId });
1494
- await clearCodexAppServerBinding(params.params.sessionFile);
1682
+ return await params.bindingStore.withLease(bindingIdentity, async () => {
1683
+ const lifecycleTiming = createCodexThreadLifecycleTimingTracker({
1684
+ ...params.timing,
1685
+ enabled: params.timing?.enabled ?? isCodexAppServerProfilerEnabled(params.params.config)
1686
+ });
1687
+ const legacyDynamicToolsFingerprint = lifecycleTiming.measureSync("legacy-dynamic-tools-fingerprint", () => legacyFingerprintDynamicTools(params.dynamicTools));
1688
+ const dynamicToolsFingerprint = lifecycleTiming.measureSync("dynamic-tools-fingerprint", () => hashCodexAppServerBindingFingerprint(legacyDynamicToolsFingerprint));
1689
+ const dynamicToolsContainDeferred = flattenCodexDynamicToolFunctions(params.dynamicTools).some((tool) => tool.deferLoading === true);
1690
+ const webSearchThreadConfigFingerprint = fingerprintJsonObject(lifecycleTiming.measureSync("web-search-plan", () => resolveCodexWebSearchPlan({
1691
+ config: params.params.config,
1692
+ disableTools: params.params.disableTools,
1693
+ nativeToolSurfaceEnabled: params.nativeCodeModeEnabled,
1694
+ nativeProviderWebSearchSupport: params.nativeProviderWebSearchSupport,
1695
+ webSearchAllowed: params.webSearchAllowed
1696
+ })).threadConfig);
1697
+ const networkProxyConfigFingerprint = params.appServer.networkProxy?.configFingerprint;
1698
+ const contextEngineBinding = lifecycleTiming.measureSync("context-engine-binding", () => buildContextEngineBinding(params.params, params.contextEngineProjection));
1699
+ const userMcpServersConfigPatch = params.userMcpServersEnabled === false ? void 0 : buildCodexUserMcpServersThreadConfigPatch(params.params.config, { agentId: params.agentId ?? params.params.agentId });
1700
+ const legacyUserMcpServersFingerprint = legacyFingerprintUserMcpServersConfigPatch(userMcpServersConfigPatch);
1701
+ const userMcpServersFingerprint = fingerprintUserMcpServersConfigPatch(userMcpServersConfigPatch);
1702
+ const environmentSelectionFingerprint = fingerprintEnvironmentSelection(params.environmentSelection);
1703
+ let binding = await lifecycleTiming.measure("read-binding", () => params.bindingStore.read(bindingIdentity));
1704
+ const normalizeBindingModelProvider = (authProfileId, modelProvider) => normalizeCodexAppServerBindingModelProvider({
1705
+ authProfileId,
1706
+ modelProvider,
1707
+ authProfileStore: params.params.authProfileStore,
1708
+ agentDir: params.params.agentDir,
1709
+ config: params.params.config
1710
+ });
1711
+ if (!binding && bindingIdentity.kind === "session" && bindingIdentity.sessionKey) {
1712
+ if (!await lifecycleTiming.measure("reclaim-binding-generation", () => reclaimCurrentCodexSessionGeneration({
1713
+ bindingStore: params.bindingStore,
1714
+ identity: bindingIdentity,
1715
+ config: params.params.config
1716
+ }))) throw new Error(`Codex session generation is no longer current: ${bindingIdentity.sessionId}`);
1495
1717
  }
1496
- binding = void 0;
1497
- }
1498
- const deferLegacyWebSearchRotationToTransientNativeSurface = params.nativeCodeModeEnabled === false && binding?.webSearchThreadConfigFingerprint === void 0 && !persistentWebSearchRestriction;
1499
- if (binding?.threadId && webSearchBindingChanged && !deferLegacyWebSearchRotationToTransientNativeSurface) {
1500
- if (transientWebSearchRestriction) {
1501
- embeddedAgentLog.debug("codex app-server web search restricted for turn; starting transient thread", { threadId: binding.threadId });
1502
- preserveExistingBinding = true;
1503
- } else {
1504
- embeddedAgentLog.debug("codex app-server web search config changed; starting a new thread", { threadId: binding.threadId });
1505
- await clearCodexAppServerBinding(params.params.sessionFile);
1718
+ const clearCurrentBinding = async (operation) => {
1719
+ const current = binding;
1720
+ if (!current?.threadId) return;
1721
+ if (!await params.bindingStore.mutate(bindingIdentity, {
1722
+ kind: "clear",
1723
+ threadId: current.threadId
1724
+ })) throw new CodexThreadBindingConflictError(current.threadId, operation);
1725
+ binding = void 0;
1726
+ };
1727
+ if (binding?.threadId && shouldRotateCodexAppServerBindingForRuntime({
1728
+ connectionClass: params.appServer.connectionClass,
1729
+ current: params.appServerRuntimeFingerprint,
1730
+ binding: binding.appServerRuntimeFingerprint
1731
+ })) {
1732
+ embeddedAgentLog.debug("codex app-server runtime identity changed; starting a new thread", {
1733
+ threadId: binding.threadId,
1734
+ connectionClass: params.appServer.connectionClass
1735
+ });
1736
+ await clearCurrentBinding("rotating a stale thread binding");
1737
+ binding = void 0;
1506
1738
  }
1507
- binding = void 0;
1508
- }
1509
- if (binding?.threadId && transientNativeToolRestriction) {
1510
- embeddedAgentLog.debug("codex app-server native tool surface disabled for turn; starting transient thread", { threadId: binding.threadId });
1511
- preserveExistingBinding = true;
1512
- binding = void 0;
1513
- }
1514
- if (binding?.threadId && (binding.contextEngine || contextEngineBinding)) {
1515
- if (!contextEngineBinding || !isContextEngineBindingCompatible(binding.contextEngine, contextEngineBinding)) {
1516
- embeddedAgentLog.debug("codex app-server context-engine binding changed; starting a new thread", {
1739
+ if (binding?.threadId && shouldRotateCodexGpt56MultiAgentBinding({
1740
+ bindingModel: binding.model,
1741
+ requestedModel: params.params.modelId
1742
+ })) {
1743
+ embeddedAgentLog.debug("codex app-server GPT-5.6 multi-agent version changed; starting a new thread", {
1517
1744
  threadId: binding.threadId,
1518
- engineId: contextEngineBinding?.engineId,
1519
- previousEngineId: binding.contextEngine?.engineId,
1520
- epoch: contextEngineBinding?.projection?.epoch,
1521
- previousEpoch: binding.contextEngine?.projection?.epoch,
1522
- fingerprint: contextEngineBinding?.projection?.fingerprint,
1523
- previousFingerprint: binding.contextEngine?.projection?.fingerprint,
1524
- policyFingerprint: contextEngineBinding?.policyFingerprint,
1525
- previousPolicyFingerprint: binding.contextEngine?.policyFingerprint
1745
+ bindingModel: binding.model,
1746
+ requestedModel: params.params.modelId
1526
1747
  });
1527
- await clearCodexAppServerBinding(params.params.sessionFile);
1748
+ await clearCurrentBinding("rotating a GPT-5.6 multi-agent thread binding");
1528
1749
  binding = void 0;
1529
- rotatedContextEngineBinding = true;
1530
1750
  }
1531
- }
1532
- if (binding?.threadId && binding.userMcpServersFingerprint !== userMcpServersFingerprint) {
1533
- embeddedAgentLog.debug("codex app-server user MCP config changed; starting a new thread", { threadId: binding.threadId });
1534
- await clearCodexAppServerBinding(params.params.sessionFile);
1535
- binding = void 0;
1536
- }
1537
- if (binding?.threadId && binding.environmentSelectionFingerprint !== environmentSelectionFingerprint) {
1538
- embeddedAgentLog.debug("codex app-server environment selection changed; starting a new thread", { threadId: binding.threadId });
1539
- await clearCodexAppServerBinding(params.params.sessionFile);
1540
- binding = void 0;
1541
- }
1542
- if (binding?.threadId && (binding.networkProxyConfigFingerprint !== networkProxyConfigFingerprint || binding.networkProxyProfileName !== params.appServer.networkProxy?.profileName)) {
1543
- embeddedAgentLog.debug("codex app-server network proxy config changed; starting a new thread", { threadId: binding.threadId });
1544
- await clearCodexAppServerBinding(params.params.sessionFile);
1545
- binding = void 0;
1546
- }
1547
- if (binding?.threadId) {
1548
- let pluginBindingStale = isCodexPluginThreadBindingStale({
1549
- codexPluginsEnabled: params.pluginThreadConfig?.enabled ?? false,
1550
- bindingFingerprint: binding.pluginAppsFingerprint,
1551
- bindingInputFingerprint: binding.pluginAppsInputFingerprint,
1552
- currentInputFingerprint: params.pluginThreadConfig?.inputFingerprint,
1553
- hasBindingPolicyContext: Boolean(binding.pluginAppPolicyContext)
1554
- });
1555
- if (!pluginBindingStale && shouldRecheckRecoverablePluginBinding({
1751
+ const startModelSelection = resolveCodexAppServerThreadModelSelection({
1752
+ provider: params.params.provider,
1753
+ model: params.params.modelId,
1556
1754
  binding,
1557
- pluginThreadConfig: params.pluginThreadConfig
1558
- })) try {
1559
- prebuiltPluginThreadConfig = await lifecycleTiming.measure("plugin-config-recovery", () => params.pluginThreadConfig?.build());
1560
- pluginBindingStale = prebuiltPluginThreadConfig?.fingerprint !== binding.pluginAppsFingerprint;
1561
- } catch (error) {
1562
- embeddedAgentLog.warn("codex app-server plugin app config recovery check failed", {
1563
- error,
1564
- threadId: binding.threadId
1565
- });
1755
+ authProfileId: params.params.authProfileId,
1756
+ authProfileStore: params.params.authProfileStore,
1757
+ agentDir: params.params.agentDir,
1758
+ config: params.params.config
1759
+ });
1760
+ const startModelProvider = startModelSelection.modelProvider;
1761
+ let preserveExistingBinding = params.nativeProviderWebSearchSupport === "unknown" && !binding?.threadId;
1762
+ let rotatedContextEngineBinding = false;
1763
+ let prebuiltPluginThreadConfig;
1764
+ const throwIfAborted = () => {
1765
+ if (!params.signal?.aborted) return;
1766
+ const reason = params.signal.reason;
1767
+ if (reason instanceof Error) throw reason;
1768
+ const error = new Error(typeof reason === "string" && reason.length > 0 ? reason : "codex app-server thread lifecycle aborted");
1769
+ error.name = "AbortError";
1770
+ throw error;
1771
+ };
1772
+ const webSearchBindingChanged = binding?.threadId && binding.webSearchThreadConfigFingerprint !== webSearchThreadConfigFingerprint;
1773
+ const persistentWebSearchRestriction = params.webSearchAllowed === false && params.persistentWebSearchAllowed === false;
1774
+ const transientNativeToolRestriction = params.nativeCodeModeEnabled === false && !persistentWebSearchRestriction;
1775
+ const transientWebSearchRestriction = isTransientWebSearchRestriction(params);
1776
+ const explicitTransientWebSearchRestriction = params.webSearchAllowed === false && params.persistentWebSearchAllowed !== false && transientWebSearchRestriction;
1777
+ const unknownProviderWebSearchSupport = params.nativeProviderWebSearchSupport === "unknown";
1778
+ if (binding?.threadId && params.mcpServersFingerprintEvaluated === true && binding.mcpServersFingerprint !== params.mcpServersFingerprint) {
1779
+ if (transientNativeToolRestriction || webSearchBindingChanged && (explicitTransientWebSearchRestriction || unknownProviderWebSearchSupport)) {
1780
+ embeddedAgentLog.debug("codex app-server MCP config changed during transient restricted turn; starting transient thread", { threadId: binding.threadId });
1781
+ preserveExistingBinding = true;
1782
+ } else {
1783
+ embeddedAgentLog.debug("codex app-server MCP config changed; starting a new thread", { threadId: binding.threadId });
1784
+ await clearCurrentBinding("rotating a stale thread binding");
1785
+ }
1786
+ binding = void 0;
1566
1787
  }
1567
- if (pluginBindingStale) {
1568
- embeddedAgentLog.debug("codex app-server plugin app config changed; starting a new thread", { threadId: binding.threadId });
1569
- await clearCodexAppServerBinding(params.params.sessionFile);
1788
+ const deferLegacyWebSearchRotationToTransientNativeSurface = params.nativeCodeModeEnabled === false && binding?.webSearchThreadConfigFingerprint === void 0 && !persistentWebSearchRestriction;
1789
+ if (binding?.threadId && webSearchBindingChanged && !deferLegacyWebSearchRotationToTransientNativeSurface) {
1790
+ if (transientWebSearchRestriction) {
1791
+ embeddedAgentLog.debug("codex app-server web search restricted for turn; starting transient thread", { threadId: binding.threadId });
1792
+ preserveExistingBinding = true;
1793
+ } else {
1794
+ embeddedAgentLog.debug("codex app-server web search config changed; starting a new thread", { threadId: binding.threadId });
1795
+ await clearCurrentBinding("rotating a stale thread binding");
1796
+ }
1570
1797
  binding = void 0;
1571
1798
  }
1572
- }
1573
- if (binding?.threadId) {
1574
- if (binding.dynamicToolsFingerprint && params.dynamicTools.length > 0 && binding.dynamicToolsContainDeferred !== dynamicToolsContainDeferred && (binding.dynamicToolsContainDeferred !== void 0 || !dynamicToolsContainDeferred)) {
1575
- embeddedAgentLog.debug("codex app-server dynamic tool loading changed; starting a new thread", { threadId: binding.threadId });
1576
- await clearCodexAppServerBinding(params.params.sessionFile);
1799
+ if (binding?.threadId && transientNativeToolRestriction) {
1800
+ embeddedAgentLog.debug("codex app-server native tool surface disabled for turn; starting transient thread", { threadId: binding.threadId });
1801
+ preserveExistingBinding = true;
1577
1802
  binding = void 0;
1578
1803
  }
1579
- }
1580
- if (binding?.threadId) if (binding.dynamicToolsFingerprint && !areDynamicToolFingerprintsCompatible(binding.dynamicToolsFingerprint, dynamicToolsFingerprint)) {
1581
- preserveExistingBinding = shouldStartTransientNoToolThread({
1582
- previous: binding.dynamicToolsFingerprint,
1583
- next: dynamicToolsFingerprint
1584
- });
1585
- if (preserveExistingBinding) embeddedAgentLog.debug("codex app-server dynamic tools unavailable for turn; starting transient thread", { threadId: binding.threadId });
1586
- else {
1587
- embeddedAgentLog.debug("codex app-server dynamic tool catalog changed; starting a new thread", { threadId: binding.threadId });
1588
- await clearCodexAppServerBinding(params.params.sessionFile);
1804
+ if (binding?.threadId && (binding.contextEngine || contextEngineBinding)) {
1805
+ if (!contextEngineBinding || !isContextEngineBindingCompatible(binding.contextEngine, contextEngineBinding)) {
1806
+ embeddedAgentLog.debug("codex app-server context-engine binding changed; starting a new thread", {
1807
+ threadId: binding.threadId,
1808
+ engineId: contextEngineBinding?.engineId,
1809
+ previousEngineId: binding.contextEngine?.engineId,
1810
+ epoch: contextEngineBinding?.projection?.epoch,
1811
+ previousEpoch: binding.contextEngine?.projection?.epoch,
1812
+ fingerprint: contextEngineBinding?.projection?.fingerprint,
1813
+ previousFingerprint: binding.contextEngine?.projection?.fingerprint,
1814
+ policyFingerprint: contextEngineBinding?.policyFingerprint,
1815
+ previousPolicyFingerprint: binding.contextEngine?.policyFingerprint
1816
+ });
1817
+ await clearCurrentBinding("rotating a stale thread binding");
1818
+ binding = void 0;
1819
+ rotatedContextEngineBinding = true;
1820
+ }
1589
1821
  }
1590
- } else {
1591
- const resumeBinding = binding;
1592
- try {
1593
- const authProfileId = params.params.authProfileId ?? resumeBinding.authProfileId;
1594
- const finalConfigPatch = params.buildFinalConfigPatch?.({
1595
- action: "resume",
1596
- binding: resumeBinding
1597
- }) ?? {
1598
- configPatch: params.finalConfigPatch,
1599
- nativeHookRelayGeneration: params.nativeHookRelayGeneration
1600
- };
1601
- const resumeConfig = mergeCodexThreadConfigs(params.config, userMcpServersConfigPatch, finalConfigPatch.configPatch);
1602
- const resumeParams = lifecycleTiming.measureSync("thread-resume-params", () => buildThreadResumeParams(params.params, {
1603
- threadId: resumeBinding.threadId,
1604
- authProfileId,
1605
- model: startModelSelection.model,
1606
- modelProvider: startModelProvider,
1607
- appServer: params.appServer,
1608
- dynamicTools: params.dynamicTools,
1609
- developerInstructions: params.developerInstructions,
1610
- config: resumeConfig,
1611
- nativeCodeModeEnabled: params.nativeCodeModeEnabled,
1612
- nativeProviderWebSearchSupport: params.nativeProviderWebSearchSupport,
1613
- nativeCodeModeOnlyEnabled: params.nativeCodeModeOnlyEnabled,
1614
- webSearchAllowed: params.webSearchAllowed
1615
- }));
1616
- const requestModelProvider = typeof resumeParams.modelProvider === "string" && resumeParams.modelProvider.trim() ? resumeParams.modelProvider : void 0;
1617
- const response = assertCodexThreadResumeResponse(await lifecycleTiming.measure("thread-resume-request", () => params.client.request("thread/resume", resumeParams, { signal: params.signal })));
1618
- throwIfAborted();
1619
- const boundAuthProfileId = authProfileId;
1620
- const nextMcpServersFingerprint = params.mcpServersFingerprintEvaluated === true ? params.mcpServersFingerprint : resumeBinding.mcpServersFingerprint;
1621
- await lifecycleTiming.measure("thread-resume-write-binding", () => writeCodexAppServerBinding(params.params.sessionFile, {
1622
- threadId: response.thread.id,
1623
- cwd: params.cwd,
1624
- authProfileId: boundAuthProfileId,
1625
- model: response.model ?? resumeParams.model ?? params.params.modelId,
1626
- modelProvider: response.modelProvider ?? requestModelProvider ?? startModelProvider,
1627
- dynamicToolsFingerprint,
1628
- dynamicToolsContainDeferred,
1629
- webSearchThreadConfigFingerprint,
1630
- userMcpServersFingerprint,
1631
- mcpServersFingerprint: nextMcpServersFingerprint,
1632
- networkProxyProfileName: params.appServer.networkProxy?.profileName,
1633
- networkProxyConfigFingerprint,
1634
- nativeHookRelayGeneration: finalConfigPatch.nativeHookRelayGeneration ?? resumeBinding.nativeHookRelayGeneration,
1635
- appServerRuntimeFingerprint: params.appServerRuntimeFingerprint,
1636
- pluginAppsFingerprint: resumeBinding.pluginAppsFingerprint,
1637
- pluginAppsInputFingerprint: resumeBinding.pluginAppsInputFingerprint,
1638
- pluginAppPolicyContext: resumeBinding.pluginAppPolicyContext,
1639
- contextEngine: contextEngineBinding,
1640
- environmentSelectionFingerprint,
1641
- createdAt: resumeBinding.createdAt
1642
- }, {
1643
- authProfileStore: params.params.authProfileStore,
1644
- agentDir: params.params.agentDir,
1645
- config: params.params.config
1646
- }));
1822
+ if (binding?.threadId && !areUserMcpServersFingerprintsCompatible({
1823
+ previous: binding.userMcpServersFingerprint,
1824
+ next: userMcpServersFingerprint,
1825
+ nextLegacy: legacyUserMcpServersFingerprint
1826
+ })) {
1827
+ embeddedAgentLog.debug("codex app-server user MCP config changed; starting a new thread", { threadId: binding.threadId });
1828
+ await clearCurrentBinding("rotating a stale thread binding");
1829
+ binding = void 0;
1830
+ }
1831
+ if (binding?.threadId && binding.environmentSelectionFingerprint !== environmentSelectionFingerprint) {
1832
+ embeddedAgentLog.debug("codex app-server environment selection changed; starting a new thread", { threadId: binding.threadId });
1833
+ await clearCurrentBinding("rotating a stale thread binding");
1834
+ binding = void 0;
1835
+ }
1836
+ if (binding?.threadId && (binding.networkProxyConfigFingerprint !== networkProxyConfigFingerprint || binding.networkProxyProfileName !== params.appServer.networkProxy?.profileName)) {
1837
+ embeddedAgentLog.debug("codex app-server network proxy config changed; starting a new thread", { threadId: binding.threadId });
1838
+ await clearCurrentBinding("rotating a stale thread binding");
1839
+ binding = void 0;
1840
+ }
1841
+ if (binding?.threadId) {
1842
+ let pluginBindingStale = isCodexPluginThreadBindingStale({
1843
+ codexPluginsEnabled: params.pluginThreadConfig?.enabled ?? false,
1844
+ bindingFingerprint: binding.pluginAppsFingerprint,
1845
+ bindingInputFingerprint: binding.pluginAppsInputFingerprint,
1846
+ currentInputFingerprint: params.pluginThreadConfig?.inputFingerprint,
1847
+ hasBindingPolicyContext: Boolean(binding.pluginAppPolicyContext)
1848
+ });
1849
+ if (!pluginBindingStale && shouldRecheckRecoverablePluginBinding({
1850
+ binding,
1851
+ pluginThreadConfig: params.pluginThreadConfig
1852
+ })) try {
1853
+ prebuiltPluginThreadConfig = await lifecycleTiming.measure("plugin-config-recovery", () => params.pluginThreadConfig?.build());
1854
+ pluginBindingStale = prebuiltPluginThreadConfig?.fingerprint !== binding.pluginAppsFingerprint;
1855
+ } catch (error) {
1856
+ embeddedAgentLog.warn("codex app-server plugin app config recovery check failed", {
1857
+ error,
1858
+ threadId: binding.threadId
1859
+ });
1860
+ }
1861
+ if (pluginBindingStale) {
1862
+ embeddedAgentLog.debug("codex app-server plugin app config changed; starting a new thread", { threadId: binding.threadId });
1863
+ await clearCurrentBinding("rotating a stale thread binding");
1864
+ binding = void 0;
1865
+ }
1866
+ }
1867
+ if (binding?.threadId) {
1868
+ if (binding.dynamicToolsFingerprint && params.dynamicTools.length > 0 && binding.dynamicToolsContainDeferred !== dynamicToolsContainDeferred && (binding.dynamicToolsContainDeferred !== void 0 || !dynamicToolsContainDeferred)) {
1869
+ embeddedAgentLog.debug("codex app-server dynamic tool loading changed; starting a new thread", { threadId: binding.threadId });
1870
+ await clearCurrentBinding("rotating a stale thread binding");
1871
+ binding = void 0;
1872
+ }
1873
+ }
1874
+ if (binding?.threadId) if (binding.dynamicToolsFingerprint && !areDynamicToolFingerprintsCompatible(binding.dynamicToolsFingerprint, dynamicToolsFingerprint, legacyDynamicToolsFingerprint)) {
1875
+ preserveExistingBinding = shouldStartTransientNoToolThread({
1876
+ previous: binding.dynamicToolsFingerprint,
1877
+ nextHasDynamicTools: params.dynamicTools.length > 0
1878
+ });
1879
+ if (preserveExistingBinding) embeddedAgentLog.debug("codex app-server dynamic tools unavailable for turn; starting transient thread", { threadId: binding.threadId });
1880
+ else {
1881
+ embeddedAgentLog.debug("codex app-server dynamic tool catalog changed; starting a new thread", { threadId: binding.threadId });
1882
+ await clearCurrentBinding("rotating a stale thread binding");
1883
+ }
1884
+ } else {
1885
+ const resumeBinding = binding;
1886
+ let resumeReservation;
1887
+ try {
1888
+ const authProfileId = params.params.authProfileId ?? resumeBinding.authProfileId;
1889
+ const finalConfigPatch = params.buildFinalConfigPatch?.({
1890
+ action: "resume",
1891
+ binding: resumeBinding
1892
+ }) ?? {
1893
+ configPatch: params.finalConfigPatch,
1894
+ nativeHookRelayGeneration: params.nativeHookRelayGeneration
1895
+ };
1896
+ const pluginAppsConfigPatch = params.pluginThreadConfig?.enabled && resumeBinding.pluginAppPolicyContext ? buildCodexPluginAppsConfigPatchFromPolicyContext(resumeBinding.pluginAppPolicyContext) : void 0;
1897
+ const resumeConfig = mergeCodexThreadConfigs(params.config, userMcpServersConfigPatch, pluginAppsConfigPatch, finalConfigPatch.configPatch);
1898
+ const resumeParams = lifecycleTiming.measureSync("thread-resume-params", () => buildThreadResumeParams(params.params, {
1899
+ threadId: resumeBinding.threadId,
1900
+ authProfileId,
1901
+ model: startModelSelection.model,
1902
+ modelProvider: startModelProvider,
1903
+ appServer: params.appServer,
1904
+ dynamicTools: params.dynamicTools,
1905
+ developerInstructions: params.developerInstructions,
1906
+ config: resumeConfig,
1907
+ nativeCodeModeEnabled: params.nativeCodeModeEnabled,
1908
+ nativeProviderWebSearchSupport: params.nativeProviderWebSearchSupport,
1909
+ nativeCodeModeOnlyEnabled: params.nativeCodeModeOnlyEnabled,
1910
+ webSearchAllowed: params.webSearchAllowed
1911
+ }));
1912
+ const requestModelProvider = typeof resumeParams.modelProvider === "string" && resumeParams.modelProvider.trim() ? resumeParams.modelProvider : void 0;
1913
+ throwIfAborted();
1914
+ resumeReservation = params.reserveResumeThread?.(resumeBinding.threadId);
1915
+ const response = await lifecycleTiming.measure("thread-resume-request", () => resumeCodexAppServerThread({
1916
+ client: params.client,
1917
+ abandonClient: params.abandonClient ?? (() => closeCodexStartupClientBestEffort(params.client)),
1918
+ request: resumeParams,
1919
+ signal: params.signal
1920
+ }));
1921
+ throwIfAborted();
1922
+ const boundAuthProfileId = authProfileId;
1923
+ const nextMcpServersFingerprint = params.mcpServersFingerprintEvaluated === true ? params.mcpServersFingerprint : resumeBinding.mcpServersFingerprint;
1924
+ const resumePatch = {
1925
+ cwd: params.cwd,
1926
+ authProfileId: boundAuthProfileId,
1927
+ model: response.model ?? resumeParams.model ?? params.params.modelId,
1928
+ modelProvider: normalizeBindingModelProvider(boundAuthProfileId, response.modelProvider ?? requestModelProvider ?? startModelProvider),
1929
+ dynamicToolsFingerprint,
1930
+ dynamicToolsContainDeferred,
1931
+ webSearchThreadConfigFingerprint,
1932
+ userMcpServersFingerprint,
1933
+ mcpServersFingerprint: nextMcpServersFingerprint,
1934
+ networkProxyProfileName: params.appServer.networkProxy?.profileName,
1935
+ networkProxyConfigFingerprint,
1936
+ nativeHookRelayGeneration: finalConfigPatch.nativeHookRelayGeneration ?? resumeBinding.nativeHookRelayGeneration,
1937
+ appServerRuntimeFingerprint: params.appServerRuntimeFingerprint,
1938
+ pluginAppsFingerprint: resumeBinding.pluginAppsFingerprint,
1939
+ pluginAppsInputFingerprint: resumeBinding.pluginAppsInputFingerprint,
1940
+ pluginAppPolicyContext: resumeBinding.pluginAppPolicyContext,
1941
+ contextEngine: contextEngineBinding,
1942
+ environmentSelectionFingerprint
1943
+ };
1944
+ if (!await lifecycleTiming.measure("thread-resume-write-binding", () => params.bindingStore.mutate(bindingIdentity, {
1945
+ kind: "patch",
1946
+ threadId: resumeBinding.threadId,
1947
+ patch: resumePatch
1948
+ }))) throw new CodexThreadBindingConflictError(resumeBinding.threadId, "committing a resumed thread");
1949
+ if (contextEngineBinding) embeddedAgentLog.info("codex app-server wrote context-engine thread binding", {
1950
+ sessionId: params.params.sessionId,
1951
+ sessionKey: params.params.sessionKey,
1952
+ threadId: response.thread.id,
1953
+ engineId: contextEngineBinding.engineId,
1954
+ epoch: contextEngineBinding.projection?.epoch,
1955
+ fingerprint: contextEngineBinding.projection?.fingerprint,
1956
+ action: "resumed"
1957
+ });
1958
+ lifecycleTiming.mark("thread-ready");
1959
+ lifecycleTiming.logSummary({
1960
+ runId: params.params.runId,
1961
+ sessionId: params.params.sessionId,
1962
+ sessionKey: params.params.sessionKey,
1963
+ threadId: response.thread.id,
1964
+ action: "resumed"
1965
+ });
1966
+ const activeTurnIds = readActiveCodexTurnIds(response.thread);
1967
+ return {
1968
+ ...resumeBinding,
1969
+ threadId: response.thread.id,
1970
+ ...resumePatch,
1971
+ lifecycle: {
1972
+ action: "resumed",
1973
+ ...activeTurnIds.length ? { activeTurnIds } : {}
1974
+ }
1975
+ };
1976
+ } catch (error) {
1977
+ resumeReservation?.release();
1978
+ if (isCodexAppServerUnsafeSubscriptionError(error)) throw error;
1979
+ const resumeRejected = error instanceof CodexAppServerRpcError;
1980
+ if (!await unsubscribeCodexThreadBestEffort(params.client, {
1981
+ threadId: resumeBinding.threadId,
1982
+ timeoutMs: 5e3
1983
+ }) && !resumeRejected && !isCodexAppServerConnectionClosedError(error) && !params.signal?.aborted) throw new CodexAppServerUnsafeSubscriptionError("Codex thread/resume subscription cleanup failed", { cause: error });
1984
+ if (isCodexAppServerConnectionClosedError(error) || params.signal?.aborted) throw error;
1985
+ embeddedAgentLog.warn("codex app-server thread resume failed; starting a new thread", { error });
1986
+ await clearCurrentBinding("rotating a stale thread binding");
1987
+ }
1988
+ }
1989
+ const pluginThreadConfig = params.pluginThreadConfig?.enabled ? prebuiltPluginThreadConfig ?? await lifecycleTiming.measure("plugin-config-build", () => params.pluginThreadConfig?.build()) : void 0;
1990
+ const finalConfigPatch = params.buildFinalConfigPatch?.({ action: "start" }) ?? {
1991
+ configPatch: params.finalConfigPatch,
1992
+ nativeHookRelayGeneration: params.nativeHookRelayGeneration
1993
+ };
1994
+ const config = lifecycleTiming.measureSync("merge-thread-config", () => mergeCodexThreadConfigs(params.config, userMcpServersConfigPatch, pluginThreadConfig?.configPatch, finalConfigPatch.configPatch));
1995
+ const startParams = lifecycleTiming.measureSync("thread-start-params", () => buildThreadStartParams(params.params, {
1996
+ cwd: params.cwd,
1997
+ dynamicTools: params.dynamicTools,
1998
+ appServer: params.appServer,
1999
+ developerInstructions: params.developerInstructions,
2000
+ config,
2001
+ nativeCodeModeEnabled: params.nativeCodeModeEnabled,
2002
+ nativeProviderWebSearchSupport: params.nativeProviderWebSearchSupport,
2003
+ nativeCodeModeOnlyEnabled: params.nativeCodeModeOnlyEnabled,
2004
+ webSearchAllowed: params.webSearchAllowed,
2005
+ environmentSelection: params.environmentSelection,
2006
+ model: startModelSelection.model,
2007
+ modelProvider: startModelProvider
2008
+ }));
2009
+ const requestModelProvider = typeof startParams.modelProvider === "string" && startParams.modelProvider.trim() ? startParams.modelProvider : void 0;
2010
+ const response = assertCodexThreadStartResponse(await lifecycleTiming.measure("thread-start-request", async () => {
2011
+ try {
2012
+ return await params.client.request("thread/start", startParams, { signal: params.signal });
2013
+ } catch (error) {
2014
+ if (error instanceof CodexAppServerRpcError) throw new CodexThreadStartRequestError(error);
2015
+ throw error;
2016
+ }
2017
+ }));
2018
+ throwIfAborted();
2019
+ const modelProvider = resolveCodexAppServerModelProvider({
2020
+ provider: params.params.provider,
2021
+ authProfileId: params.params.authProfileId,
2022
+ authProfileStore: params.params.authProfileStore,
2023
+ agentDir: params.params.agentDir,
2024
+ config: params.params.config
2025
+ });
2026
+ const nextMcpServersFingerprint = params.mcpServersFingerprintEvaluated === true ? params.mcpServersFingerprint : void 0;
2027
+ if (!preserveExistingBinding) {
2028
+ if (!await lifecycleTiming.measure("thread-start-write-binding", () => params.bindingStore.mutate(bindingIdentity, {
2029
+ kind: "set",
2030
+ if: { kind: "absent" },
2031
+ binding: {
2032
+ threadId: response.thread.id,
2033
+ cwd: params.cwd,
2034
+ authProfileId: params.params.authProfileId,
2035
+ model: response.model ?? startParams.model ?? params.params.modelId,
2036
+ modelProvider: normalizeBindingModelProvider(params.params.authProfileId, response.modelProvider ?? requestModelProvider ?? startModelProvider ?? modelProvider),
2037
+ dynamicToolsFingerprint,
2038
+ dynamicToolsContainDeferred,
2039
+ webSearchThreadConfigFingerprint,
2040
+ userMcpServersFingerprint,
2041
+ mcpServersFingerprint: nextMcpServersFingerprint,
2042
+ networkProxyProfileName: params.appServer.networkProxy?.profileName,
2043
+ networkProxyConfigFingerprint,
2044
+ nativeHookRelayGeneration: finalConfigPatch.nativeHookRelayGeneration,
2045
+ appServerRuntimeFingerprint: params.appServerRuntimeFingerprint,
2046
+ pluginAppsFingerprint: pluginThreadConfig?.fingerprint,
2047
+ pluginAppsInputFingerprint: pluginThreadConfig?.inputFingerprint,
2048
+ pluginAppPolicyContext: pluginThreadConfig?.policyContext,
2049
+ contextEngine: contextEngineBinding,
2050
+ environmentSelectionFingerprint
2051
+ }
2052
+ }))) throw new CodexThreadBindingConflictError(response.thread.id, "committing a fresh thread");
1647
2053
  if (contextEngineBinding) embeddedAgentLog.info("codex app-server wrote context-engine thread binding", {
1648
2054
  sessionId: params.params.sessionId,
1649
2055
  sessionKey: params.params.sessionKey,
@@ -1651,90 +2057,18 @@ async function startOrResumeThread(params) {
1651
2057
  engineId: contextEngineBinding.engineId,
1652
2058
  epoch: contextEngineBinding.projection?.epoch,
1653
2059
  fingerprint: contextEngineBinding.projection?.fingerprint,
1654
- action: "resumed"
2060
+ action: rotatedContextEngineBinding ? "rotated" : "started"
1655
2061
  });
1656
- lifecycleTiming.mark("thread-ready");
1657
- lifecycleTiming.logSummary({
1658
- runId: params.params.runId,
1659
- sessionId: params.params.sessionId,
1660
- sessionKey: params.params.sessionKey,
1661
- threadId: response.thread.id,
1662
- action: "resumed"
1663
- });
1664
- const activeTurnIds = readActiveCodexTurnIds(response.thread);
1665
- return {
1666
- ...resumeBinding,
1667
- threadId: response.thread.id,
1668
- cwd: params.cwd,
1669
- authProfileId: boundAuthProfileId,
1670
- model: response.model ?? resumeParams.model ?? params.params.modelId,
1671
- modelProvider: response.modelProvider ?? requestModelProvider ?? startModelProvider,
1672
- dynamicToolsFingerprint,
1673
- dynamicToolsContainDeferred,
1674
- webSearchThreadConfigFingerprint,
1675
- userMcpServersFingerprint,
1676
- mcpServersFingerprint: nextMcpServersFingerprint,
1677
- networkProxyProfileName: params.appServer.networkProxy?.profileName,
1678
- networkProxyConfigFingerprint,
1679
- nativeHookRelayGeneration: finalConfigPatch.nativeHookRelayGeneration ?? resumeBinding.nativeHookRelayGeneration,
1680
- appServerRuntimeFingerprint: params.appServerRuntimeFingerprint,
1681
- pluginAppsFingerprint: resumeBinding.pluginAppsFingerprint,
1682
- pluginAppsInputFingerprint: resumeBinding.pluginAppsInputFingerprint,
1683
- pluginAppPolicyContext: resumeBinding.pluginAppPolicyContext,
1684
- contextEngine: contextEngineBinding,
1685
- environmentSelectionFingerprint,
1686
- lifecycle: {
1687
- action: "resumed",
1688
- ...activeTurnIds.length ? { activeTurnIds } : {}
1689
- }
1690
- };
1691
- } catch (error) {
1692
- if (isCodexAppServerConnectionClosedError(error)) throw error;
1693
- embeddedAgentLog.warn("codex app-server thread resume failed; starting a new thread", { error });
1694
- await clearCodexAppServerBinding(params.params.sessionFile);
1695
- }
1696
- }
1697
- const pluginThreadConfig = params.pluginThreadConfig?.enabled ? prebuiltPluginThreadConfig ?? await lifecycleTiming.measure("plugin-config-build", () => params.pluginThreadConfig?.build()) : void 0;
1698
- const finalConfigPatch = params.buildFinalConfigPatch?.({ action: "start" }) ?? {
1699
- configPatch: params.finalConfigPatch,
1700
- nativeHookRelayGeneration: params.nativeHookRelayGeneration
1701
- };
1702
- const config = lifecycleTiming.measureSync("merge-thread-config", () => mergeCodexThreadConfigs(params.config, userMcpServersConfigPatch, pluginThreadConfig?.configPatch, finalConfigPatch.configPatch));
1703
- const startParams = lifecycleTiming.measureSync("thread-start-params", () => buildThreadStartParams(params.params, {
1704
- cwd: params.cwd,
1705
- dynamicTools: params.dynamicTools,
1706
- appServer: params.appServer,
1707
- developerInstructions: params.developerInstructions,
1708
- config,
1709
- nativeCodeModeEnabled: params.nativeCodeModeEnabled,
1710
- nativeProviderWebSearchSupport: params.nativeProviderWebSearchSupport,
1711
- nativeCodeModeOnlyEnabled: params.nativeCodeModeOnlyEnabled,
1712
- webSearchAllowed: params.webSearchAllowed,
1713
- environmentSelection: params.environmentSelection,
1714
- model: startModelSelection.model,
1715
- modelProvider: startModelProvider
1716
- }));
1717
- const requestModelProvider = typeof startParams.modelProvider === "string" && startParams.modelProvider.trim() ? startParams.modelProvider : void 0;
1718
- const response = assertCodexThreadStartResponse(await lifecycleTiming.measure("thread-start-request", async () => {
1719
- try {
1720
- return await params.client.request("thread/start", startParams, { signal: params.signal });
1721
- } catch (error) {
1722
- if (error instanceof CodexAppServerRpcError) throw new CodexThreadStartRequestError(error);
1723
- throw error;
1724
2062
  }
1725
- }));
1726
- throwIfAborted();
1727
- const modelProvider = resolveCodexAppServerModelProvider({
1728
- provider: params.params.provider,
1729
- authProfileId: params.params.authProfileId,
1730
- authProfileStore: params.params.authProfileStore,
1731
- agentDir: params.params.agentDir,
1732
- config: params.params.config
1733
- });
1734
- const createdAt = (/* @__PURE__ */ new Date()).toISOString();
1735
- const nextMcpServersFingerprint = params.mcpServersFingerprintEvaluated === true ? params.mcpServersFingerprint : void 0;
1736
- if (!preserveExistingBinding) {
1737
- await lifecycleTiming.measure("thread-start-write-binding", () => writeCodexAppServerBinding(params.params.sessionFile, {
2063
+ lifecycleTiming.mark("thread-ready");
2064
+ lifecycleTiming.logSummary({
2065
+ runId: params.params.runId,
2066
+ sessionId: params.params.sessionId,
2067
+ sessionKey: params.params.sessionKey,
2068
+ threadId: response.thread.id,
2069
+ action: rotatedContextEngineBinding ? "rotated" : "started"
2070
+ });
2071
+ return {
1738
2072
  threadId: response.thread.id,
1739
2073
  cwd: params.cwd,
1740
2074
  authProfileId: params.params.authProfileId,
@@ -1742,7 +2076,6 @@ async function startOrResumeThread(params) {
1742
2076
  modelProvider: response.modelProvider ?? requestModelProvider ?? startModelProvider ?? modelProvider,
1743
2077
  dynamicToolsFingerprint,
1744
2078
  dynamicToolsContainDeferred,
1745
- webSearchThreadConfigFingerprint,
1746
2079
  userMcpServersFingerprint,
1747
2080
  mcpServersFingerprint: nextMcpServersFingerprint,
1748
2081
  networkProxyProfileName: params.appServer.networkProxy?.profileName,
@@ -1754,64 +2087,35 @@ async function startOrResumeThread(params) {
1754
2087
  pluginAppPolicyContext: pluginThreadConfig?.policyContext,
1755
2088
  contextEngine: contextEngineBinding,
1756
2089
  environmentSelectionFingerprint,
1757
- createdAt
1758
- }, {
1759
- authProfileStore: params.params.authProfileStore,
1760
- agentDir: params.params.agentDir,
1761
- config: params.params.config
1762
- }));
1763
- if (contextEngineBinding) embeddedAgentLog.info("codex app-server wrote context-engine thread binding", {
1764
- sessionId: params.params.sessionId,
1765
- sessionKey: params.params.sessionKey,
1766
- threadId: response.thread.id,
1767
- engineId: contextEngineBinding.engineId,
1768
- epoch: contextEngineBinding.projection?.epoch,
1769
- fingerprint: contextEngineBinding.projection?.fingerprint,
1770
- action: rotatedContextEngineBinding ? "rotated" : "started"
1771
- });
1772
- }
1773
- lifecycleTiming.mark("thread-ready");
1774
- lifecycleTiming.logSummary({
1775
- runId: params.params.runId,
1776
- sessionId: params.params.sessionId,
1777
- sessionKey: params.params.sessionKey,
1778
- threadId: response.thread.id,
1779
- action: rotatedContextEngineBinding ? "rotated" : "started"
2090
+ lifecycle: {
2091
+ action: "started",
2092
+ ...rotatedContextEngineBinding ? { rotatedContextEngineBinding } : {}
2093
+ }
2094
+ };
1780
2095
  });
1781
- return {
1782
- schemaVersion: 2,
1783
- threadId: response.thread.id,
1784
- sessionFile: params.params.sessionFile,
1785
- cwd: params.cwd,
1786
- authProfileId: params.params.authProfileId,
1787
- model: response.model ?? startParams.model ?? params.params.modelId,
1788
- modelProvider: response.modelProvider ?? requestModelProvider ?? startModelProvider ?? modelProvider,
1789
- dynamicToolsFingerprint,
1790
- dynamicToolsContainDeferred,
1791
- userMcpServersFingerprint,
1792
- mcpServersFingerprint: nextMcpServersFingerprint,
1793
- networkProxyProfileName: params.appServer.networkProxy?.profileName,
1794
- networkProxyConfigFingerprint,
1795
- nativeHookRelayGeneration: finalConfigPatch.nativeHookRelayGeneration,
1796
- appServerRuntimeFingerprint: params.appServerRuntimeFingerprint,
1797
- pluginAppsFingerprint: pluginThreadConfig?.fingerprint,
1798
- pluginAppsInputFingerprint: pluginThreadConfig?.inputFingerprint,
1799
- pluginAppPolicyContext: pluginThreadConfig?.policyContext,
1800
- contextEngine: contextEngineBinding,
1801
- environmentSelectionFingerprint,
1802
- createdAt,
1803
- updatedAt: createdAt,
1804
- lifecycle: {
1805
- action: "started",
1806
- ...rotatedContextEngineBinding ? { rotatedContextEngineBinding } : {}
1807
- }
1808
- };
1809
2096
  }
1810
2097
  function shouldRotateCodexAppServerBindingForRuntime(params) {
1811
2098
  if (!params.current) return false;
1812
2099
  if (params.binding === params.current) return false;
1813
2100
  return params.connectionClass === "remote" || Boolean(params.binding);
1814
2101
  }
2102
+ function resolveCodexGpt56MultiAgentVersion(modelRef) {
2103
+ let modelId = modelRef?.trim().toLowerCase();
2104
+ if (!modelId) return;
2105
+ const slashIndex = modelId.indexOf("/");
2106
+ if (slashIndex > 0) {
2107
+ const provider = modelId.slice(0, slashIndex);
2108
+ if (provider !== "openai" && provider !== "codex") return;
2109
+ modelId = modelId.slice(slashIndex + 1);
2110
+ }
2111
+ if (modelId === "gpt-5.6-sol" || modelId === "gpt-5.6-terra") return "v2";
2112
+ return modelId === "gpt-5.6-luna" ? "v1" : void 0;
2113
+ }
2114
+ function shouldRotateCodexGpt56MultiAgentBinding(params) {
2115
+ const bindingVersion = resolveCodexGpt56MultiAgentVersion(params.bindingModel);
2116
+ const requestedVersion = resolveCodexGpt56MultiAgentVersion(params.requestedModel);
2117
+ return Boolean(bindingVersion && requestedVersion && bindingVersion !== requestedVersion);
2118
+ }
1815
2119
  function isTransientWebSearchRestriction(params) {
1816
2120
  if (params.nativeProviderWebSearchSupport === "unknown") return true;
1817
2121
  if (params.params.config?.tools?.web?.search?.enabled === false) return false;
@@ -1907,7 +2211,7 @@ function buildThreadStartParams(params, options) {
1907
2211
  ...modelSelection.modelProvider ? { modelProvider: modelSelection.modelProvider } : {},
1908
2212
  cwd: options.cwd,
1909
2213
  approvalPolicy: options.appServer.approvalPolicy,
1910
- approvalsReviewer: options.appServer.approvalsReviewer,
2214
+ approvalsReviewer: resolveCodexThreadApprovalsReviewer(options.appServer, options.config),
1911
2215
  ...codexThreadSandboxOrPermissions(options.appServer),
1912
2216
  ...options.appServer.serviceTier !== void 0 ? { serviceTier: options.appServer.serviceTier } : {},
1913
2217
  personality: CODEX_NATIVE_PERSONALITY_NONE,
@@ -1921,21 +2225,10 @@ function buildThreadStartParams(params, options) {
1921
2225
  }),
1922
2226
  ...resolveCodexThreadEnvironmentSelection(options),
1923
2227
  developerInstructions: options.developerInstructions ?? buildDeveloperInstructions(params, { dynamicTools: options.dynamicTools }),
1924
- dynamicTools: toCodexThreadStartDynamicTools(options.dynamicTools),
1925
- experimentalRawEvents: true,
1926
- persistExtendedHistory: true
2228
+ dynamicTools: [...options.dynamicTools],
2229
+ experimentalRawEvents: true
1927
2230
  };
1928
2231
  }
1929
- function toCodexThreadStartDynamicTools(dynamicTools) {
1930
- return dynamicTools.flatMap((tool) => tool.type === "namespace" ? tool.tools.map((child) => toCodexLegacyDynamicTool(child, tool.name)) : [toCodexLegacyDynamicTool(tool)]);
1931
- }
1932
- function toCodexLegacyDynamicTool(tool, namespace) {
1933
- const { type: _type, ...legacyTool } = tool;
1934
- return namespace ? {
1935
- ...legacyTool,
1936
- namespace
1937
- } : legacyTool;
1938
- }
1939
2232
  function buildThreadResumeParams(params, options) {
1940
2233
  const resolvedModelProvider = resolveCodexAppServerModelProvider({
1941
2234
  provider: params.provider,
@@ -1957,7 +2250,7 @@ function buildThreadResumeParams(params, options) {
1957
2250
  model: modelSelection.model,
1958
2251
  ...modelSelection.modelProvider ? { modelProvider: modelSelection.modelProvider } : {},
1959
2252
  approvalPolicy: options.appServer.approvalPolicy,
1960
- approvalsReviewer: options.appServer.approvalsReviewer,
2253
+ approvalsReviewer: resolveCodexThreadApprovalsReviewer(options.appServer, options.config),
1961
2254
  ...codexThreadSandboxOrPermissions(options.appServer),
1962
2255
  ...options.appServer.serviceTier !== void 0 ? { serviceTier: options.appServer.serviceTier } : {},
1963
2256
  personality: CODEX_NATIVE_PERSONALITY_NONE,
@@ -1968,8 +2261,7 @@ function buildThreadResumeParams(params, options) {
1968
2261
  webSearchAllowed: options.webSearchAllowed,
1969
2262
  appServer: options.appServer
1970
2263
  }),
1971
- developerInstructions: options.developerInstructions ?? buildDeveloperInstructions(params, { dynamicTools: options.dynamicTools }),
1972
- persistExtendedHistory: true
2264
+ developerInstructions: options.developerInstructions ?? buildDeveloperInstructions(params, { dynamicTools: options.dynamicTools })
1973
2265
  };
1974
2266
  }
1975
2267
  function resolveCodexBindingModelProviderFallback(params) {
@@ -2082,7 +2374,7 @@ function buildTurnStartParams(params, options) {
2082
2374
  model: modelSelection.model,
2083
2375
  personality: CODEX_NATIVE_PERSONALITY_NONE,
2084
2376
  ...options.appServer.serviceTier !== void 0 ? { serviceTier: options.appServer.serviceTier } : {},
2085
- effort: resolveReasoningEffort(params.thinkLevel, modelSelection.model),
2377
+ effort: resolveReasoningEffort(params.thinkLevel, modelSelection.model, readCodexSupportedReasoningEfforts(params.model?.compat)),
2086
2378
  ...options.environmentSelection ? { environments: options.environmentSelection } : {},
2087
2379
  collaborationMode: buildTurnCollaborationMode(params, {
2088
2380
  model: modelSelection.model,
@@ -2093,6 +2385,9 @@ function buildTurnStartParams(params, options) {
2093
2385
  })
2094
2386
  };
2095
2387
  }
2388
+ function resolveCodexThreadApprovalsReviewer(appServer, config) {
2389
+ return config?.approvals_reviewer === "user" ? "user" : appServer.approvalsReviewer;
2390
+ }
2096
2391
  function codexThreadSandboxOrPermissions(appServer) {
2097
2392
  if (appServer.networkProxy) return {};
2098
2393
  return { sandbox: appServer.sandbox };
@@ -2108,7 +2403,7 @@ function buildTurnCollaborationMode(params, options = {}) {
2108
2403
  mode: "default",
2109
2404
  settings: {
2110
2405
  model,
2111
- reasoning_effort: resolveReasoningEffort(params.thinkLevel, model),
2406
+ reasoning_effort: resolveReasoningEffort(params.thinkLevel, model, readCodexSupportedReasoningEfforts(params.model?.compat)),
2112
2407
  developer_instructions: buildTurnScopedCollaborationInstructions(params, options)
2113
2408
  }
2114
2409
  };
@@ -2116,7 +2411,7 @@ function buildTurnCollaborationMode(params, options = {}) {
2116
2411
  function buildTurnScopedCollaborationInstructions(params, options = {}) {
2117
2412
  const contextInstructions = joinPresentSections(options.turnScopedDeveloperInstructions, options.memoryCollaborationInstructions, options.skillsCollaborationInstructions);
2118
2413
  if (params.trigger === "cron") return joinPresentSections(buildCronCollaborationInstructions(), contextInstructions);
2119
- if (params.trigger === "heartbeat") return joinPresentSections(buildHeartbeatCollaborationInstructions(), contextInstructions, options.heartbeatCollaborationInstructions);
2414
+ if (params.trigger === "heartbeat" && params.bootstrapContextRunKind !== "commitment-only") return joinPresentSections(buildHeartbeatCollaborationInstructions(), contextInstructions, options.heartbeatCollaborationInstructions);
2120
2415
  if (contextInstructions?.trim()) return joinPresentSections(buildDefaultCollaborationInstructions(), contextInstructions);
2121
2416
  return null;
2122
2417
  }
@@ -2156,15 +2451,24 @@ function joinPresentSections(...sections) {
2156
2451
  function codexDynamicToolsFingerprint(dynamicTools) {
2157
2452
  return fingerprintDynamicTools(dynamicTools);
2158
2453
  }
2454
+ function codexLegacyDynamicToolsFingerprint(dynamicTools) {
2455
+ return legacyFingerprintDynamicTools(dynamicTools);
2456
+ }
2159
2457
  function areCodexDynamicToolFingerprintsCompatible(params) {
2160
- return areDynamicToolFingerprintsCompatible(params.previous, params.next);
2458
+ return areDynamicToolFingerprintsCompatible(params.previous, params.next, params.nextLegacy);
2161
2459
  }
2162
2460
  function fingerprintDynamicTools(dynamicTools) {
2461
+ return hashCodexAppServerBindingFingerprint(legacyFingerprintDynamicTools(dynamicTools));
2462
+ }
2463
+ function legacyFingerprintDynamicTools(dynamicTools) {
2163
2464
  return JSON.stringify(dynamicTools.map(fingerprintDynamicToolSpec).toSorted(compareJsonFingerprint));
2164
2465
  }
2165
- function fingerprintUserMcpServersConfigPatch(configPatch) {
2466
+ function legacyFingerprintUserMcpServersConfigPatch(configPatch) {
2166
2467
  return configPatch ? JSON.stringify(stabilizeJsonValue(configPatch)) : void 0;
2167
2468
  }
2469
+ function fingerprintUserMcpServersConfigPatch(configPatch) {
2470
+ return configPatch ? hashCodexAppServerBindingFingerprint(JSON.stringify(stabilizeJsonValue(configPatch))) : void 0;
2471
+ }
2168
2472
  function fingerprintJsonObject(value) {
2169
2473
  return JSON.stringify(stabilizeJsonValue(value));
2170
2474
  }
@@ -2194,12 +2498,19 @@ function stabilizeJsonValue(value) {
2194
2498
  function readActiveCodexTurnIds(thread) {
2195
2499
  return (thread.turns ?? []).filter((turn) => turn.status === "inProgress").map((turn) => typeof turn.id === "string" ? turn.id : "").filter((turnId) => turnId.trim().length > 0);
2196
2500
  }
2197
- const EMPTY_DYNAMIC_TOOLS_FINGERPRINT = JSON.stringify([]);
2198
- function areDynamicToolFingerprintsCompatible(previous, next) {
2199
- return !previous || previous === next;
2501
+ const LEGACY_EMPTY_DYNAMIC_TOOLS_FINGERPRINT = legacyFingerprintDynamicTools([]);
2502
+ const EMPTY_DYNAMIC_TOOLS_FINGERPRINT = hashCodexAppServerBindingFingerprint(LEGACY_EMPTY_DYNAMIC_TOOLS_FINGERPRINT);
2503
+ function areDynamicToolFingerprintsCompatible(previous, next, nextLegacy) {
2504
+ return !previous || previous === next || previous === nextLegacy;
2505
+ }
2506
+ function areUserMcpServersFingerprintsCompatible(params) {
2507
+ return params.previous === params.next || params.previous === params.nextLegacy || params.nextLegacy !== void 0 && params.previous === hashCodexAppServerBindingFingerprint(params.nextLegacy);
2200
2508
  }
2201
2509
  function shouldStartTransientNoToolThread(params) {
2202
- return Boolean(params.previous && params.previous !== EMPTY_DYNAMIC_TOOLS_FINGERPRINT && params.next === EMPTY_DYNAMIC_TOOLS_FINGERPRINT);
2510
+ return Boolean(params.previous && !isEmptyDynamicToolsFingerprint(params.previous) && !params.nextHasDynamicTools);
2511
+ }
2512
+ function isEmptyDynamicToolsFingerprint(fingerprint) {
2513
+ return fingerprint === EMPTY_DYNAMIC_TOOLS_FINGERPRINT || fingerprint === LEGACY_EMPTY_DYNAMIC_TOOLS_FINGERPRINT;
2203
2514
  }
2204
2515
  function compareJsonFingerprint(left, right) {
2205
2516
  return JSON.stringify(left).localeCompare(JSON.stringify(right));
@@ -2213,7 +2524,7 @@ function buildDeveloperInstructions(params, options = {}) {
2213
2524
  "You are a personal agent running inside OpenClaw. OpenClaw has dynamic tools for OpenClaw-owned messaging, cron, sessions, media, gateway, and nodes.",
2214
2525
  buildDeferredDynamicToolManifest(options.dynamicTools),
2215
2526
  buildSkillWorkshopInstruction(options.dynamicTools),
2216
- "Use Codex native `spawn_agent` for Codex subagents. Use OpenClaw `sessions_spawn` only for OpenClaw or ACP delegation.",
2527
+ "Use Codex native `spawn_agent` for Codex subagents. `spawn_agent` and the other native collaboration tools may be deferred: when `spawn_agent` is not directly listed, load it with `tool_search` before spawning. Use OpenClaw `sessions_spawn` only for OpenClaw or ACP delegation, never as a substitute for `spawn_agent`.",
2217
2528
  buildVisibleReplyInstruction(params, options.dynamicTools),
2218
2529
  nativeCommandGuidance,
2219
2530
  params.extraSystemPrompt
@@ -2259,10 +2570,21 @@ function resolveCodexAppServerModelProvider(params) {
2259
2570
  if (isCodexAppServerNativeAuthProfile(params) && normalizedLower === "openai") return;
2260
2571
  return normalizedLower === "openai" ? "openai" : normalized;
2261
2572
  }
2262
- function resolveReasoningEffort(thinkLevel, modelId) {
2573
+ function resolveReasoningEffort(thinkLevel, modelId, supportedReasoningEfforts) {
2574
+ if (thinkLevel === "off" || thinkLevel === "adaptive") return null;
2575
+ if (supportedReasoningEfforts) return resolveCodexSupportedReasoningEffort({
2576
+ requested: thinkLevel,
2577
+ supportedReasoningEfforts
2578
+ }) ?? null;
2579
+ const fallbackReasoningEfforts = resolveCodexFallbackReasoningEfforts(modelId);
2580
+ if (fallbackReasoningEfforts) return resolveCodexSupportedReasoningEffort({
2581
+ requested: thinkLevel,
2582
+ supportedReasoningEfforts: fallbackReasoningEfforts
2583
+ }) ?? null;
2263
2584
  if (thinkLevel === "minimal") return isModernCodexModel(modelId) ? "low" : "minimal";
2264
2585
  if (thinkLevel === "low" || thinkLevel === "medium" || thinkLevel === "high" || thinkLevel === "xhigh") return thinkLevel;
2586
+ if (thinkLevel === "max" && isMaxReasoningCodexModel(modelId)) return "max";
2265
2587
  return null;
2266
2588
  }
2267
2589
  //#endregion
2268
- export { resolveCodexContextEngineProjectionReserveTokens as A, isCodexNotificationForTurn as B, isForcedPrivateQaCodexRuntime as C, fitCodexProjectedContextForTurnStart as D, resolveCodexDynamicToolsLoadingForRuntime as E, ensureCodexPluginActivation as F, readCodexNotificationTurnId as H, pluginReadParams as I, buildCodexAppInventoryCacheKey as L, buildCodexPluginThreadConfigInputFingerprint as M, mergeCodexThreadConfigs as N, projectContextEngineAssemblyForCodex as O, shouldBuildCodexPluginThreadConfig as P, defaultCodexAppInventoryCache as R, filterCodexDynamicTools as S, resolveCodexDynamicToolsLoading as T, readCodexNotificationThreadId as V, resolveCodexWebSearchPlan as _, buildDeveloperInstructions as a, sanitizeCodexHistoryImagePayloads as b, codexDynamicToolsFingerprint as c, resolveCodexAppServerRequestModelSelection as d, resolveCodexAppServerThreadModelSelection as f, buildCodexNativeWebSearchThreadConfig as g, startOrResumeThread as h, buildContextEngineBinding as i, buildCodexPluginThreadConfig as j, resolveCodexContextEngineProjectionMaxChars as k, isContextEngineBindingCompatible as l, resolveReasoningEffort as m, areCodexDynamicToolFingerprintsCompatible as n, buildTurnCollaborationMode as o, resolveCodexBindingModelProviderFallback as p, buildCodexRuntimeThreadConfig as r, buildTurnStartParams as s, CODEX_NATIVE_PERSONALITY_NONE as t, resolveCodexAppServerModelProvider as u, isCodexAppServerProfilerEnabled as v, normalizeCodexDynamicToolName as w, sanitizeInlineImageDataUrl$1 as x, invalidInlineImageText as y, describeCodexNotificationCorrelation as z };
2590
+ export { resolveCodexContextEngineProjectionMaxChars as A, buildCodexPluginThreadConfig as B, filterCodexDynamicTools as C, resolveCodexDynamicToolsLoadingForRuntime as D, resolveCodexDynamicToolsLoading as E, closeCodexStartupClientBestEffort as F, pluginReadParams as G, mergeCodexThreadConfigs as H, interruptCodexTurnBestEffort as I, buildCodexAppInventoryCacheKey as K, retireCodexAppServerClientAfterTimedOutTurn as L, CODEX_APP_SERVER_INTERRUPT_TIMEOUT_MS as M, CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS as N, fitCodexProjectedContextForTurnStart as O, CodexAppServerUnsafeSubscriptionError as P, unsubscribeCodexThreadBestEffort as R, sanitizeInlineImageDataUrl$1 as S, normalizeCodexDynamicToolName as T, shouldBuildCodexPluginThreadConfig as U, buildCodexPluginThreadConfigInputFingerprint as V, ensureCodexPluginActivation as W, buildCodexNativeWebSearchThreadConfig as _, buildDeveloperInstructions as a, invalidInlineImageText as b, codexDynamicToolsFingerprint as c, resolveCodexAppServerModelProvider as d, resolveCodexAppServerRequestModelSelection as f, startOrResumeThread as g, resolveReasoningEffort as h, buildContextEngineBinding as i, resolveCodexContextEngineProjectionReserveTokens as j, projectContextEngineAssemblyForCodex as k, codexLegacyDynamicToolsFingerprint as l, resolveCodexBindingModelProviderFallback as m, areCodexDynamicToolFingerprintsCompatible as n, buildTurnCollaborationMode as o, resolveCodexAppServerThreadModelSelection as p, defaultCodexAppInventoryCache as q, buildCodexRuntimeThreadConfig as r, buildTurnStartParams as s, CODEX_NATIVE_PERSONALITY_NONE as t, isContextEngineBindingCompatible as u, resolveCodexWebSearchPlan as v, isForcedPrivateQaCodexRuntime as w, sanitizeCodexHistoryImagePayloads as x, isCodexAppServerProfilerEnabled as y, buildCodexPluginAppsConfigPatchFromPolicyContext as z };