evolcore 0.0.23 → 0.0.24

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 (84) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +3 -1
  3. package/bin/codex-managed-acl.ps1 +7 -2
  4. package/bin/codex-managed-hook.mjs +127 -10
  5. package/bin/ec-safe-output.js +85 -3
  6. package/bin/install-codex-managed-hooks.mjs +17 -6
  7. package/dist/agents/baseagent-availability.js +4 -4
  8. package/dist/agents/baseagent.js +29 -1
  9. package/dist/agents/claude-runner.js +148 -13
  10. package/dist/agents/codex/app-server-client.js +18 -8
  11. package/dist/agents/codex/catalog.js +1 -1
  12. package/dist/agents/codex/diagnostics.js +215 -0
  13. package/dist/agents/codex-runner.js +438 -59
  14. package/dist/agents/ecagent-runner.js +17 -3
  15. package/dist/agents/gemini-runner.js +60 -2
  16. package/dist/agents/runner-types.js +1 -1
  17. package/dist/aun/aid/managed-operation.js +28 -6
  18. package/dist/aun/aid/store.js +41 -0
  19. package/dist/aun/msg/managed-operation.js +6 -1
  20. package/dist/aun/outbox.js +56 -18
  21. package/dist/aun/rpc/client.js +4 -5
  22. package/dist/channels/aun.js +147 -76
  23. package/dist/channels/daemon.js +7 -1
  24. package/dist/channels/dingtalk.js +1 -0
  25. package/dist/channels/feishu.js +10 -4
  26. package/dist/channels/wecom.js +2 -1
  27. package/dist/cli/agent-command.js +29 -3
  28. package/dist/cli/agent-create.js +538 -0
  29. package/dist/cli/agent.js +26 -675
  30. package/dist/cli/aun-commands.js +60 -20
  31. package/dist/cli/ctl-command.js +2 -0
  32. package/dist/cli/daemon-commands.js +148 -17
  33. package/dist/cli/index.js +2 -2
  34. package/dist/cli/managed-command-guard.js +3 -0
  35. package/dist/cli/restart-monitor.js +432 -8
  36. package/dist/cli/stats.js +110 -0
  37. package/dist/config/builtin-roles.js +2 -0
  38. package/dist/core/auth/auth-gateway.js +3 -2
  39. package/dist/core/auth/authorization-audit.js +72 -19
  40. package/dist/core/auth/operation-authorizer.js +63 -3
  41. package/dist/core/auth/operation-catalog.js +10 -2
  42. package/dist/core/auth/remote-access-store.js +3 -2
  43. package/dist/core/baseagent-loader.js +4 -4
  44. package/dist/core/bootstrap-messages.js +39 -18
  45. package/dist/core/bootstrap-service.js +32 -26
  46. package/dist/core/capability/capability-manager.js +8 -4
  47. package/dist/core/capability/providers/codex-capability-provider.js +1 -1
  48. package/dist/core/command/agent-control.js +7 -2
  49. package/dist/core/command/command-handler.js +87 -25
  50. package/dist/core/command/menu-catalog.js +3 -1
  51. package/dist/core/command/menu-handler.js +274 -70
  52. package/dist/core/command/menu-protocol.js +6 -2
  53. package/dist/core/command/slash-gate.js +1 -3
  54. package/dist/core/command/slash-handler.js +157 -48
  55. package/dist/core/data-migration.js +5 -15
  56. package/dist/core/event-catalog.js +3 -0
  57. package/dist/core/handoff/runtime.js +3 -1
  58. package/dist/core/handoff/store.js +7 -1
  59. package/dist/core/message/im-renderer.js +13 -6
  60. package/dist/core/message/message-bridge.js +5 -36
  61. package/dist/core/message/message-utils.js +6 -3
  62. package/dist/core/message/response-engine.js +73 -21
  63. package/dist/core/model/catalog-service.js +3 -1
  64. package/dist/core/model/model-catalog.js +10 -0
  65. package/dist/core/model/model-permission.js +4 -1
  66. package/dist/core/permission/approval-gateway.js +169 -33
  67. package/dist/core/permission/tool-policy.js +13 -6
  68. package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
  69. package/dist/core/session/session-manager.js +49 -13
  70. package/dist/core/session/session-renew.js +1 -0
  71. package/dist/index.js +174 -28
  72. package/dist/ipc.js +68 -21
  73. package/dist/paths.js +23 -1
  74. package/dist/stats/billing.js +18 -4
  75. package/dist/utils/atomic-write.js +26 -2
  76. package/dist/utils/cross-platform.js +436 -1
  77. package/dist/utils/error-utils.js +11 -4
  78. package/dist/utils/model-prices.jsonl +23 -20
  79. package/dist/utils/npm-ops.js +24 -1
  80. package/dist/utils/windows-autostart.js +1 -1
  81. package/kits/templates/roles/admin.json +2 -0
  82. package/package.json +7 -3
  83. package/AGENTS.md +0 -62
  84. package/dist/utils/codex-cli.js +0 -43
@@ -5,10 +5,10 @@ import { checkReadonly, checkDangerousCommand, evaluateToolPreflight } from '../
5
5
  import { normalizeExecutionPermissionMode, normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
6
6
  import { buildCodexProtectedFilesystemRules, isSameOrDescendant, resolveProtectedCandidate } from '../core/permission/protected-paths.js';
7
7
  import { CODEX_THREAD_ACTIVE_WRITER, CodexAppServerClient, } from './codex/app-server-client.js';
8
- import { resolveOpenaiConfig } from './baseagent.js';
8
+ import { resolveOpenaiConfig, resolveCodexCliPath } from './baseagent.js';
9
9
  import { logger } from '../utils/logger.js';
10
10
  import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
11
- import { auditCodexApprovalDecision, auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
11
+ import { auditCodexApprovalDecision, auditPermissionLifecycle, auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
12
12
  import { isRetryableError } from '../utils/error-utils.js';
13
13
  import { renderActionAsText } from '../core/interaction-router.js';
14
14
  import { buildEnvelope, isInteractionSendAccepted, sendInteractionPayload } from '../core/message/message-utils.js';
@@ -17,11 +17,12 @@ import { AGENT_DELEGATION_TOKEN_ENV, hashDelegatedCommandArgv } from '../core/au
17
17
  import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
18
18
  import { classifyEvolcoreShellCommand, containsLiteralManagedTmpDirOutsideSendContent, hasCodexCmdCarrierEcIntent, hasPowerShellDelegationAssignment, normalizeEvolcoreCommandArgv, parseCodexToolCommand, parseCodexCarrierCommand, parseLiteralShellCommand, parseCodexShellCommandArgv, resolveCodexShellCarrierArgv, } from '../core/permission/ec-command-parser.js';
19
19
  import { compareVersions } from '../utils/npm-ops.js';
20
- import { resolvePaths, resolveRoot } from '../paths.js';
20
+ import { agentSessionsDir, resolvePaths, resolveRoot } from '../paths.js';
21
21
  import { ensureProcessManagedTempDir, getManagedTaskTempDir } from '../cli/task-context.js';
22
22
  import { buildSessionTurnList } from '../core/session/session-turns.js';
23
+ import { CodexSessionFileAdapter } from '../core/session/adapters/codex-session-file-adapter.js';
23
24
  import { execFileSync } from 'child_process';
24
- import { execCodexCliSync, resolveCodexCliPath } from '../utils/codex-cli.js';
25
+ import { probeCli, probeCliSync } from '../utils/cross-platform.js';
25
26
  import { createHash, randomBytes } from 'crypto';
26
27
  import { buildModelRequestHeaders } from './request-identity.js';
27
28
  import fs from 'fs';
@@ -41,6 +42,13 @@ export function formatApprovalLogSummary(summary) {
41
42
  return singleLine;
42
43
  return singleLine.slice(0, APPROVAL_LOG_SUMMARY_MAX_LENGTH - 3).trimEnd() + '...';
43
44
  }
45
+ /**
46
+ * 规范化模型 ID:去掉 Claude Code SDK 专有的上下文窗口后缀 [1m]/[200k]
47
+ * Codex/OpenAI API 不认识这些后缀,必须传入标准模型 ID
48
+ */
49
+ function stripContextWindowSuffix(modelId) {
50
+ return modelId.replace(/\[(?:1m|200k)\]$/i, '');
51
+ }
44
52
  // ── Codex 模型目录(动态获取,含 effort) ──
45
53
  class AsyncEventQueue {
46
54
  queue = [];
@@ -89,6 +97,11 @@ function errorCodeForToolDenial(decisionSource, policyCode) {
89
97
  return 'INVALID_ARGUMENT';
90
98
  return 'EXECUTION_FAILED';
91
99
  }
100
+ function isCodexReasoningNotification(method) {
101
+ return method === 'item/reasoning/textDelta'
102
+ || method === 'item/reasoning/summaryTextDelta'
103
+ || method === 'item/reasoning/summaryPartAdded';
104
+ }
92
105
  const CODEX_THREAD_HANDOFF_TIMEOUT_MS = 2_000;
93
106
  const managedTempDirsForExitCleanup = new Map();
94
107
  let managedTempExitCleanupRegistered = false;
@@ -235,21 +248,59 @@ export function parseCodexCliVersion(output) {
235
248
  export function isCodexCliVersionSupported(version) {
236
249
  return compareVersions(version, MIN_CODEX_CLI_VERSION) >= 0;
237
250
  }
251
+ // Windows shim/process startup can exceed the quick probe deadline under load.
252
+ const CODEX_CLI_PROBE_TIMEOUT_MS = 3000;
253
+ const CODEX_CLI_PROBE_RETRY_TIMEOUT_MS = 15_000;
254
+ function compactCodexProbeText(value, maxLength = 600) {
255
+ if (Buffer.isBuffer(value))
256
+ value = value.toString('utf-8');
257
+ if (typeof value !== 'string')
258
+ return undefined;
259
+ const text = value.replace(/\s+/gu, ' ').trim();
260
+ return text.length <= maxLength ? text : `${text.slice(0, maxLength - 3).trimEnd()}...`;
261
+ }
262
+ function logCodexProbeFailure(args, startedAt, error, timeoutMs) {
263
+ const detail = error;
264
+ logger.warn(`[CodexProbe] CLI probe failed: ${JSON.stringify({
265
+ command: ['codex', ...args],
266
+ path: detail?.path ?? null,
267
+ spawnargs: detail?.spawnargs ?? null,
268
+ elapsedMs: Date.now() - startedAt,
269
+ timeoutMs,
270
+ code: detail?.code ?? null,
271
+ status: detail?.status ?? null,
272
+ signal: detail?.signal ?? null,
273
+ timedOut: detail?.code === 'ETIMEDOUT',
274
+ message: compactCodexProbeText(detail?.message ?? String(error)),
275
+ stdout: compactCodexProbeText(detail?.stdout),
276
+ stderr: compactCodexProbeText(detail?.stderr),
277
+ })}`);
278
+ }
279
+ function probeCodexCli(args) {
280
+ return probeCliSync('codex', args, { onFailure: logCodexProbeFailure });
281
+ }
238
282
  export function getCodexCliVersion() {
283
+ const startedAt = Date.now();
239
284
  try {
240
- const output = execCodexCliSync(['--version'], {
241
- encoding: 'utf-8',
242
- timeout: 3000,
243
- stdio: ['pipe', 'pipe', 'pipe'],
244
- });
245
- return parseCodexCliVersion(output);
285
+ const output = probeCodexCli(['--version']);
286
+ const version = parseCodexCliVersion(output);
287
+ if (!version) {
288
+ logCodexProbeFailure(['--version'], startedAt, {
289
+ code: 'INVALID_VERSION_OUTPUT',
290
+ message: 'Codex CLI version output could not be parsed',
291
+ status: 0,
292
+ stdout: output,
293
+ }, CODEX_CLI_PROBE_TIMEOUT_MS);
294
+ }
295
+ return version;
246
296
  }
247
297
  catch {
248
298
  return null;
249
299
  }
250
300
  }
251
301
  export function getCodexAppServerAvailability() {
252
- if (!resolveCodexCliPath()) {
302
+ const cliPath = resolveCodexCliPath();
303
+ if (!cliPath) {
253
304
  return { available: false, reason: '未检测到 Codex CLI。' };
254
305
  }
255
306
  const version = getCodexCliVersion();
@@ -265,21 +316,45 @@ export function getCodexAppServerAvailability() {
265
316
  };
266
317
  }
267
318
  try {
268
- execCodexCliSync(['app-server', '--help'], {
269
- encoding: 'utf-8',
270
- timeout: 3000,
271
- stdio: ['pipe', 'pipe', 'pipe'],
272
- });
319
+ probeCodexCli(['app-server', '--help']);
273
320
  return { available: true, version };
274
321
  }
275
- catch {
276
- return { available: false, version, reason: `Codex CLI ${version} 不支持 app-server。` };
322
+ catch (error) {
323
+ if (error?.code === 'ETIMEDOUT') {
324
+ return { available: false, version, reason: `Codex CLI ${version} 的 app-server 探测超时,请稍后重试。` };
325
+ }
326
+ return { available: false, version, reason: `Codex CLI ${version} 的 app-server 探测失败,请检查 CLI 启动日志。` };
277
327
  }
278
328
  }
279
329
  export function isCodexAppServerAvailable() {
280
330
  return getCodexAppServerAvailability().available;
281
331
  }
282
- // ── Codex Runner ──
332
+ /** Daemon startup/hot reload use asynchronous probes; sync CLI callers share the cache. */
333
+ export async function getCodexAppServerAvailabilityAsync() {
334
+ const options = {
335
+ timeouts: [CODEX_CLI_PROBE_TIMEOUT_MS, CODEX_CLI_PROBE_RETRY_TIMEOUT_MS],
336
+ onFailure: logCodexProbeFailure,
337
+ };
338
+ let version;
339
+ try {
340
+ version = parseCodexCliVersion(await probeCli('codex', ['--version'], options)) ?? undefined;
341
+ if (!version)
342
+ return { available: false, reason: '无法读取 Codex CLI 版本。' };
343
+ if (!isCodexCliVersionSupported(version)) {
344
+ return { available: false, version, requiresUpgrade: true,
345
+ reason: `Codex CLI ${version} 低于最低要求 ${MIN_CODEX_CLI_VERSION}。请升级 Codex CLI:npm install -g @openai/codex@latest` };
346
+ }
347
+ await probeCli('codex', ['app-server', '--help'], options);
348
+ return { available: true, version };
349
+ }
350
+ catch (error) {
351
+ const detail = error;
352
+ return { available: false, version, reason: detail.code === 'ETIMEDOUT' || detail.killed
353
+ ? 'Codex CLI 探测超时,请稍后重试。'
354
+ : `Codex CLI 探测失败:${error instanceof Error ? error.message : String(error)}` };
355
+ }
356
+ }
357
+ export const CODEX_SESSION_TOKEN_ENV = 'EVOLCORE_CODEX_SESSION_TOKEN';
283
358
  export class CodexRunner {
284
359
  name = 'codex';
285
360
  // turn/start persists a new user turn before runQuery resolves. App-server
@@ -291,7 +366,7 @@ export class CodexRunner {
291
366
  activeAbortControllers = new Map();
292
367
  activeStreams = new Map();
293
368
  activeSessions = new Map(); // sessionId → threadId
294
- /** Threads created/resumed for a turn but not yet durably admitted by turn/start. */
369
+ /** New threads held in memory until the exact first turn is visible in the rollout. */
295
370
  provisionalSessions = new Map();
296
371
  activeTurns = new Map();
297
372
  activeSubagentTurns = new Map();
@@ -320,8 +395,14 @@ export class CodexRunner {
320
395
  permissionGateway;
321
396
  sendPromptFn;
322
397
  permissionContexts = new Map();
398
+ /** Fallback authorization lifecycle ids for app-server requests without an RPC id. */
399
+ approvalLifecycleIds = new WeakMap();
323
400
  resolvedConfig;
324
401
  pendingInterruptTtlMs = 30_000;
402
+ /** Fallback for direct runner callers that do not inject SessionManager. */
403
+ codexSessionFileAdapter = new CodexSessionFileAdapter();
404
+ /** threadId → managed-hook credential, issued by this runner's agent only. */
405
+ threadCredentials = new Map();
325
406
  constructor(config, callbacks, runtime) {
326
407
  // Compatibility for legacy direct callers that still pass the removed
327
408
  // internal fields in a synthetic config. AgentLoader uses runtime binding.
@@ -350,7 +431,7 @@ export class CodexRunner {
350
431
  fileRewind: 'git-head',
351
432
  fullaccess: true,
352
433
  };
353
- this.model = this.resolvedConfig.model;
434
+ this.model = stripContextWindowSuffix(this.resolvedConfig.model);
354
435
  if (this.resolvedConfig.effort)
355
436
  this.effort = this.resolvedConfig.effort;
356
437
  this.onSessionIdUpdate = callbacks.onSessionIdUpdate;
@@ -775,6 +856,11 @@ export class CodexRunner {
775
856
  error.code = 'TOPIC_BACKEND_DISCOVERY_REJECTED';
776
857
  throw error;
777
858
  }
859
+ // The binding callback can yield while a replacement run claims this
860
+ // session. Re-check the provisional owner before publishing any local
861
+ // indexes so a late first turn cannot overwrite the newer thread.
862
+ if (this.provisionalSessions.get(sessionId) !== threadId)
863
+ return false;
778
864
  this.provisionalSessions.delete(sessionId);
779
865
  this.activeSessions.set(sessionId, threadId);
780
866
  if (carrierToPublish) {
@@ -784,6 +870,30 @@ export class CodexRunner {
784
870
  this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${threadId}`);
785
871
  return true;
786
872
  }
873
+ async waitForCodexTurnPersistence(sessionManager, projectPath, threadId, turnId, timeoutMs = 0) {
874
+ const check = () => {
875
+ try {
876
+ if (typeof sessionManager?.checkSessionTurnPersisted === 'function') {
877
+ return sessionManager.checkSessionTurnPersisted(projectPath, threadId, 'codex', turnId, this.resolvedConfig.agentAid) === true;
878
+ }
879
+ return this.codexSessionFileAdapter.checkTurnPersisted(projectPath, threadId, turnId, this.resolvedConfig.agentAid);
880
+ }
881
+ catch (error) {
882
+ logger.debug(`[CodexRunner] rollout persistence check failed: session=${threadId} turn=${turnId} `
883
+ + `error=${error instanceof Error ? error.message : String(error)}`);
884
+ return false;
885
+ }
886
+ };
887
+ const deadline = Date.now() + Math.max(0, timeoutMs);
888
+ while (true) {
889
+ if (check())
890
+ return true;
891
+ const remaining = deadline - Date.now();
892
+ if (remaining <= 0)
893
+ return false;
894
+ await new Promise(resolve => setTimeout(resolve, Math.min(10, remaining)));
895
+ }
896
+ }
787
897
  cleanupProvisionalThread(sessionId, threadId) {
788
898
  if (this.provisionalSessions.get(sessionId) !== threadId)
789
899
  return;
@@ -1094,7 +1204,7 @@ export class CodexRunner {
1094
1204
  }
1095
1205
  }
1096
1206
  // ── ModelSwitcher ──
1097
- setModel(model) { this.model = model; this.resetAppServerClient(); }
1207
+ setModel(model) { this.model = stripContextWindowSuffix(model); this.resetAppServerClient(); }
1098
1208
  getModel() { return this.model; }
1099
1209
  getModelCatalog() {
1100
1210
  return getRunnerCatalog('codex', this.resolvedConfig.agentAid, this.resolvedConfig);
@@ -1315,7 +1425,28 @@ export class CodexRunner {
1315
1425
  }
1316
1426
  await appServer.assertManagedHooksAvailable();
1317
1427
  }
1318
- async evaluatePreToolUse(threadId, toolName, toolInput, signal, callId) {
1428
+ /**
1429
+ * Decide whether the managed hook may proceed with a tool call.
1430
+ *
1431
+ * Credential contract (see CODEX_SESSION_TOKEN_ENV):
1432
+ * - sessionToken present and issued by this runner for this thread → the
1433
+ * call belongs to an EvolCore-managed thread; full policy evaluation.
1434
+ * - sessionToken present but unknown/mismatched → forgery or stale state;
1435
+ * fail closed with applicable=true so no other outcome allows it.
1436
+ * - sessionToken absent → unmanaged process; not this runner's concern
1437
+ * (applicable=false; the IPC layer allows such calls outright).
1438
+ */
1439
+ async evaluatePreToolUse(threadId, toolName, toolInput, signal, callId, sessionToken) {
1440
+ if (typeof sessionToken === 'string' && sessionToken) {
1441
+ if (!this.validateThreadCredential(threadId, sessionToken)) {
1442
+ return {
1443
+ ok: false,
1444
+ applicable: true,
1445
+ decision: 'deny',
1446
+ reason: 'Codex session credential was not recognized',
1447
+ };
1448
+ }
1449
+ }
1319
1450
  const sessionKey = this.findSessionKeyByThread(threadId);
1320
1451
  const activeThread = this.activeSessions.get(sessionKey) ?? this.provisionalSessions.get(sessionKey);
1321
1452
  const context = this.permissionContexts.get(sessionKey);
@@ -1363,6 +1494,16 @@ export class CodexRunner {
1363
1494
  }
1364
1495
  };
1365
1496
  if (!activeThread || !this.isThreadOwnedBySession(sessionKey, threadId)) {
1497
+ if (typeof sessionToken === 'string' && sessionToken) {
1498
+ // A credentialed thread this runner cannot place (e.g. resumed state
1499
+ // lost across a runner reload) must never fall through to allow.
1500
+ return {
1501
+ ok: false,
1502
+ applicable: true,
1503
+ decision: 'deny',
1504
+ reason: 'EvolCore-managed Codex thread is not tracked by an active session',
1505
+ };
1506
+ }
1366
1507
  return { ok: true, applicable: false };
1367
1508
  }
1368
1509
  const proactiveSession = context?.chatmode === 'proactive';
@@ -1603,7 +1744,7 @@ export class CodexRunner {
1603
1744
  }
1604
1745
  }
1605
1746
  let resumingThread = !!agentSessionId;
1606
- const callModel = modelOverride?.model || this.model;
1747
+ const callModel = stripContextWindowSuffix(modelOverride?.model || this.model);
1607
1748
  const callEffort = modelOverride?.effortMode === 'model_default'
1608
1749
  ? undefined
1609
1750
  : modelOverride?.effort ?? this.effort;
@@ -1647,7 +1788,7 @@ export class CodexRunner {
1647
1788
  approvalPolicy: effectiveApprovalPolicy,
1648
1789
  approvalsReviewer: 'user',
1649
1790
  ...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
1650
- config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, runtimeEnv ? { shell_environment_policy: { set: runtimeEnv } } : undefined, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrierCandidate, callMode === 'fullaccess' ? runtimeEnv?.[AGENT_DELEGATION_TOKEN_ENV] : undefined), this.buildLifecycleLockdownConfig(callMode)), callMode),
1791
+ config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, runtimeEnv ? { shell_environment_policy: { set: runtimeEnv } } : undefined, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrierCandidate, callMode === 'fullaccess' ? runtimeEnv?.[AGENT_DELEGATION_TOKEN_ENV] : undefined, agentSessionId), this.buildLifecycleLockdownConfig(callMode)), callMode),
1651
1792
  ...(developerInstructions ? { developerInstructions } : {}),
1652
1793
  };
1653
1794
  logger.info(`[CodexRunner] runQuery permMode=${requestedPermissionMode}->${callMode} ` +
@@ -1690,6 +1831,7 @@ export class CodexRunner {
1690
1831
  this.threadDelegationCarriers.delete(missingThreadId);
1691
1832
  this.unbindExternalToolBoundary(missingThreadId);
1692
1833
  this.clearTrackedApprovalItemsForThread(missingThreadId);
1834
+ this.dropThreadCredential(missingThreadId);
1693
1835
  agentSessionId = undefined;
1694
1836
  resumingThread = false;
1695
1837
  knownDelegationCarrier = undefined;
@@ -1795,6 +1937,7 @@ export class CodexRunner {
1795
1937
  projectPath,
1796
1938
  model: callModel,
1797
1939
  committed: resumingThread,
1940
+ persistenceFinalized: resumingThread,
1798
1941
  streamedAgentMessageIds: new Set(),
1799
1942
  agentMessageDeltaText: new Map(),
1800
1943
  completedItemIds: new Set(),
@@ -1834,8 +1977,9 @@ export class CodexRunner {
1834
1977
  this.unregisterChildThread(notifThreadId);
1835
1978
  this.endAppServerQueueWhenSubagentsDone(queue, state);
1836
1979
  }
1837
- if (this.isSubagentActivityItem(item))
1980
+ if (this.isSubagentActivityItem(item) || isCodexReasoningNotification(notification.method)) {
1838
1981
  queue.push(notification);
1982
+ }
1839
1983
  if (notification.method === 'item/completed' && state.turnCompleted) {
1840
1984
  this.endAppServerQueueWhenSubagentsDone(queue, state);
1841
1985
  }
@@ -1882,11 +2026,43 @@ export class CodexRunner {
1882
2026
  ...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
1883
2027
  });
1884
2028
  if (!resumingThread) {
1885
- state.commitProvisional = async () => {
1886
- const committed = await this.commitProvisionalThread(sessionId, threadId, carrierToPublish, sessionManager, turn);
1887
- if (committed)
1888
- agentSessionId = threadId;
1889
- return committed;
2029
+ let confirmationInFlight;
2030
+ state.confirmProvisional = (waitMs = 0) => {
2031
+ if (state.committed)
2032
+ return Promise.resolve(true);
2033
+ if (state.persistenceFinalized)
2034
+ return Promise.resolve(false);
2035
+ if (confirmationInFlight)
2036
+ return confirmationInFlight;
2037
+ const providerTurnId = state.turnId;
2038
+ if (!providerTurnId)
2039
+ return Promise.resolve(false);
2040
+ const confirmation = (async () => {
2041
+ const persisted = await this.waitForCodexTurnPersistence(sessionManager, projectPath, threadId, providerTurnId, waitMs);
2042
+ if (!persisted)
2043
+ return false;
2044
+ try {
2045
+ const committed = await this.commitProvisionalThread(sessionId, threadId, carrierToPublish, sessionManager, turn);
2046
+ if (!committed) {
2047
+ const staleError = new Error('Codex provisional thread was superseded before persistence confirmation');
2048
+ staleError.code = 'RUNNER_START_SUPERSEDED';
2049
+ throw staleError;
2050
+ }
2051
+ agentSessionId = threadId;
2052
+ state.committed = true;
2053
+ return true;
2054
+ }
2055
+ catch (error) {
2056
+ state.persistenceFinalized = true;
2057
+ throw error;
2058
+ }
2059
+ })();
2060
+ confirmationInFlight = confirmation;
2061
+ void confirmation.finally(() => {
2062
+ if (confirmationInFlight === confirmation)
2063
+ confirmationInFlight = undefined;
2064
+ }).catch(() => undefined);
2065
+ return confirmation;
1890
2066
  };
1891
2067
  }
1892
2068
  const turnId = turnResponse.turn?.id;
@@ -1894,6 +2070,12 @@ export class CodexRunner {
1894
2070
  state.turnId = turnId;
1895
2071
  this.activeTurns.set(sessionId, { threadId, turnId });
1896
2072
  }
2073
+ if (!resumingThread && turnId) {
2074
+ // Check immediately, but leave a just-created rollout as a candidate.
2075
+ // Subsequent notifications recheck it and the terminal boundary gets
2076
+ // the short settle window below.
2077
+ await state.confirmProvisional?.(0);
2078
+ }
1897
2079
  if (turnId && this.consumePendingInterrupt(sessionId)) {
1898
2080
  await this.interruptAppServerTurn(threadId, turnId);
1899
2081
  controller.abort('User interrupt');
@@ -2042,11 +2224,16 @@ export class CodexRunner {
2042
2224
  }
2043
2225
  capturedController?.abort('Session closed');
2044
2226
  const capturedSubscription = capturedThreadId ? this.threadSubscriptions.get(capturedThreadId) : undefined;
2227
+ const currentActiveThreadId = this.activeSessions.get(sessionId);
2228
+ const currentProvisionalThreadId = this.provisionalSessions.get(sessionId);
2229
+ const capturedThreadIds = new Set([capturedThreadId, capturedProvisionalThreadId].filter((value) => value !== undefined));
2230
+ const threadLifecycleReplaced = (currentActiveThreadId !== undefined && !capturedThreadIds.has(currentActiveThreadId))
2231
+ || (currentProvisionalThreadId !== undefined && !capturedThreadIds.has(currentProvisionalThreadId));
2045
2232
  const lifecycleReplaced = (capturedStream !== undefined && this.activeStreams.get(sessionId) !== capturedStream)
2046
2233
  || (capturedController !== undefined && this.activeAbortControllers.get(sessionId) !== capturedController)
2047
2234
  || (capturedTurn !== undefined && this.activeTurns.get(sessionId) !== capturedTurn)
2048
2235
  || (capturedPermissionContext !== undefined && this.permissionContexts.get(sessionId) !== capturedPermissionContext)
2049
- || (capturedThreadId !== undefined && this.activeSessions.get(sessionId) !== capturedThreadId);
2236
+ || threadLifecycleReplaced;
2050
2237
  if (capturedSubscription && !lifecycleReplaced
2051
2238
  && this.threadSubscriptions.get(capturedThreadId) === capturedSubscription) {
2052
2239
  await this.releaseThreadSubscription(capturedSubscription.client, capturedThreadId, 'session-closed');
@@ -2083,7 +2270,7 @@ export class CodexRunner {
2083
2270
  }
2084
2271
  }
2085
2272
  resolveSessionFile(agentSessionId, _projectPath) {
2086
- // Codex session 文件: ~/.codex/sessions/YYYY/MM/DD/rollout-*-{threadId}.jsonl
2273
+ // Codex sessions are shared with the local Codex Desktop installation.
2087
2274
  const sessionsDir = path.join(os.homedir(), '.codex', 'sessions');
2088
2275
  if (!fs.existsSync(sessionsDir))
2089
2276
  return null;
@@ -2145,7 +2332,7 @@ export class CodexRunner {
2145
2332
  if (!this.isThreadNotFoundError(error))
2146
2333
  throw error;
2147
2334
  logger.info(`[CodexRunner] Compact thread not loaded, resuming before compact: ${agentSessionId}`);
2148
- const compactModel = modelOverride?.model || this.model;
2335
+ const compactModel = stripContextWindowSuffix(modelOverride?.model || this.model);
2149
2336
  const compactEffort = modelOverride?.effortMode === 'model_default'
2150
2337
  ? undefined
2151
2338
  : modelOverride?.effort ?? this.effort;
@@ -2165,7 +2352,7 @@ export class CodexRunner {
2165
2352
  approvalPolicy: compactPolicy,
2166
2353
  approvalsReviewer: 'user',
2167
2354
  ...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
2168
- config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(_sessionId, compactDelegationCarrier), this.buildLifecycleLockdownConfig(compactMode)), compactMode),
2355
+ config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(_sessionId, compactDelegationCarrier, undefined, agentSessionId), this.buildLifecycleLockdownConfig(compactMode)), compactMode),
2169
2356
  });
2170
2357
  this.markThreadSubscribed(appServer, agentSessionId);
2171
2358
  this.threadProjectPaths.set(agentSessionId, path.resolve(_projectPath));
@@ -2274,10 +2461,14 @@ export class CodexRunner {
2274
2461
  }
2275
2462
  return false;
2276
2463
  }
2464
+ codexHomeDir() {
2465
+ return process.env.CODEX_HOME || path.join(process.env.HOME || os.homedir(), '.codex');
2466
+ }
2277
2467
  findCodexSessionFile(threadId) {
2278
- const root = process.env.CODEX_HOME
2279
- ? path.join(process.env.CODEX_HOME, 'sessions')
2280
- : path.join(process.env.HOME || os.homedir(), '.codex', 'sessions');
2468
+ return this.findRolloutFile(path.join(this.codexHomeDir(), 'sessions'), threadId)
2469
+ ?? this.findRolloutFile(path.join(this.codexHomeDir(), 'archived_sessions'), threadId);
2470
+ }
2471
+ findRolloutFile(root, threadId) {
2281
2472
  const stack = [root];
2282
2473
  let newest;
2283
2474
  while (stack.length > 0) {
@@ -2309,6 +2500,101 @@ export class CodexRunner {
2309
2500
  }
2310
2501
  return newest?.path;
2311
2502
  }
2503
+ // ── Managed-hook thread credentials ─────────────────────────────────
2504
+ // The Windows managed hook is machine-wide, so unmanaged Codex processes
2505
+ // also reach it. Credentials let the hook tell EvolCore-owned threads
2506
+ // (policy enforced) apart from unmanaged ones (allowed unconditionally).
2507
+ // They follow the thread (not the daemon process) so a daemon restart does
2508
+ // not strand a resumable thread: the token file is re-read on resume, and
2509
+ // it is removed lazily once the rollout file itself disappears.
2510
+ threadCredentialsDir() {
2511
+ const aid = this.resolvedConfig.agentAid;
2512
+ if (!aid)
2513
+ return undefined;
2514
+ return path.join(agentSessionsDir(aid), 'codex-tokens');
2515
+ }
2516
+ threadCredentialPath(threadId) {
2517
+ const dir = this.threadCredentialsDir();
2518
+ return dir ? path.join(dir, `${encodeURIComponent(threadId)}.json`) : undefined;
2519
+ }
2520
+ /** Issue or re-read this agent's credential for a thread. */
2521
+ ensureThreadCredential(threadId) {
2522
+ const cached = this.threadCredentials.get(threadId);
2523
+ if (cached)
2524
+ return cached;
2525
+ const file = this.threadCredentialPath(threadId);
2526
+ if (!file)
2527
+ return undefined;
2528
+ try {
2529
+ const record = JSON.parse(fs.readFileSync(file, 'utf8'));
2530
+ if (record && typeof record.token === 'string' && record.token && record.threadId === threadId) {
2531
+ this.threadCredentials.set(threadId, record.token);
2532
+ return record.token;
2533
+ }
2534
+ }
2535
+ catch { /* missing or unreadable: fall through to issuance */ }
2536
+ const token = randomBytes(32).toString('hex');
2537
+ const record = {
2538
+ token,
2539
+ threadId,
2540
+ createdAt: new Date().toISOString(),
2541
+ rolloutPath: this.findCodexSessionFile(threadId),
2542
+ };
2543
+ try {
2544
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
2545
+ fs.writeFileSync(file, JSON.stringify(record, null, 2), { mode: 0o600 });
2546
+ this.threadCredentials.set(threadId, token);
2547
+ return token;
2548
+ }
2549
+ catch (error) {
2550
+ logger.warn(`[CodexRunner] failed to persist thread credential: thread=${threadId} error=${error instanceof Error ? error.message : String(error)}`);
2551
+ return undefined;
2552
+ }
2553
+ }
2554
+ /**
2555
+ * Validate a credential presented by the managed hook. Returns the matched
2556
+ * threadId when this runner issued the token; undefined otherwise. Token
2557
+ * records whose rollout file has disappeared are pruned lazily.
2558
+ */
2559
+ validateThreadCredential(threadId, token) {
2560
+ const expected = this.ensureThreadCredentialLoaded(threadId);
2561
+ if (!expected || expected !== token)
2562
+ return false;
2563
+ const rollout = this.findCodexSessionFile(threadId);
2564
+ if (!rollout) {
2565
+ // The thread's rollout is gone: nothing left to protect, drop the record.
2566
+ this.dropThreadCredential(threadId);
2567
+ return false;
2568
+ }
2569
+ return true;
2570
+ }
2571
+ ensureThreadCredentialLoaded(threadId) {
2572
+ const cached = this.threadCredentials.get(threadId);
2573
+ if (cached)
2574
+ return cached;
2575
+ const file = this.threadCredentialPath(threadId);
2576
+ if (!file)
2577
+ return undefined;
2578
+ try {
2579
+ const record = JSON.parse(fs.readFileSync(file, 'utf8'));
2580
+ if (record && typeof record.token === 'string' && record.token && record.threadId === threadId) {
2581
+ this.threadCredentials.set(threadId, record.token);
2582
+ return record.token;
2583
+ }
2584
+ }
2585
+ catch { /* absent or unreadable */ }
2586
+ return undefined;
2587
+ }
2588
+ dropThreadCredential(threadId) {
2589
+ this.threadCredentials.delete(threadId);
2590
+ const file = this.threadCredentialPath(threadId);
2591
+ if (!file)
2592
+ return;
2593
+ try {
2594
+ fs.rmSync(file, { force: true });
2595
+ }
2596
+ catch { /* best effort */ }
2597
+ }
2312
2598
  async forkSession(agentSessionId, projectPath, title) {
2313
2599
  const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId), 'fork');
2314
2600
  try {
@@ -2597,6 +2883,31 @@ export class CodexRunner {
2597
2883
  ? 'FileChange'
2598
2884
  : 'Bash';
2599
2885
  const toolInput = this.buildPermissionInput(request.method, params);
2886
+ const approvalCallId = typeof (params.itemId ?? params.item_id ?? params.callId ?? params.call_id) === 'string'
2887
+ ? String(params.itemId ?? params.item_id ?? params.callId ?? params.call_id)
2888
+ : undefined;
2889
+ const approvalRequestId = request.id !== undefined
2890
+ ? String(request.id)
2891
+ : approvalCallId ?? `codex-approval-${crypto.randomUUID()}`;
2892
+ this.approvalLifecycleIds.set(request, approvalRequestId);
2893
+ const approvalContext = this.permissionContexts.get(sessionKey);
2894
+ auditPermissionLifecycle({
2895
+ phase: 'request',
2896
+ requestId: approvalRequestId,
2897
+ callId: approvalCallId,
2898
+ correlationId: approvalCallId,
2899
+ sessionId: sessionKey,
2900
+ agentAid: approvalContext?.selfAid,
2901
+ agentName: approvalContext?.agentName,
2902
+ permissionMode: normalizeExecutionPermissionMode(this.chatModes.get(sessionKey) ?? this.currentMode),
2903
+ toolName,
2904
+ decision: 'pending',
2905
+ decisionSource: 'approval',
2906
+ role: approvalContext?.role,
2907
+ taskId: approvalContext?.taskId,
2908
+ summary: summarizeToolInputForAudit(toolName, toolInput).slice(0, 512),
2909
+ approvalMethod: request.method,
2910
+ });
2600
2911
  // A trusted fullaccess run has already been explicitly authorized by the
2601
2912
  // daemon owner. Do not route its approvals through ordinary capability,
2602
2913
  // workspace, or unattended-trigger policy checks.
@@ -2834,6 +3145,49 @@ export class CodexRunner {
2834
3145
  reason: reason ?? policyCode ?? (decision === 'allow' ? 'approval accepted' : 'approval denied'),
2835
3146
  parseFailure,
2836
3147
  });
3148
+ const lifecycleRequestId = request.id !== undefined
3149
+ ? String(request.id)
3150
+ : this.approvalLifecycleIds.get(request)
3151
+ ?? callId
3152
+ ?? `codex-approval-${crypto.randomUUID()}`;
3153
+ auditPermissionLifecycle({
3154
+ phase: 'response',
3155
+ requestId: lifecycleRequestId,
3156
+ callId,
3157
+ correlationId: callId,
3158
+ sessionId: sessionKey,
3159
+ agentAid: context?.selfAid,
3160
+ agentName: context?.agentName,
3161
+ permissionMode: normalizeExecutionPermissionMode(this.chatModes.get(sessionKey) ?? this.currentMode),
3162
+ toolName,
3163
+ decision,
3164
+ decisionSource,
3165
+ policyCode,
3166
+ reason: reason ?? policyCode ?? (decision === 'allow' ? 'approval accepted' : 'approval denied'),
3167
+ role: context?.role,
3168
+ taskId: context?.taskId,
3169
+ summary: auditSummary,
3170
+ approvalMethod: request.method,
3171
+ });
3172
+ auditPermissionLifecycle({
3173
+ phase: 'final',
3174
+ requestId: lifecycleRequestId,
3175
+ callId,
3176
+ correlationId: callId,
3177
+ sessionId: sessionKey,
3178
+ agentAid: context?.selfAid,
3179
+ agentName: context?.agentName,
3180
+ permissionMode: normalizeExecutionPermissionMode(this.chatModes.get(sessionKey) ?? this.currentMode),
3181
+ toolName,
3182
+ decision,
3183
+ decisionSource,
3184
+ policyCode,
3185
+ reason: reason ?? policyCode ?? (decision === 'allow' ? 'approval accepted' : 'approval denied'),
3186
+ role: context?.role,
3187
+ taskId: context?.taskId,
3188
+ summary: auditSummary,
3189
+ approvalMethod: request.method,
3190
+ });
2837
3191
  logger.info(`[CodexRunner] app-server approval audit ${JSON.stringify({
2838
3192
  requestId: request.id !== undefined ? String(request.id) : undefined,
2839
3193
  ...(callId ? { callId, correlationId: callId } : {}),
@@ -3924,26 +4278,37 @@ export class CodexRunner {
3924
4278
  for await (const notification of notifications) {
3925
4279
  if (!this.activeAbortControllers.has(sessionId))
3926
4280
  break;
3927
- if (!state.committed && notification.method === 'turn/completed') {
3928
- const turn = notification.params?.turn ?? {};
3929
- if (turn.status === 'completed') {
3930
- const committed = await state.commitProvisional?.();
3931
- if (!committed) {
3932
- const staleError = new Error('Codex provisional thread was superseded before commit');
3933
- staleError.code = 'RUNNER_START_SUPERSEDED';
3934
- throw staleError;
3935
- }
3936
- state.committed = true;
3937
- if (!sessionIdEmitted) {
3938
- sessionIdEmitted = true;
3939
- yield { type: 'session_id', sessionId: state.threadId };
3940
- }
4281
+ if (!state.committed && !state.persistenceFinalized) {
4282
+ const terminal = notification.method === 'turn/completed';
4283
+ const committed = await state.confirmProvisional?.(terminal ? 250 : 0) ?? false;
4284
+ if (committed && !sessionIdEmitted) {
4285
+ sessionIdEmitted = true;
4286
+ yield { type: 'session_id', sessionId: state.threadId };
4287
+ }
4288
+ else if (terminal && !committed) {
4289
+ state.persistenceFinalized = true;
4290
+ const persistenceError = new Error(`Codex turn completed before its rollout became recoverable: thread=${state.threadId} turn=${state.turnId ?? '<unknown>'}`);
4291
+ persistenceError.code = 'CODEX_TURN_PERSISTENCE_UNCONFIRMED';
4292
+ throw persistenceError;
3941
4293
  }
3942
4294
  }
3943
4295
  yield* this.mapAppServerNotification(notification, sessionId, state);
3944
4296
  }
3945
4297
  }
3946
4298
  finally {
4299
+ if (!state.committed && !state.persistenceFinalized && state.confirmProvisional) {
4300
+ try {
4301
+ // Do not let an interrupt wait for persistence that did not exist at
4302
+ // cancellation time. A zero-wait check still captures a rollout
4303
+ // that was already durable before the stream was closed.
4304
+ await state.confirmProvisional(0);
4305
+ }
4306
+ catch (error) {
4307
+ logger.debug(`[CodexRunner] provisional thread rejected during final persistence check: `
4308
+ + `session=${sessionId} thread=${state.threadId} error=${error instanceof Error ? error.message : String(error)}`);
4309
+ }
4310
+ state.persistenceFinalized = true;
4311
+ }
3947
4312
  unsubscribe();
3948
4313
  await this.releaseThreadSubscription(appServer, state.threadId, 'turn-finished');
3949
4314
  if (this.activeAbortControllers.get(sessionId) === controller) {
@@ -3990,6 +4355,15 @@ export class CodexRunner {
3990
4355
  }
3991
4356
  break;
3992
4357
  }
4358
+ // Reasoning notifications are provider-internal liveness signals.
4359
+ // Do not expose their content; the ResponseEngine consumes the heartbeat
4360
+ // solely to reset the idle monitor.
4361
+ case 'item/reasoning/textDelta':
4362
+ case 'item/reasoning/summaryTextDelta':
4363
+ case 'item/reasoning/summaryPartAdded': {
4364
+ yield { type: 'heartbeat', source: 'reasoning' };
4365
+ break;
4366
+ }
3993
4367
  case 'item/completed': {
3994
4368
  const item = params.item;
3995
4369
  if (item?.id)
@@ -4601,12 +4975,17 @@ export class CodexRunner {
4601
4975
  }
4602
4976
  }
4603
4977
  managedTempInstruction(sessionId) {
4604
- if (!this.ensureSessionTempDir(sessionId))
4978
+ const managedTempDir = this.ensureSessionTempDir(sessionId);
4979
+ if (!managedTempDir)
4605
4980
  return undefined;
4606
- return '临时文件必须使用环境变量 `$TMPDIR`。它指向当前 EvolCore 会话的受管临时工作区;不要直接使用 `/tmp`,也不要使用其他会话的临时目录。当前是可持久化 thread 的恢复轮次,早先上下文中的 `/tmp/evolcore-codex-*` 绝对路径可能已经失效;不要重用旧路径。若文件不存在,请先在当前 `$TMPDIR` 重新创建,再继续执行后续 `ec` 命令。';
4981
+ return `临时文件必须使用环境变量 \`$TMPDIR\`。当前值是 \`${managedTempDir}\`,它是当前 EvolCore 会话唯一允许的临时工作区;不要直接使用 \`/tmp\`,也不要使用其他会话的临时目录。当前是可持久化 thread 的恢复轮次,早先上下文中的 \`/tmp/evolcore-codex-*\` 绝对路径可能已经失效;不要重用旧路径。若文件不存在,请先在当前 \`$TMPDIR\` 重新创建,再继续执行后续 \`ec\` 命令。`;
4607
4982
  }
4608
- buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrier, taskDelegationToken) {
4983
+ buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrier, taskDelegationToken, threadId) {
4609
4984
  const managedTempDir = this.ensureSessionTempDir(sessionId);
4985
+ // The managed-hook credential rides the thread's shell environment so the
4986
+ // PreToolUse hook can prove the call belongs to an EvolCore-owned thread.
4987
+ // It follows the thread (start AND resume) rather than the daemon process.
4988
+ const sessionToken = threadId ? this.ensureThreadCredential(threadId) : undefined;
4610
4989
  return {
4611
4990
  shell_environment_policy: {
4612
4991
  set: {
@@ -4617,6 +4996,7 @@ export class CodexRunner {
4617
4996
  ...(taskDelegationToken || delegationCarrier
4618
4997
  ? { [AGENT_DELEGATION_TOKEN_ENV]: taskDelegationToken ?? delegationCarrier }
4619
4998
  : {}),
4999
+ ...(sessionToken ? { [CODEX_SESSION_TOKEN_ENV]: sessionToken } : {}),
4620
5000
  ...(managedTempDir ? { TMPDIR: managedTempDir } : {}),
4621
5001
  },
4622
5002
  },
@@ -4672,6 +5052,7 @@ export class CodexRunner {
4672
5052
  this.appServerClient?.close(),
4673
5053
  this.fullAccessAppServerClient?.close(),
4674
5054
  ]);
5055
+ this.codexSessionFileAdapter.close();
4675
5056
  this.appServerClient = null;
4676
5057
  this.fullAccessAppServerClient = null;
4677
5058
  }
@@ -4682,8 +5063,6 @@ export class CodexAgentPlugin {
4682
5063
  isEnabled(agent) {
4683
5064
  if (!agent.config.baseagents?.codex)
4684
5065
  return false;
4685
- if (!isCodexAppServerAvailable())
4686
- return false;
4687
5066
  try {
4688
5067
  const override = agent.config.baseagents.codex;
4689
5068
  const syntheticConfig = { agents: { codex: override } };
@@ -4694,8 +5073,8 @@ export class CodexAgentPlugin {
4694
5073
  return false;
4695
5074
  }
4696
5075
  }
4697
- createAgent(agent, callbacks) {
4698
- const availability = getCodexAppServerAvailability();
5076
+ async createAgent(agent, callbacks) {
5077
+ const availability = await getCodexAppServerAvailabilityAsync();
4699
5078
  if (!availability.available) {
4700
5079
  throw new Error(availability.reason || 'Missing codex CLI with app-server');
4701
5080
  }