evolcore 0.0.26 → 0.0.28

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 (177) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/bin/codex-managed-acl.ps1 +43 -0
  3. package/dist/agents/baseagent-availability.js +68 -0
  4. package/dist/agents/baseagent.js +10 -5
  5. package/dist/agents/claude-runner.js +123 -22
  6. package/dist/agents/codex/app-server-client.js +70 -26
  7. package/dist/agents/codex/catalog.js +7 -1
  8. package/dist/agents/codex-runner.js +363 -421
  9. package/dist/agents/ecagent-runner.js +170 -25
  10. package/dist/agents/gemini-runner.js +6 -0
  11. package/dist/agents/runner-types.js +50 -2
  12. package/dist/aun/aid/agentmd.js +73 -31
  13. package/dist/aun/aid/client.js +1 -1
  14. package/dist/aun/aid/control-aid.js +180 -1
  15. package/dist/aun/aid/identity.js +3 -3
  16. package/dist/aun/group/history.js +303 -0
  17. package/dist/aun/group/identity.js +43 -0
  18. package/dist/aun/group/index.js +5 -0
  19. package/dist/aun/{msg/group.js → group/operations.js} +161 -38
  20. package/dist/aun/{menu-client.js → menu/client.js} +13 -7
  21. package/dist/aun/menu/index.js +2 -0
  22. package/dist/aun/menu/transport.js +49 -0
  23. package/dist/aun/msg/index.js +2 -1
  24. package/dist/aun/msg/managed-operation.js +84 -18
  25. package/dist/aun/msg/mention-schema.js +51 -0
  26. package/dist/aun/msg/p2p.js +13 -8
  27. package/dist/aun/msg/upload.js +43 -88
  28. package/dist/aun/outbox.js +29 -7
  29. package/dist/aun/rpc/client.js +1 -0
  30. package/dist/aun/storage/client.js +39 -50
  31. package/dist/channels/aun.js +544 -239
  32. package/dist/channels/daemon.js +10 -6
  33. package/dist/cli/agent-command.js +108 -6
  34. package/dist/cli/agent-create.js +515 -2
  35. package/dist/cli/agent.js +109 -8
  36. package/dist/cli/aun-commands.js +191 -42
  37. package/dist/cli/config.js +8 -5
  38. package/dist/cli/contact.js +2 -2
  39. package/dist/cli/ctl-command.js +2 -2
  40. package/dist/cli/daemon-commands.js +111 -162
  41. package/dist/cli/diag.js +3 -3
  42. package/dist/cli/fs-command.js +3 -3
  43. package/dist/cli/handoff-command.js +6 -6
  44. package/dist/cli/index.js +8 -2
  45. package/dist/cli/init.js +34 -1
  46. package/dist/cli/managed-command-guard.js +2 -1
  47. package/dist/cli/menu-client.js +13 -5
  48. package/dist/cli/model.js +3 -3
  49. package/dist/cli/queue-command.js +2 -2
  50. package/dist/cli/response.js +2 -2
  51. package/dist/cli/restart-monitor.js +81 -19
  52. package/dist/cli/task-command.js +59 -0
  53. package/dist/cli/task-context.js +97 -49
  54. package/dist/cli/trigger-command.js +20 -10
  55. package/dist/config/builtin-roles.js +10 -0
  56. package/dist/config/config-field-policy.js +11 -0
  57. package/dist/config/config-manager.js +32 -4
  58. package/dist/config/config-operation-service.js +20 -5
  59. package/dist/config/contact-book-store.js +1 -1
  60. package/dist/config/contact-book.js +1 -1
  61. package/dist/config/contact-operation-service.js +1 -1
  62. package/dist/config/peer-role-resolver.js +1 -1
  63. package/dist/config/resolved-config-op.js +3 -0
  64. package/dist/config-store.js +1 -1
  65. package/dist/core/agent-application-service.js +5 -0
  66. package/dist/core/audit/event-key.js +17 -2
  67. package/dist/core/audit/log-integrity.js +34 -8
  68. package/dist/core/auth/agent-delegation.js +48 -14
  69. package/dist/core/auth/auth-gateway.js +2 -0
  70. package/dist/core/auth/authorization-audit.js +34 -38
  71. package/dist/core/auth/operation-authorizer.js +67 -5
  72. package/dist/core/auth/operation-catalog.js +110 -0
  73. package/dist/core/auth/remote-access-store.js +115 -9
  74. package/dist/core/bootstrap-messages.js +66 -7
  75. package/dist/core/bootstrap-service.js +110 -19
  76. package/dist/core/command/agent-control.js +88 -7
  77. package/dist/core/command/agent-storage-menu.js +443 -0
  78. package/dist/core/command/cli-intent-parser.js +19 -0
  79. package/dist/core/command/command-handler.js +105 -40
  80. package/dist/core/command/data-root-resolver.js +273 -0
  81. package/dist/core/command/diag-service.js +292 -27
  82. package/dist/core/command/group-menu.js +7 -2
  83. package/dist/core/command/menu-catalog.js +34 -4
  84. package/dist/core/command/menu-handler.js +364 -96
  85. package/dist/core/command/menu-protocol.js +43 -6
  86. package/dist/core/command/role-menu.js +2 -2
  87. package/dist/core/command/slash-handler.js +61 -37
  88. package/dist/core/event-catalog.js +4 -0
  89. package/dist/core/evolagent-registry.js +28 -2
  90. package/dist/core/group/invite-service.js +924 -0
  91. package/dist/core/interaction-router.js +22 -8
  92. package/dist/core/message/im-renderer.js +94 -16
  93. package/dist/core/message/message-bridge.js +140 -13
  94. package/dist/core/message/message-log.js +5 -3
  95. package/dist/core/message/message-queue.js +73 -41
  96. package/dist/core/message/response-engine.js +729 -118
  97. package/dist/core/permission/approval-gateway.js +36 -3
  98. package/dist/core/permission/ec-command-parser.js +76 -36
  99. package/dist/core/permission/protected-paths.js +134 -16
  100. package/dist/core/permission/sandbox-runtime.js +72 -3
  101. package/dist/core/permission/tool-error-code.js +15 -0
  102. package/dist/core/permission/tool-policy.js +8 -8
  103. package/dist/core/relation/peer-identity.js +57 -6
  104. package/dist/core/relation/shared-data.js +106 -0
  105. package/dist/core/role/runtime-policy.js +64 -0
  106. package/dist/core/runtime-lock.js +10 -26
  107. package/dist/core/session/runner-task-history.js +170 -0
  108. package/dist/core/session/session-manager.js +38 -6
  109. package/dist/core/session/session-mapper.js +41 -0
  110. package/dist/core/session/session-turn-coordinator.js +37 -7
  111. package/dist/core/session/session-turns.js +48 -7
  112. package/dist/core/task/task-recovery-service.js +176 -0
  113. package/dist/core/task/task-resume-operation.js +32 -0
  114. package/dist/eck/group-rules-sync.js +5 -3
  115. package/dist/eck/kit-renderer.js +1 -0
  116. package/dist/index.js +322 -207
  117. package/dist/ipc.js +141 -15
  118. package/dist/paths.js +17 -4
  119. package/dist/product.js +3 -0
  120. package/dist/trigger/event-source.js +19 -0
  121. package/dist/trigger/feedback.js +233 -40
  122. package/dist/trigger/parser.js +55 -5
  123. package/dist/trigger/patch.js +9 -0
  124. package/dist/trigger/scheduler.js +30 -4
  125. package/dist/trigger/validation.js +20 -3
  126. package/dist/utils/atomic-write.js +98 -0
  127. package/dist/utils/cross-platform.js +87 -16
  128. package/dist/utils/ecweb-utils.js +1 -0
  129. package/dist/utils/error-utils.js +40 -2
  130. package/dist/utils/logger.js +23 -13
  131. package/dist/utils/npm-ops.js +119 -4
  132. package/dist/utils/package-version-info.js +244 -0
  133. package/dist/utils/tool-summary.js +59 -3
  134. package/dist/utils/welcome.js +174 -1
  135. package/dist/utils/windows-shell-trust.js +37 -0
  136. package/ecagent/dist/harness/agent-harness.js +3 -0
  137. package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
  138. package/kits/docs/INDEX.md +0 -6
  139. package/kits/docs/channels/aun.md +23 -2
  140. package/kits/docs/channels/feishu.md +1 -1
  141. package/kits/docs/context-assembly.md +29 -17
  142. package/kits/docs/evolcore/INDEX.md +8 -3
  143. package/kits/docs/evolcore/agent-create.md +39 -7
  144. package/kits/docs/evolcore/agent.md +33 -1
  145. package/kits/docs/evolcore/aid.md +9 -1
  146. package/kits/docs/evolcore/config.md +40 -1
  147. package/kits/docs/evolcore/group-collaboration.md +1 -1
  148. package/kits/docs/evolcore/group.md +117 -2
  149. package/kits/docs/evolcore/menu-client.md +16 -0
  150. package/kits/docs/evolcore/msg.md +3 -0
  151. package/kits/docs/evolcore/storage.md +3 -0
  152. package/kits/docs/evolcore/task.md +14 -0
  153. package/kits/docs/evolcore/trigger.md +51 -3
  154. package/kits/docs/venues/group.md +5 -0
  155. package/kits/eck_manifest.json +27 -0
  156. package/kits/rules/01-overview.md +11 -7
  157. package/kits/rules/04-relation.md +2 -1
  158. package/kits/rules/05-venue.md +1 -1
  159. package/kits/rules/06-channel.md +6 -4
  160. package/kits/schemas/agent-config.schema.12.json +25 -1
  161. package/kits/schemas/daemon.schema.6.json +2 -2
  162. package/kits/schemas/defaults.schema.5.json +1 -1
  163. package/kits/schemas/relation-config.schema.1.json +1 -1
  164. package/kits/schemas/relation-config.schema.5.json +1 -1
  165. package/kits/schemas/role-config.schema.1.json +14 -0
  166. package/kits/templates/roles/member.json +5 -0
  167. package/kits/templates/roles/visitor.json +5 -0
  168. package/kits/templates/system-fragments/baseagent.md +1 -1
  169. package/kits/templates/system-fragments/bootstrap.md +26 -9
  170. package/kits/templates/system-fragments/channel.md +8 -44
  171. package/kits/templates/system-fragments/commands.md +10 -2
  172. package/kits/templates/system-fragments/group-unread-history.md +7 -0
  173. package/kits/templates/system-fragments/reply.md +56 -0
  174. package/package.json +7 -3
  175. package/dist/aun/group-identity.js +0 -9
  176. /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
  177. /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
@@ -1,13 +1,13 @@
1
1
  import { getRunnerCatalog, catalogEfforts, assertCatalogSelectable } from '../core/model/model-catalog.js';
2
2
  // Codex/OpenAI API 不认识 Claude Code SDK 的窗口后缀,必须传入基础模型 ID。
3
3
  import { stripContextWindowSuffix } from '../core/model/context-window-suffix.js';
4
- import { hasTrustedFullAccessContext } from './runner-types.js';
4
+ import { formatAsyncQuestionAnswer, hasTrustedFullAccessContext, parseAsyncUserQuestions, unsettledInterruptError, withInterruptDeadline } from './runner-types.js';
5
5
  import { requestDangerousCommandPermission } from '../core/permission/approval-gateway.js';
6
6
  import { checkReadonly, checkDangerousCommand, evaluateToolPreflight } from '../core/permission/tool-policy.js';
7
7
  import { normalizeExecutionPermissionMode, normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
8
- import { buildCodexProtectedFilesystemRules, isSameOrDescendant, resolveProtectedCandidate } from '../core/permission/protected-paths.js';
8
+ import { assertWindowsCodexFilesystemRuleBudget, buildCodexProtectedFilesystemRules, isSameOrDescendant, resolveProtectedCandidate, } from '../core/permission/protected-paths.js';
9
9
  import { CODEX_THREAD_ACTIVE_WRITER, CodexAppServerClient, } from './codex/app-server-client.js';
10
- import { resolveOpenaiConfig, resolveCodexCliPath } from './baseagent.js';
10
+ import { resolveOpenaiConfig, resolveCodexCliPath, resolveCodexPowerShellShimPath, } from './baseagent.js';
11
11
  import { logger } from '../utils/logger.js';
12
12
  import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
13
13
  import { auditCodexApprovalDecision, auditPermissionLifecycle, auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
@@ -15,12 +15,12 @@ import { classifyError, ErrorType, isOutputTokenLimitError, isRetryableError } f
15
15
  import { renderActionAsText } from '../core/interaction-router.js';
16
16
  import { buildEnvelope, isInteractionSendAccepted, sendInteractionPayload } from '../core/message/message-utils.js';
17
17
  import { resolveCodexCapabilityThreadConfigForProject } from '../core/capability/capability-manager.js';
18
- import { AGENT_DELEGATION_TOKEN_ENV, hashDelegatedCommandArgv } from '../core/auth/agent-delegation.js';
18
+ import { AGENT_DELEGATION_TOKEN_ENV, CODEX_SESSION_TOKEN_ENV, hashDelegatedCommandArgv } from '../core/auth/agent-delegation.js';
19
19
  import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
20
- import { classifyEvolcoreShellCommand, containsLiteralManagedTmpDirOutsideSendContent, hasCodexCmdCarrierEcIntent, hasPowerShellDelegationAssignment, normalizeEvolcoreCommandArgv, parseCodexToolCommand, parseCodexCarrierCommand, parseLiteralShellCommand, parseCodexShellCommandArgv, resolveCodexShellCarrierArgv, } from '../core/permission/ec-command-parser.js';
20
+ import { classifyEvolcoreShellCommand, containsLiteralManagedTmpDirOutsideSendContent, hasCodexCmdCarrierEcIntent, hasPowerShellCredentialAssignment, normalizeEvolcoreCommandArgv, parseCodexToolCommand, parseCodexCarrierCommand, parseLiteralShellCommand, parseCodexShellCommandArgv, resolveCodexShellCarrierArgv, } from '../core/permission/ec-command-parser.js';
21
21
  import { compareVersions } from '../utils/npm-ops.js';
22
22
  import { agentSessionsDir, resolvePaths, resolveRoot } from '../paths.js';
23
- import { ensureProcessManagedTempDir, getManagedTaskTempDir } from '../cli/task-context.js';
23
+ import { ensureSessionRuntimeDir, getManagedTaskTempDir } from '../cli/task-context.js';
24
24
  import { buildSessionTurnList } from '../core/session/session-turns.js';
25
25
  import { CodexSessionFileAdapter } from '../core/session/adapters/codex-session-file-adapter.js';
26
26
  import { execFileSync } from 'child_process';
@@ -38,6 +38,7 @@ const MIME_EXT = {
38
38
  'image/webp': '.webp',
39
39
  };
40
40
  const APPROVAL_LOG_SUMMARY_MAX_LENGTH = 256;
41
+ const CODEX_INTERRUPT_TIMEOUT_MS = 5_000;
41
42
  function toAgentErrorType(message) {
42
43
  switch (classifyError(new Error(message))) {
43
44
  case ErrorType.CONTEXT_TOO_LONG: return 'context_too_long';
@@ -108,60 +109,6 @@ function isCodexReasoningNotification(method) {
108
109
  || method === 'item/reasoning/summaryPartAdded';
109
110
  }
110
111
  const CODEX_THREAD_HANDOFF_TIMEOUT_MS = 2_000;
111
- const managedTempDirsForExitCleanup = new Map();
112
- let managedTempExitCleanupRegistered = false;
113
- function isTrustedManagedTempDirectory(directory, parent) {
114
- try {
115
- const parentPath = path.resolve(parent);
116
- const directoryPath = path.resolve(directory);
117
- // The session directory must be a direct child of the private parent.
118
- // This lexical check is required before canonicalization so a replaced
119
- // parent symlink cannot turn an old session path into another tree.
120
- if (path.dirname(directoryPath) !== parentPath)
121
- return false;
122
- const trustedDirectory = (candidate) => {
123
- const stat = fs.lstatSync(candidate);
124
- if (!stat.isDirectory() || stat.isSymbolicLink())
125
- return false;
126
- if (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid())
127
- return false;
128
- return process.platform === 'win32' || (stat.mode & 0o077) === 0;
129
- };
130
- if (!trustedDirectory(parentPath) || !trustedDirectory(directoryPath))
131
- return false;
132
- const canonicalParent = resolveProtectedCandidate(parentPath);
133
- const canonicalDirectory = resolveProtectedCandidate(directoryPath);
134
- // Keep the canonical and lexical paths identical after the lstat checks;
135
- // this rejects symlink replacement in either node or an existing ancestor.
136
- return canonicalParent === parentPath
137
- && canonicalDirectory === directoryPath
138
- && canonicalDirectory !== canonicalParent
139
- && path.dirname(canonicalDirectory) === canonicalParent
140
- && isSameOrDescendant(canonicalDirectory, canonicalParent);
141
- }
142
- catch {
143
- return false;
144
- }
145
- }
146
- function registerManagedTempDirForExitCleanup(directory, parent) {
147
- managedTempDirsForExitCleanup.set(directory, parent);
148
- if (managedTempExitCleanupRegistered)
149
- return;
150
- managedTempExitCleanupRegistered = true;
151
- process.once('exit', () => {
152
- for (const [candidatePath, parentPath] of managedTempDirsForExitCleanup) {
153
- try {
154
- if (isTrustedManagedTempDirectory(candidatePath, parentPath)) {
155
- fs.rmSync(path.resolve(candidatePath), { recursive: true, force: true });
156
- }
157
- }
158
- catch {
159
- // Process exit cleanup is best effort; session cleanup logs failures
160
- // while the runner is still active.
161
- }
162
- }
163
- });
164
- }
165
112
  function stableSecurityValue(value) {
166
113
  if (value === undefined)
167
114
  return 'undefined';
@@ -234,7 +181,6 @@ function outputTextForManagedHook(value) {
234
181
  }
235
182
  return String(value);
236
183
  }
237
- const CODEX_DELEGATION_CARRIER_METADATA_KEY = '__codexDelegationCarrierV1';
238
184
  const CODEX_NO_ROLLOUT_RE = /no rollout found for thread id/i;
239
185
  const CODEX_ACTIVE_WRITER_RE = /already has an active writer/i;
240
186
  function isCodexNoRolloutError(error) {
@@ -266,10 +212,13 @@ function compactCodexProbeText(value, maxLength = 600) {
266
212
  const text = value.replace(/\s+/gu, ' ').trim();
267
213
  return text.length <= maxLength ? text : `${text.slice(0, maxLength - 3).trimEnd()}...`;
268
214
  }
269
- function logCodexProbeFailure(args, startedAt, error, timeoutMs) {
215
+ function logCodexProbeFailure(args, startedAt, error, timeoutMs, launch) {
270
216
  const detail = error;
271
217
  logger.warn(`[CodexProbe] CLI probe failed: ${JSON.stringify({
272
218
  command: ['codex', ...args],
219
+ resolvedPath: launch?.resolvedPath ?? null,
220
+ launcherKind: launch?.launcherKind ?? null,
221
+ launcherPath: launch?.launcherPath ?? null,
273
222
  path: detail?.path ?? null,
274
223
  spawnargs: detail?.spawnargs ?? null,
275
224
  elapsedMs: Date.now() - startedAt,
@@ -283,13 +232,13 @@ function logCodexProbeFailure(args, startedAt, error, timeoutMs) {
283
232
  stderr: compactCodexProbeText(detail?.stderr),
284
233
  })}`);
285
234
  }
286
- function probeCodexCli(args) {
287
- return probeCliSync('codex', args, { onFailure: logCodexProbeFailure });
235
+ function probeCodexCli(args, executable) {
236
+ return probeCliSync('codex', args, { executable, onFailure: logCodexProbeFailure });
288
237
  }
289
- export function getCodexCliVersion() {
238
+ export function getCodexCliVersion(executable) {
290
239
  const startedAt = Date.now();
291
240
  try {
292
- const output = probeCodexCli(['--version']);
241
+ const output = probeCodexCli(['--version'], executable);
293
242
  const version = parseCodexCliVersion(output);
294
243
  if (!version) {
295
244
  logCodexProbeFailure(['--version'], startedAt, {
@@ -308,9 +257,15 @@ export function getCodexCliVersion() {
308
257
  export function getCodexAppServerAvailability() {
309
258
  const cliPath = resolveCodexCliPath();
310
259
  if (!cliPath) {
311
- return { available: false, reason: '未检测到 Codex CLI。' };
260
+ const powershellShim = resolveCodexPowerShellShimPath();
261
+ return {
262
+ available: false,
263
+ reason: powershellShim
264
+ ? '检测到 codex.ps1,但当前 Windows 启动路径要求 codex.exe/codex.cmd/codex.bat;请重新安装 @openai/codex 以恢复 npm shim。'
265
+ : '未检测到 Codex CLI。',
266
+ };
312
267
  }
313
- const version = getCodexCliVersion();
268
+ const version = getCodexCliVersion(cliPath);
314
269
  if (!version) {
315
270
  return { available: false, reason: '无法读取 Codex CLI 版本。' };
316
271
  }
@@ -323,7 +278,7 @@ export function getCodexAppServerAvailability() {
323
278
  };
324
279
  }
325
280
  try {
326
- probeCodexCli(['app-server', '--help']);
281
+ probeCodexCli(['app-server', '--help'], cliPath);
327
282
  return { available: true, version };
328
283
  }
329
284
  catch (error) {
@@ -338,10 +293,21 @@ export function isCodexAppServerAvailable() {
338
293
  }
339
294
  /** Daemon startup/hot reload use asynchronous probes; sync CLI callers share the cache. */
340
295
  export async function getCodexAppServerAvailabilityAsync() {
296
+ const cliPath = resolveCodexCliPath();
341
297
  const options = {
342
298
  timeouts: [CODEX_CLI_PROBE_TIMEOUT_MS, CODEX_CLI_PROBE_RETRY_TIMEOUT_MS],
299
+ executable: cliPath ?? undefined,
343
300
  onFailure: logCodexProbeFailure,
344
301
  };
302
+ if (!cliPath) {
303
+ const powershellShim = resolveCodexPowerShellShimPath();
304
+ return {
305
+ available: false,
306
+ reason: powershellShim
307
+ ? '检测到 codex.ps1,但当前 Windows 启动路径要求 codex.exe/codex.cmd/codex.bat;请重新安装 @openai/codex 以恢复 npm shim。'
308
+ : '未检测到 Codex CLI。',
309
+ };
310
+ }
345
311
  let version;
346
312
  try {
347
313
  version = parseCodexCliVersion(await probeCli('codex', ['--version'], options)) ?? undefined;
@@ -361,7 +327,6 @@ export async function getCodexAppServerAvailabilityAsync() {
361
327
  : `Codex CLI 探测失败:${error instanceof Error ? error.message : String(error)}` };
362
328
  }
363
329
  }
364
- export const CODEX_SESSION_TOKEN_ENV = 'EVOLCORE_CODEX_SESSION_TOKEN';
365
330
  export class CodexRunner {
366
331
  name = 'codex';
367
332
  // turn/start persists a new user turn before runQuery resolves. App-server
@@ -380,14 +345,12 @@ export class CodexRunner {
380
345
  threadProjectPaths = new Map();
381
346
  /** Child Codex threads inherit the EvolCore session that started them. */
382
347
  childThreadSessions = new Map();
383
- sessionTempDirs = new Map();
384
- managedTempNonce = randomBytes(16).toString('hex');
385
348
  threadExternalToolFingerprints = new Map();
386
349
  threadExternalToolBoundaries = new Map();
387
350
  trackedApprovalItems = new Map();
388
351
  pendingToolDenials = new Map();
389
- threadDelegationCarriers = new Map();
390
352
  pendingInterrupts = new Map();
353
+ unconfirmedInterrupts = new Map();
391
354
  steeringTails = new Map();
392
355
  threadOperationLocks = new Map();
393
356
  activeOperationReleases = new Map();
@@ -405,11 +368,15 @@ export class CodexRunner {
405
368
  /** Fallback authorization lifecycle ids for app-server requests without an RPC id. */
406
369
  approvalLifecycleIds = new WeakMap();
407
370
  resolvedConfig;
371
+ codexVersion;
408
372
  pendingInterruptTtlMs = 30_000;
409
373
  /** Fallback for direct runner callers that do not inject SessionManager. */
410
374
  codexSessionFileAdapter = new CodexSessionFileAdapter();
411
375
  /** threadId → managed-hook credential, issued by this runner's agent only. */
412
376
  threadCredentials = new Map();
377
+ /** @deprecated Compatibility alias for direct runner integrations. */
378
+ threadDelegationCarriers = this.threadCredentials;
379
+ asyncQuestionItems = new Set();
413
380
  constructor(config, callbacks, runtime) {
414
381
  // Compatibility for legacy direct callers that still pass the removed
415
382
  // internal fields in a synthetic config. AgentLoader uses runtime binding.
@@ -426,6 +393,7 @@ export class CodexRunner {
426
393
  configuredHeaders: this.resolvedConfig.headers,
427
394
  });
428
395
  this.capabilities = {
396
+ taskRecovery: true,
429
397
  clear: false,
430
398
  compact: true,
431
399
  fork: true,
@@ -442,6 +410,7 @@ export class CodexRunner {
442
410
  if (this.resolvedConfig.effort)
443
411
  this.effort = this.resolvedConfig.effort;
444
412
  this.onSessionIdUpdate = callbacks.onSessionIdUpdate;
413
+ this.codexVersion = runtime?.codexVersion;
445
414
  }
446
415
  getAppServerClient(fullAccess = false) {
447
416
  const existing = fullAccess ? this.fullAccessAppServerClient : this.appServerClient;
@@ -449,6 +418,7 @@ export class CodexRunner {
449
418
  return existing;
450
419
  const client = new CodexAppServerClient({
451
420
  apiKey: this.resolvedConfig.apiKey,
421
+ codexVersion: this.codexVersion,
452
422
  authType: this.resolvedConfig.authType,
453
423
  baseUrl: this.resolvedConfig.baseUrl,
454
424
  provider: this.resolvedConfig.provider,
@@ -642,16 +612,13 @@ export class CodexRunner {
642
612
  }
643
613
  this.threadSubscriptions.set(threadId, { client: targetClient, state: 'subscribed' });
644
614
  }
645
- createDelegationCarrier() {
646
- return randomBytes(32).toString('base64url');
647
- }
648
- approvedLiteralEvolcoreCommand(command, managedTempDir, dialect = 'posix', expectedDelegationToken, executable) {
615
+ approvedLiteralEvolcoreCommand(command, managedTempDir, dialect = 'posix', expectedCredentialToken, executable) {
649
616
  const parseOptions = {
650
617
  allowManagedTmpDir: true,
651
618
  dialect,
652
619
  verifyShellExecutable: process.platform === 'win32',
653
620
  ...(managedTempDir ? { managedTempDir } : {}),
654
- ...(expectedDelegationToken !== undefined ? { expectedDelegationToken } : {}),
621
+ ...(expectedCredentialToken !== undefined ? { expectedCredentialToken } : {}),
655
622
  };
656
623
  const classification = classifyEvolcoreShellCommand(command, parseOptions);
657
624
  if (classification.kind === 'bounded-output')
@@ -678,10 +645,10 @@ export class CodexRunner {
678
645
  }
679
646
  return {
680
647
  argv: normalizeEvolcoreCommandArgv(parsed.argv),
681
- ...(parsed.delegationToken ? { delegationToken: parsed.delegationToken } : {}),
648
+ ...(parsed.credentialToken ? { credentialToken: parsed.credentialToken } : {}),
682
649
  };
683
650
  }
684
- resolveCanonicalEcApproval(toolInput, managedTempDir, expectedDelegationToken) {
651
+ resolveCanonicalEcApproval(toolInput, managedTempDir, expectedCredentialToken) {
685
652
  const explicitArgv = Array.isArray(toolInput.commandArgv)
686
653
  && toolInput.commandArgv.every(value => typeof value === 'string')
687
654
  ? toolInput.commandArgv
@@ -702,12 +669,12 @@ export class CodexRunner {
702
669
  ? resolveCodexShellCarrierArgv(explicitArgv)
703
670
  : undefined;
704
671
  if (explicitCarrier !== undefined) {
705
- return this.approvedLiteralEvolcoreCommand(explicitCarrier.command, managedTempDir, explicitCarrier.dialect, expectedDelegationToken, explicitCarrier.executable);
672
+ return this.approvedLiteralEvolcoreCommand(explicitCarrier.command, managedTempDir, explicitCarrier.dialect, expectedCredentialToken, explicitCarrier.executable);
706
673
  }
707
674
  if (hasCodexCmdCarrierEcIntent(toolInput)) {
708
675
  const parsed = parseCodexToolCommand(toolInput, {
709
676
  verifyShellExecutable: process.platform === 'win32',
710
- ...(expectedDelegationToken !== undefined ? { expectedDelegationToken } : {}),
677
+ ...(expectedCredentialToken !== undefined ? { expectedCredentialToken } : {}),
711
678
  });
712
679
  if (!parsed.ok)
713
680
  return { parseFailure: parsed };
@@ -719,7 +686,7 @@ export class CodexRunner {
719
686
  } }
720
687
  : {
721
688
  argv: normalizeEvolcoreCommandArgv(parsed.argv),
722
- ...(parsed.delegationToken ? { delegationToken: parsed.delegationToken } : {}),
689
+ ...(parsed.credentialToken ? { credentialToken: parsed.credentialToken } : {}),
723
690
  };
724
691
  }
725
692
  const command = typeof toolInput.command === 'string' ? toolInput.command : '';
@@ -739,7 +706,7 @@ export class CodexRunner {
739
706
  allowManagedTmpDir: true,
740
707
  verifyShellExecutable: process.platform === 'win32',
741
708
  ...(managedTempDir ? { managedTempDir } : {}),
742
- ...(expectedDelegationToken !== undefined ? { expectedDelegationToken } : {}),
709
+ ...(expectedCredentialToken !== undefined ? { expectedCredentialToken } : {}),
743
710
  });
744
711
  if (canonicalCommand?.[0] === 'ec') {
745
712
  return containsLiteralManagedTmpDirOutsideSendContent(canonicalCommand)
@@ -774,7 +741,7 @@ export class CodexRunner {
774
741
  return true;
775
742
  const shellCarrier = resolveCodexShellCarrierArgv(rawArgv);
776
743
  if (shellCarrier?.dialect === 'powershell'
777
- && hasPowerShellDelegationAssignment(shellCarrier.command))
744
+ && hasPowerShellCredentialAssignment(shellCarrier.command))
778
745
  return true;
779
746
  return hasCodexCmdCarrierEcIntent(toolInput)
780
747
  || shellCarrier !== undefined
@@ -787,20 +754,20 @@ export class CodexRunner {
787
754
  }
788
755
  armApprovedDelegationCommand(sessionId, argv) {
789
756
  const threadId = this.activeSessions.get(sessionId) ?? this.provisionalSessions.get(sessionId);
790
- const carrierToken = threadId ? this.threadDelegationCarriers.get(threadId) : undefined;
757
+ const sessionToken = threadId ? this.ensureThreadCredentialLoaded(threadId) : undefined;
791
758
  const commandHash = hashDelegatedCommandArgv(argv);
792
759
  const arm = this.permissionContexts.get(sessionId)?.armApprovedDelegationCommand;
793
- if (!carrierToken || !commandHash || !arm)
760
+ if (!sessionToken || !commandHash || !arm)
794
761
  return false;
795
762
  try {
796
- return arm(carrierToken, commandHash);
763
+ return arm(sessionToken, commandHash);
797
764
  }
798
765
  catch (error) {
799
766
  logger.warn(`[CodexRunner] delegation arm callback failed: session=${sessionId} error=${error instanceof Error ? error.message : String(error)}`);
800
767
  return false;
801
768
  }
802
769
  }
803
- delegationCarrierForSession(sessionId, threadId) {
770
+ sessionTokenForSession(sessionId, threadId) {
804
771
  const candidates = [
805
772
  threadId,
806
773
  this.activeSessions.get(sessionId),
@@ -809,45 +776,13 @@ export class CodexRunner {
809
776
  for (const candidate of candidates) {
810
777
  if (!candidate)
811
778
  continue;
812
- const carrier = this.threadDelegationCarriers.get(candidate);
813
- if (carrier)
814
- return carrier;
779
+ const token = this.ensureThreadCredentialLoaded(candidate);
780
+ if (token)
781
+ return token;
815
782
  }
816
783
  return undefined;
817
784
  }
818
- async readThreadDelegationCarrier(appServer, threadId) {
819
- try {
820
- const result = await appServer.threadShellCommand(threadId, `node -e "process.stdout.write(process.env.${AGENT_DELEGATION_TOKEN_ENV}||String())"`);
821
- const carrier = result.output.trim();
822
- if (result.exitCode !== undefined && result.exitCode !== 0)
823
- return undefined;
824
- return /^[A-Za-z0-9_-]{32,128}$/.test(carrier) ? carrier : undefined;
825
- }
826
- catch (error) {
827
- logger.warn(`[CodexRunner] failed to recover delegation carrier for loaded thread ${threadId}: ${error instanceof Error ? error.message : String(error)}`);
828
- return undefined;
829
- }
830
- }
831
- async loadPersistedDelegationCarrier(sessionManager, sessionId, threadId) {
832
- if (typeof sessionManager?.getSessionById !== 'function')
833
- return undefined;
834
- try {
835
- const session = await sessionManager.getSessionById(sessionId);
836
- const persisted = session?.metadata?.[CODEX_DELEGATION_CARRIER_METADATA_KEY];
837
- if (!persisted || typeof persisted !== 'object' || Array.isArray(persisted))
838
- return undefined;
839
- if (persisted.threadId !== threadId || typeof persisted.carrier !== 'string')
840
- return undefined;
841
- return /^[A-Za-z0-9_-]{32,128}$/.test(persisted.carrier)
842
- ? persisted.carrier
843
- : undefined;
844
- }
845
- catch (error) {
846
- logger.debug(`[CodexRunner] failed to load persisted delegation carrier: session=${sessionId} error=${error}`);
847
- return undefined;
848
- }
849
- }
850
- async commitProvisionalThread(sessionId, threadId, carrierToPublish, sessionManager, turn) {
785
+ async commitProvisionalThread(sessionId, threadId, turn) {
851
786
  if (this.provisionalSessions.get(sessionId) !== threadId)
852
787
  return false;
853
788
  const bindingResult = this.onSessionIdUpdate
@@ -871,10 +806,6 @@ export class CodexRunner {
871
806
  return false;
872
807
  this.provisionalSessions.delete(sessionId);
873
808
  this.activeSessions.set(sessionId, threadId);
874
- if (carrierToPublish) {
875
- this.threadDelegationCarriers.set(carrierToPublish.threadId, carrierToPublish.carrier);
876
- await this.persistDelegationCarrier(sessionManager, sessionId, carrierToPublish.threadId, carrierToPublish.carrier);
877
- }
878
809
  this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${threadId}`);
879
810
  return true;
880
811
  }
@@ -907,9 +838,9 @@ export class CodexRunner {
907
838
  return;
908
839
  this.provisionalSessions.delete(sessionId);
909
840
  this.threadProjectPaths.delete(threadId);
910
- this.threadDelegationCarriers.delete(threadId);
911
841
  this.unbindExternalToolBoundary(threadId);
912
842
  this.clearTrackedApprovalItemsForThread(threadId);
843
+ this.dropThreadCredential(threadId);
913
844
  // A stale provisional start can finish after a replacement run has
914
845
  // already activated another parent thread for this session. Do not tear
915
846
  // down the replacement run's child mappings in that race.
@@ -919,31 +850,6 @@ export class CodexRunner {
919
850
  }
920
851
  this.pruneThreadOperationLocks();
921
852
  }
922
- async persistDelegationCarrier(sessionManager, sessionId, threadId, carrier) {
923
- if (typeof sessionManager?.getSessionById !== 'function')
924
- return;
925
- try {
926
- const session = await sessionManager.getSessionById(sessionId);
927
- if (!session)
928
- return;
929
- const current = session.metadata?.[CODEX_DELEGATION_CARRIER_METADATA_KEY];
930
- if (current?.threadId === threadId && current?.carrier === carrier)
931
- return;
932
- if (typeof sessionManager.patchSessionMetadata === 'function') {
933
- await sessionManager.patchSessionMetadata(sessionId, {
934
- [CODEX_DELEGATION_CARRIER_METADATA_KEY]: { threadId, carrier },
935
- });
936
- }
937
- else if (typeof sessionManager.updateSession === 'function') {
938
- await sessionManager.updateSession(sessionId, {
939
- metadata: { [CODEX_DELEGATION_CARRIER_METADATA_KEY]: { threadId, carrier } },
940
- });
941
- }
942
- }
943
- catch (error) {
944
- logger.warn(`[CodexRunner] failed to persist delegation carrier: session=${sessionId} thread=${threadId} error=${error}`);
945
- }
946
- }
947
853
  resetAppServerClient() {
948
854
  const clients = [this.appServerClient, this.fullAccessAppServerClient]
949
855
  .filter((client) => !!client);
@@ -952,8 +858,8 @@ export class CodexRunner {
952
858
  this.threadSubscriptions.clear();
953
859
  this.threadExternalToolFingerprints.clear();
954
860
  this.threadExternalToolBoundaries.clear();
955
- for (const childThreadId of this.childThreadSessions.keys())
956
- this.threadProjectPaths.delete(childThreadId);
861
+ for (const childThreadId of [...this.childThreadSessions.keys()])
862
+ this.unregisterChildThread(childThreadId);
957
863
  // Child turn ids belong to the discarded app-server connection. Keeping
958
864
  // them would make a later session interrupt attempt target stale threads
959
865
  // through the replacement client.
@@ -961,7 +867,6 @@ export class CodexRunner {
961
867
  this.childThreadSessions.clear();
962
868
  this.trackedApprovalItems.clear();
963
869
  this.pendingToolDenials.clear();
964
- this.threadDelegationCarriers.clear();
965
870
  for (const client of clients) {
966
871
  client.close().catch(error => {
967
872
  logger.debug(`[CodexRunner] Failed to close stale app-server client: ${error}`);
@@ -1308,47 +1213,53 @@ export class CodexRunner {
1308
1213
  }
1309
1214
  buildPermissionProfileConfig(sessionId, mode, projectPath, bootstrapAgentMdPath, personalMdPath) {
1310
1215
  const profileName = this.permissionProfileName(sessionId);
1311
- const managedTempDir = this.ensureSessionTempDir(sessionId);
1216
+ const managedTempDir = ensureSessionRuntimeDir(sessionId);
1312
1217
  const network = {
1313
1218
  enabled: true,
1314
1219
  unix_sockets: { [resolvePaths().instanceSocket]: 'allow' },
1315
1220
  };
1221
+ const filesystem = {
1222
+ ...buildCodexProtectedFilesystemRules(resolveRoot(), {
1223
+ denyLClassRead: mode === 'readonly',
1224
+ // The app-server itself is launched behind EvolCore's Linux
1225
+ // H-class guard, which already mounts every L-class directory
1226
+ // read-only. Avoid Codex's per-file glob expansion inside that
1227
+ // nested mount namespace; it can fail while creating a missing
1228
+ // target such as triggers/history.jsonl.
1229
+ expandLClassReadDeny: process.platform !== 'linux',
1230
+ }),
1231
+ ...this.resolveGitMetadataWriteRules(projectPath, mode),
1232
+ ...(bootstrapAgentMdPath ? {
1233
+ [path.resolve(bootstrapAgentMdPath)]: mode === 'readonly' ? 'read' : 'write',
1234
+ } : {}),
1235
+ ...(personalMdPath ? {
1236
+ [path.resolve(personalMdPath)]: mode === 'readonly' ? 'read' : 'write',
1237
+ } : {}),
1238
+ ...(mode !== 'readonly' && managedTempDir ? {
1239
+ [managedTempDir]: 'write',
1240
+ [managedTempDir + '/**']: 'write',
1241
+ [path.join(managedTempDir, 'locks')]: 'write',
1242
+ [path.join(managedTempDir, 'locks') + '/**']: 'write',
1243
+ } : {}),
1244
+ };
1245
+ // Codex resolves this complete profile before serializing the Windows
1246
+ // helper payload. Check after all runner-owned additions, not merely the
1247
+ // protected-path base projection, so Git and managed-temp rules cannot
1248
+ // push an otherwise valid profile back over CreateProcess' limit.
1249
+ assertWindowsCodexFilesystemRuleBudget(filesystem);
1316
1250
  return {
1317
1251
  default_permissions: profileName,
1318
1252
  permissions: {
1319
1253
  [profileName]: {
1320
1254
  extends: mode === 'readonly' ? ':read-only' : ':workspace',
1321
- filesystem: {
1322
- ...buildCodexProtectedFilesystemRules(resolveRoot(), {
1323
- denyLClassRead: mode === 'readonly',
1324
- // The app-server itself is launched behind EvolCore's Linux
1325
- // H-class guard, which already mounts every L-class directory
1326
- // read-only. Avoid Codex's per-file glob expansion inside that
1327
- // nested mount namespace; it can fail while creating a missing
1328
- // target such as triggers/history.jsonl.
1329
- expandLClassReadDeny: process.platform !== 'linux',
1330
- }),
1331
- ...this.resolveGitMetadataWriteRules(projectPath, mode),
1332
- ...(bootstrapAgentMdPath ? {
1333
- [path.resolve(bootstrapAgentMdPath)]: mode === 'readonly' ? 'read' : 'write',
1334
- } : {}),
1335
- ...(personalMdPath ? {
1336
- [path.resolve(personalMdPath)]: mode === 'readonly' ? 'read' : 'write',
1337
- } : {}),
1338
- ...(mode !== 'readonly' && managedTempDir ? {
1339
- [managedTempDir]: 'write',
1340
- [managedTempDir + '/**']: 'write',
1341
- [path.join(path.dirname(managedTempDir), 'evolcore-locks')]: 'write',
1342
- [path.join(path.dirname(managedTempDir), 'evolcore-locks') + '/**']: 'write',
1343
- } : {}),
1344
- },
1255
+ filesystem,
1345
1256
  network,
1346
1257
  },
1347
1258
  },
1348
1259
  };
1349
1260
  }
1350
- buildExecutionSandboxOptions(sessionId, mode, projectPath, bootstrapAgentMdPath, personalMdPath) {
1351
- const decision = resolvePhaseOneExecutionSandbox(mode, this.permissionContexts.get(sessionId)?.role);
1261
+ buildExecutionSandboxOptions(sessionId, mode, projectPath, bootstrapAgentMdPath, personalMdPath, permissionContext = this.permissionContexts.get(sessionId) ?? {}) {
1262
+ const decision = resolvePhaseOneExecutionSandbox(mode, permissionContext.role);
1352
1263
  return {
1353
1264
  decision,
1354
1265
  permissionProfileConfig: decision.state === 'active'
@@ -1386,6 +1297,26 @@ export class CodexRunner {
1386
1297
  }
1387
1298
  setSendPrompt(fn) { this.sendPromptFn = fn; }
1388
1299
  setPermissionContext(sessionId, context) { this.permissionContexts.set(sessionId, context); }
1300
+ async steerAsyncQuestionAnswer(sessionId, answer) {
1301
+ const active = this.activeTurns.get(sessionId);
1302
+ if (!active)
1303
+ return 'inactive';
1304
+ if (active.threadId !== answer.providerThreadId)
1305
+ throw new Error('Async question backend has changed');
1306
+ const client = this.getAppServerClientForThread(active.threadId);
1307
+ try {
1308
+ await client.turnSteer(active.threadId, active.turnId, [{
1309
+ type: 'text', text: formatAsyncQuestionAnswer(answer), text_elements: [],
1310
+ }]);
1311
+ }
1312
+ catch (error) {
1313
+ if (error.code === 'CODEX_NO_ACTIVE_TURN')
1314
+ return 'inactive';
1315
+ // Transport failures are ambiguous: never risk submitting the same answer twice.
1316
+ throw error;
1317
+ }
1318
+ return 'accepted';
1319
+ }
1389
1320
  injectUserMessage(sessionId, text) {
1390
1321
  // Bind queued reminders to the turn that generated them. A completed
1391
1322
  // turn may be replaced before the serialized steer reaches app-server;
@@ -1722,9 +1653,11 @@ export class CodexRunner {
1722
1653
  const knownThreadId = topicBinding
1723
1654
  ? this.cachedThreadForTopic(sessionId, topicAgentSessionId)
1724
1655
  : initialAgentSessionId || this.activeSessions.get(sessionId);
1725
- const operationKey = this.threadOperationKey(knownThreadId, sessionId);
1656
+ const operationKey = this.threadOperationKey(modelOverride?.recovery?.agentSessionId || knownThreadId, sessionId);
1657
+ await this.assertInterruptSettled(sessionId, modelOverride?.recovery?.agentSessionId || knownThreadId);
1726
1658
  const release = await this.acquireThreadOperation(operationKey, 'thread/resume+turn/start');
1727
1659
  try {
1660
+ await this.assertInterruptSettled(sessionId, modelOverride?.recovery?.agentSessionId || knownThreadId);
1728
1661
  const stream = await this.runQueryLocked(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv);
1729
1662
  this.activeOperationReleases.set(sessionId, release);
1730
1663
  return this.holdThreadOperation(stream, sessionId, release);
@@ -1754,6 +1687,14 @@ export class CodexRunner {
1754
1687
  logger.debug(`[CodexRunner] persisted main backend lookup failed; using local cache: session=${sessionId} error=${error instanceof Error ? error.message : String(error)}`);
1755
1688
  }
1756
1689
  }
1690
+ const recoverySessionId = modelOverride?.recovery?.agentSessionId;
1691
+ if (modelOverride?.recovery && (!recoverySessionId || agentSessionId !== recoverySessionId)) {
1692
+ throw new Error('Task recovery requires the original Codex thread binding');
1693
+ }
1694
+ if (modelOverride?.asyncQuestionAnswer && agentSessionId !== modelOverride.asyncQuestionAnswer.providerThreadId) {
1695
+ throw new Error('Async question reply requires its original Codex backend');
1696
+ }
1697
+ await this.assertInterruptSettled(sessionId, agentSessionId);
1757
1698
  let resumingThread = !!agentSessionId;
1758
1699
  const callModel = stripContextWindowSuffix(modelOverride?.model || this.model);
1759
1700
  const callEffort = modelOverride?.effortMode === 'model_default'
@@ -1774,6 +1715,10 @@ export class CodexRunner {
1774
1715
  this.dropExpiredPendingInterrupt(sessionId);
1775
1716
  const effectiveApprovalPolicy = callApprovalPolicy;
1776
1717
  const capabilityConfig = await this.resolveCapabilityThreadConfig(projectPath);
1718
+ const userInputConfig = { features: {
1719
+ default_mode_request_user_input: modelOverride?.enableRequestUserInput
1720
+ ?? this.resolvedConfig.enableRequestUserInput ?? true,
1721
+ } };
1777
1722
  const externalToolConfig = callMode === 'fullaccess'
1778
1723
  ? {}
1779
1724
  : await this.resolveExternalToolApprovalConfig(appServer, projectPath, capabilityConfig, callMode);
@@ -1781,25 +1726,21 @@ export class CodexRunner {
1781
1726
  await this.assertProactiveHookAvailability(sessionId, appServer);
1782
1727
  const managedTempInstruction = this.managedTempInstruction(sessionId);
1783
1728
  const developerInstructions = [systemPromptAppend, managedTempInstruction].filter(Boolean).join('\n\n') || undefined;
1784
- let knownDelegationCarrier = agentSessionId
1785
- ? this.threadDelegationCarriers.get(agentSessionId)
1729
+ let threadSessionToken = agentSessionId
1730
+ ? this.ensureThreadCredential(agentSessionId)
1731
+ : this.createThreadCredential();
1732
+ if (!threadSessionToken)
1733
+ throw new Error('Codex thread session credential is unavailable');
1734
+ const persistentRuntimeEnv = runtimeEnv
1735
+ ? Object.fromEntries(Object.entries(runtimeEnv).filter(([name]) => callMode === 'fullaccess' || name.toUpperCase() !== AGENT_DELEGATION_TOKEN_ENV))
1786
1736
  : undefined;
1787
- if (agentSessionId && !knownDelegationCarrier) {
1788
- knownDelegationCarrier = await this.loadPersistedDelegationCarrier(sessionManager, sessionId, agentSessionId);
1789
- }
1790
- let delegationCarrierCandidate = knownDelegationCarrier ?? this.createDelegationCarrier();
1791
- // Keep newly discovered carrier state local until generation-aware
1792
- // activation accepts the backend. A stale run must not publish a carrier
1793
- // that a later generation could mistake for its own thread.
1794
- let carrierToPublish;
1795
- const hasTaskDelegation = typeof runtimeEnv?.[AGENT_DELEGATION_TOKEN_ENV] === 'string';
1796
1737
  let threadOptions = {
1797
1738
  model: callModel,
1798
1739
  effort: callEffort,
1799
1740
  approvalPolicy: effectiveApprovalPolicy,
1800
1741
  approvalsReviewer: 'user',
1801
1742
  ...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
1802
- 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),
1743
+ config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, persistentRuntimeEnv ? { shell_environment_policy: { set: persistentRuntimeEnv } } : undefined, capabilityConfig, userInputConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionId, threadSessionToken, callMode === 'fullaccess' ? runtimeEnv?.[AGENT_DELEGATION_TOKEN_ENV] : undefined), this.buildLifecycleLockdownConfig(callMode)), callMode, callMode === 'fullaccess' ? runtimeEnv?.[AGENT_DELEGATION_TOKEN_ENV] : undefined),
1803
1744
  ...(developerInstructions ? { developerInstructions } : {}),
1804
1745
  };
1805
1746
  logger.info(`[CodexRunner] runQuery permMode=${requestedPermissionMode}->${callMode} ` +
@@ -1825,12 +1766,12 @@ export class CodexRunner {
1825
1766
  // A legacy main session can retain a provider ID after its rollout was
1826
1767
  // removed externally. It is safe to recover before input submission;
1827
1768
  // topic bindings remain authoritative and fail closed.
1828
- if (topicBinding || !isCodexNoRolloutError(error))
1769
+ if (topicBinding || recoverySessionId || modelOverride?.asyncQuestionAnswer || !isCodexNoRolloutError(error))
1829
1770
  throw error;
1830
1771
  if (typeof sessionManager?.clearMainSessionBackendIfMatches !== 'function')
1831
1772
  throw error;
1832
1773
  const missingThreadId = agentSessionId;
1833
- const cleared = await sessionManager.clearMainSessionBackendIfMatches(sessionId, missingThreadId, [CODEX_DELEGATION_CARRIER_METADATA_KEY]);
1774
+ const cleared = await sessionManager.clearMainSessionBackendIfMatches(sessionId, missingThreadId);
1834
1775
  if (cleared !== 'cleared' && cleared !== 'already_clear') {
1835
1776
  const recoveryError = new Error(`Codex missing-rollout recovery rejected: ${cleared}`);
1836
1777
  recoveryError.code = 'CODEX_MISSING_ROLLOUT_RECOVERY_REJECTED';
@@ -1839,17 +1780,15 @@ export class CodexRunner {
1839
1780
  if (this.activeSessions.get(sessionId) === missingThreadId)
1840
1781
  this.activeSessions.delete(sessionId);
1841
1782
  this.threadProjectPaths.delete(missingThreadId);
1842
- this.threadDelegationCarriers.delete(missingThreadId);
1843
1783
  this.unbindExternalToolBoundary(missingThreadId);
1844
1784
  this.clearTrackedApprovalItemsForThread(missingThreadId);
1845
1785
  this.dropThreadCredential(missingThreadId);
1846
1786
  agentSessionId = undefined;
1847
1787
  resumingThread = false;
1848
- knownDelegationCarrier = undefined;
1849
- delegationCarrierCandidate = this.createDelegationCarrier();
1788
+ threadSessionToken = this.createThreadCredential();
1850
1789
  threadOptions = {
1851
1790
  ...threadOptions,
1852
- 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, persistentRuntimeEnv ? { shell_environment_policy: { set: persistentRuntimeEnv } } : undefined, capabilityConfig, userInputConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionId, threadSessionToken, callMode === 'fullaccess' ? runtimeEnv?.[AGENT_DELEGATION_TOKEN_ENV] : undefined), this.buildLifecycleLockdownConfig(callMode)), callMode, callMode === 'fullaccess' ? runtimeEnv?.[AGENT_DELEGATION_TOKEN_ENV] : undefined),
1853
1792
  };
1854
1793
  logger.warn(`[CodexRunner] recovered missing rollout before input submission: session=${sessionId} thread=${missingThreadId}`);
1855
1794
  threadResponse = await appServer.threadStart(projectPath, threadOptions);
@@ -1862,43 +1801,12 @@ export class CodexRunner {
1862
1801
  let threadId = threadResponse.thread?.id || agentSessionId;
1863
1802
  if (!threadId)
1864
1803
  throw new Error('Codex app-server did not return a thread id');
1865
- this.markThreadSubscribed(appServer, threadId);
1866
- if (!resumingThread) {
1867
- carrierToPublish = { threadId, carrier: delegationCarrierCandidate };
1868
- }
1869
- else if (hasTaskDelegation && !knownDelegationCarrier) {
1870
- // A BOUND topic cannot silently fork to migrate carrier metadata: its
1871
- // active-turn activation is allowed to confirm only the authoritative
1872
- // backend ID, so a fork would deterministically conflict. threadResume
1873
- // already received the candidate in this turn's environment config;
1874
- // publish it for the same thread after binding confirmation instead.
1875
- let recoveredCarrier = topicBinding
1876
- ? delegationCarrierCandidate
1877
- : await this.readThreadDelegationCarrier(appServer, threadId);
1878
- if (!recoveredCarrier) {
1879
- const staleThreadId = threadId;
1880
- logger.warn(`[CodexRunner] loaded thread has no recoverable delegation carrier; forking once to preserve history: thread=${staleThreadId}`);
1881
- const forked = await appServer.threadFork(staleThreadId, projectPath, undefined, {
1882
- model: callModel,
1883
- effort: callEffort,
1884
- approvalPolicy: effectiveApprovalPolicy,
1885
- approvalsReviewer: 'user',
1886
- ...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
1887
- config: threadOptions.config,
1888
- });
1889
- const forkedThreadId = forked.thread?.id;
1890
- if (!forkedThreadId)
1891
- throw new Error('Codex delegation-carrier recovery fork did not return a thread id');
1892
- this.markThreadSubscribed(appServer, forkedThreadId);
1893
- await this.releaseThreadSubscription(appServer, staleThreadId, 'carrier-recovery-fork-source');
1894
- await this.releaseThreadSubscription(appServer, forkedThreadId, 'carrier-recovery-fork');
1895
- threadId = forkedThreadId;
1896
- recoveredCarrier = delegationCarrierCandidate;
1897
- }
1898
- carrierToPublish = { threadId, carrier: recoveredCarrier };
1804
+ if (recoverySessionId && threadId !== recoverySessionId) {
1805
+ throw new Error('Task recovery cannot replace the original Codex thread');
1899
1806
  }
1900
- else if (knownDelegationCarrier) {
1901
- carrierToPublish = { threadId, carrier: knownDelegationCarrier };
1807
+ this.markThreadSubscribed(appServer, threadId);
1808
+ if (!resumingThread && this.ensureThreadCredential(threadId, threadSessionToken) !== threadSessionToken) {
1809
+ throw new Error('Codex thread session credential could not be bound');
1902
1810
  }
1903
1811
  const turn = modelOverride?.turn;
1904
1812
  if (!resumingThread) {
@@ -1906,9 +1814,6 @@ export class CodexRunner {
1906
1814
  // has been accepted. In particular, a pending interrupt must not leave
1907
1815
  // a durable ID for a thread that never produced a rollout.
1908
1816
  this.provisionalSessions.set(sessionId, threadId);
1909
- if (carrierToPublish) {
1910
- this.threadDelegationCarriers.set(carrierToPublish.threadId, carrierToPublish.carrier);
1911
- }
1912
1817
  }
1913
1818
  else {
1914
1819
  agentSessionId = threadId;
@@ -1928,10 +1833,6 @@ export class CodexRunner {
1928
1833
  throw error;
1929
1834
  }
1930
1835
  this.activeSessions.set(sessionId, threadId);
1931
- if (carrierToPublish) {
1932
- this.threadDelegationCarriers.set(carrierToPublish.threadId, carrierToPublish.carrier);
1933
- await this.persistDelegationCarrier(sessionManager, sessionId, carrierToPublish.threadId, carrierToPublish.carrier);
1934
- }
1935
1836
  this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${threadId}`);
1936
1837
  }
1937
1838
  this.threadProjectPaths.set(threadId, path.resolve(projectPath));
@@ -1939,7 +1840,11 @@ export class CodexRunner {
1939
1840
  const controller = new AbortController();
1940
1841
  this.activeAbortControllers.set(sessionId, controller);
1941
1842
  const tempFiles = [];
1942
- const input = this.buildAppServerInput(prompt, images, tempFiles, sessionId, runtimeEnv);
1843
+ const answer = modelOverride?.asyncQuestionAnswer;
1844
+ if (answer && answer.providerThreadId !== threadId) {
1845
+ throw new Error('Async question reply cannot resume a different Codex thread');
1846
+ }
1847
+ const input = this.buildAppServerInput(answer ? formatAsyncQuestionAnswer(answer) : prompt, images, tempFiles, sessionId, runtimeEnv);
1943
1848
  const queue = new AsyncEventQueue();
1944
1849
  controller.signal.addEventListener('abort', () => queue.end(), { once: true });
1945
1850
  const state = {
@@ -2053,7 +1958,7 @@ export class CodexRunner {
2053
1958
  if (!persisted)
2054
1959
  return false;
2055
1960
  try {
2056
- const committed = await this.commitProvisionalThread(sessionId, threadId, carrierToPublish, sessionManager, turn);
1961
+ const committed = await this.commitProvisionalThread(sessionId, threadId, turn);
2057
1962
  if (!committed) {
2058
1963
  const staleError = new Error('Codex provisional thread was superseded before persistence confirmation');
2059
1964
  staleError.code = 'RUNNER_START_SUPERSEDED';
@@ -2164,16 +2069,65 @@ export class CodexRunner {
2164
2069
  }
2165
2070
  }
2166
2071
  async interruptAppServerTurn(threadId, turnId) {
2072
+ const sessionId = this.findSessionKeyByThread(threadId);
2073
+ const subscribed = this.threadSubscriptions.get(threadId);
2074
+ const client = subscribed
2075
+ ? subscribed.client
2076
+ : this.getAppServerClient(sessionId ? this.chatModes.get(sessionId) === 'fullaccess' : false);
2077
+ const key = JSON.stringify([threadId, turnId]);
2078
+ const previous = this.unconfirmedInterrupts.get(key);
2079
+ previous?.unsubscribe?.();
2080
+ const record = { sessionId, threadId, turnId, client };
2081
+ this.unconfirmedInterrupts.set(key, record);
2082
+ let confirm;
2083
+ const confirmed = new Promise(resolve => { confirm = resolve; });
2084
+ const clear = () => {
2085
+ if (this.unconfirmedInterrupts.get(key) === record)
2086
+ this.unconfirmedInterrupts.delete(key);
2087
+ record.unsubscribe?.();
2088
+ confirm();
2089
+ };
2090
+ record.unsubscribe = client.onNotification?.(notification => {
2091
+ const params = notification.params ?? {};
2092
+ if ((params.threadId ?? params.thread_id) !== threadId)
2093
+ return;
2094
+ if (notification.method === 'thread/closed'
2095
+ || (notification.method === 'turn/completed'
2096
+ && (params.turn?.id ?? params.turnId ?? params.turn_id) === turnId))
2097
+ clear();
2098
+ });
2167
2099
  try {
2168
- const sessionId = this.findSessionKeyByThread(threadId);
2169
- const subscribed = this.threadSubscriptions.get(threadId);
2170
- const client = subscribed
2171
- ? subscribed.client
2172
- : this.getAppServerClient(sessionId ? this.chatModes.get(sessionId) === 'fullaccess' : false);
2173
- await client.turnInterrupt(threadId, turnId);
2100
+ await withInterruptDeadline(Promise.race([
2101
+ client.turnInterrupt(threadId, turnId).then(accepted => {
2102
+ if (!accepted)
2103
+ throw new Error('Codex interrupt was not accepted');
2104
+ }),
2105
+ confirmed,
2106
+ ]), 'Codex turn/interrupt', CODEX_INTERRUPT_TIMEOUT_MS);
2174
2107
  }
2175
2108
  catch (error) {
2176
- logger.debug(`[CodexRunner] app-server turn interrupt failed: ${error}`);
2109
+ logger.warn(`[CodexRunner] Unconfirmed interrupt: thread=${threadId} turn=${turnId} error=${String(error)}`);
2110
+ }
2111
+ }
2112
+ async assertInterruptSettled(sessionId, threadId) {
2113
+ const pending = [...this.unconfirmedInterrupts.entries()].filter(([, record]) => record.sessionId === sessionId || (threadId && record.threadId === threadId));
2114
+ await Promise.all(pending.map(async ([key, record]) => {
2115
+ try {
2116
+ // Local stream closure and unsubscribe do not prove the backend turn
2117
+ // stopped. Check its exact identity before allowing another writer.
2118
+ const response = await withInterruptDeadline(record.client.threadRead(record.threadId, true, 2_000), 'Codex interrupt verification', 2_000);
2119
+ const turn = response.thread?.turns?.find(turn => turn.id === record.turnId);
2120
+ if (turn && ['completed', 'failed', 'interrupted'].includes(String(turn.status))
2121
+ && !response.thread?.turns?.some(turn => turn.status === 'inProgress')
2122
+ && this.unconfirmedInterrupts.get(key) === record) {
2123
+ this.unconfirmedInterrupts.delete(key);
2124
+ record.unsubscribe?.();
2125
+ }
2126
+ }
2127
+ catch { /* Ambiguous execution remains fenced; report it below. */ }
2128
+ }));
2129
+ if ([...this.unconfirmedInterrupts.values()].some(record => record.sessionId === sessionId || (threadId && record.threadId === threadId))) {
2130
+ throw unsettledInterruptError('Codex');
2177
2131
  }
2178
2132
  }
2179
2133
  // ── Session commands ──
@@ -2185,7 +2139,6 @@ export class CodexRunner {
2185
2139
  return;
2186
2140
  if (previousThreadId && previousThreadId !== agentSessionId) {
2187
2141
  this.threadProjectPaths.delete(previousThreadId);
2188
- this.threadDelegationCarriers.delete(previousThreadId);
2189
2142
  this.unbindExternalToolBoundary(previousThreadId);
2190
2143
  this.clearTrackedApprovalItemsForThread(previousThreadId);
2191
2144
  }
@@ -2269,10 +2222,8 @@ export class CodexRunner {
2269
2222
  if (this.activeSessions.get(sessionId) === capturedThreadId)
2270
2223
  this.activeSessions.delete(sessionId);
2271
2224
  this.chatModes.delete(sessionId);
2272
- this.cleanupSessionTempDir(sessionId);
2273
2225
  if (capturedThreadId) {
2274
2226
  this.threadProjectPaths.delete(capturedThreadId);
2275
- this.threadDelegationCarriers.delete(capturedThreadId);
2276
2227
  this.unbindExternalToolBoundary(capturedThreadId);
2277
2228
  }
2278
2229
  this.clearTrackedApprovalItemsForThread(capturedThreadId);
@@ -2312,9 +2263,7 @@ export class CodexRunner {
2312
2263
  this.activeTurns.delete(sessionId);
2313
2264
  this.steeringTails.delete(sessionId);
2314
2265
  this.chatModes.delete(sessionId);
2315
- this.cleanupSessionTempDir(sessionId);
2316
2266
  this.threadProjectPaths.delete(threadId);
2317
- this.threadDelegationCarriers.delete(threadId);
2318
2267
  this.unbindExternalToolBoundary(threadId);
2319
2268
  this.clearTrackedApprovalItemsForThread(threadId);
2320
2269
  this.pruneThreadOperationLocks();
@@ -2355,19 +2304,19 @@ export class CodexRunner {
2355
2304
  : await this.resolveExternalToolApprovalConfig(appServer, _projectPath, capabilityConfig, compactMode);
2356
2305
  if (compactMode !== 'fullaccess')
2357
2306
  await this.assertProactiveHookAvailability(_sessionId, appServer);
2358
- const compactDelegationCarrier = this.threadDelegationCarriers.get(agentSessionId)
2359
- ?? this.createDelegationCarrier();
2307
+ const compactSessionToken = this.ensureThreadCredential(agentSessionId);
2308
+ if (!compactSessionToken)
2309
+ throw new Error('Codex thread session credential is unavailable');
2360
2310
  await appServer.threadResume(agentSessionId, _projectPath, {
2361
2311
  model: compactModel,
2362
2312
  effort: compactEffort,
2363
2313
  approvalPolicy: compactPolicy,
2364
2314
  approvalsReviewer: 'user',
2365
2315
  ...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
2366
- config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(_sessionId, compactDelegationCarrier, undefined, agentSessionId), this.buildLifecycleLockdownConfig(compactMode)), compactMode),
2316
+ config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(_sessionId, compactSessionToken), this.buildLifecycleLockdownConfig(compactMode)), compactMode),
2367
2317
  });
2368
2318
  this.markThreadSubscribed(appServer, agentSessionId);
2369
2319
  this.threadProjectPaths.set(agentSessionId, path.resolve(_projectPath));
2370
- this.threadDelegationCarriers.set(agentSessionId, compactDelegationCarrier);
2371
2320
  this.bindExternalToolBoundary(agentSessionId, externalToolConfig);
2372
2321
  await this.startAndWaitForCompact(appServer, agentSessionId);
2373
2322
  return finish({ ok: true, durationMs: Date.now() - startedAt });
@@ -2528,23 +2477,32 @@ export class CodexRunner {
2528
2477
  const dir = this.threadCredentialsDir();
2529
2478
  return dir ? path.join(dir, `${encodeURIComponent(threadId)}.json`) : undefined;
2530
2479
  }
2480
+ createThreadCredential() {
2481
+ return randomBytes(32).toString('hex');
2482
+ }
2531
2483
  /** Issue or re-read this agent's credential for a thread. */
2532
- ensureThreadCredential(threadId) {
2484
+ ensureThreadCredential(threadId, candidate) {
2533
2485
  const cached = this.threadCredentials.get(threadId);
2534
2486
  if (cached)
2535
- return cached;
2487
+ return !candidate || cached === candidate ? cached : undefined;
2536
2488
  const file = this.threadCredentialPath(threadId);
2537
- if (!file)
2538
- return undefined;
2489
+ // Low-level runner callers may not have an agent binding (for example a
2490
+ // standalone integration test). Keep those calls functional in memory;
2491
+ // daemon-owned runners always have an AID and persist the credential.
2492
+ if (!file) {
2493
+ const token = candidate ?? this.createThreadCredential();
2494
+ this.threadCredentials.set(threadId, token);
2495
+ return token;
2496
+ }
2539
2497
  try {
2540
2498
  const record = JSON.parse(fs.readFileSync(file, 'utf8'));
2541
2499
  if (record && typeof record.token === 'string' && record.token && record.threadId === threadId) {
2542
2500
  this.threadCredentials.set(threadId, record.token);
2543
- return record.token;
2501
+ return !candidate || record.token === candidate ? record.token : undefined;
2544
2502
  }
2545
2503
  }
2546
2504
  catch { /* missing or unreadable: fall through to issuance */ }
2547
- const token = randomBytes(32).toString('hex');
2505
+ const token = candidate ?? this.createThreadCredential();
2548
2506
  const record = {
2549
2507
  token,
2550
2508
  threadId,
@@ -2606,49 +2564,55 @@ export class CodexRunner {
2606
2564
  }
2607
2565
  catch { /* best effort */ }
2608
2566
  }
2609
- async forkSession(agentSessionId, projectPath, title) {
2567
+ async forkSession(agentSessionId, projectPath, title, context) {
2610
2568
  const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId), 'fork');
2611
2569
  try {
2612
- return await this.forkSessionLocked(agentSessionId, projectPath, title);
2570
+ return await this.forkSessionLocked(agentSessionId, projectPath, title, undefined, context);
2613
2571
  }
2614
2572
  finally {
2615
2573
  release();
2616
2574
  }
2617
2575
  }
2618
- async forkSessionLocked(agentSessionId, projectPath, title, lastTurnId) {
2619
- const sessionKey = this.findSessionKeyByThread(agentSessionId);
2620
- const mode = this.chatModes.get(sessionKey) ?? this.currentMode;
2621
- const executionSandbox = this.buildExecutionSandboxOptions(sessionKey, mode, projectPath);
2576
+ async forkSessionLocked(agentSessionId, projectPath, title, lastTurnId, context) {
2577
+ // Historical threads may have no live runner binding after idle cleanup or
2578
+ // restart. Cached turn permissions can also outlive a role change. Derive
2579
+ // this management operation solely from its current authenticated request;
2580
+ // callers without that context remain readonly, including old fullaccess
2581
+ // threads. Do not mutate shared turn/session permission maps while forking.
2582
+ const sessionKey = context?.sessionId ?? this.findSessionKeyByThread(agentSessionId);
2583
+ const mode = normalizePermissionMode(context?.permissionMode).mode;
2584
+ const executionSandbox = this.buildExecutionSandboxOptions(sessionKey, mode, projectPath, undefined, undefined, context ?? {});
2622
2585
  const capabilityConfig = await this.resolveCapabilityThreadConfig(projectPath);
2623
- const appServer = this.threadSubscriptions.get(agentSessionId)?.client
2624
- ?? this.getAppServerClient(mode === 'fullaccess');
2586
+ const appServer = this.getAppServerClient();
2625
2587
  const externalToolConfig = await this.resolveExternalToolApprovalConfig(appServer, projectPath, capabilityConfig, mode);
2626
2588
  if (sessionKey)
2627
2589
  await this.assertProactiveHookAvailability(sessionKey, appServer);
2628
- const delegationCarrier = this.createDelegationCarrier();
2590
+ const forkSessionToken = this.createThreadCredential();
2629
2591
  const response = await appServer.threadFork(agentSessionId, projectPath, title, {
2630
2592
  model: this.model,
2631
2593
  effort: this.effort,
2632
2594
  approvalPolicy: this.toApprovalPolicy(mode),
2633
2595
  approvalsReviewer: 'user',
2634
2596
  ...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
2635
- config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionKey, delegationCarrier), this.buildLifecycleLockdownConfig(mode)), mode),
2597
+ config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionKey, forkSessionToken), this.buildLifecycleLockdownConfig(mode)), mode),
2636
2598
  ...(lastTurnId ? { lastTurnId } : {}),
2637
2599
  });
2638
2600
  const forkedThreadId = response.thread?.id;
2639
2601
  if (!forkedThreadId)
2640
2602
  throw new Error('Codex fork did not return a thread id');
2603
+ if (this.ensureThreadCredential(forkedThreadId, forkSessionToken) !== forkSessionToken) {
2604
+ throw new Error('Codex fork session credential could not be bound');
2605
+ }
2641
2606
  this.markThreadSubscribed(appServer, forkedThreadId);
2642
- this.threadDelegationCarriers.set(forkedThreadId, delegationCarrier);
2643
2607
  this.threadProjectPaths.set(forkedThreadId, path.resolve(projectPath));
2644
2608
  this.bindExternalToolBoundary(forkedThreadId, externalToolConfig);
2645
2609
  await this.releaseThreadSubscription(appServer, forkedThreadId, 'fork-finished');
2646
2610
  return forkedThreadId;
2647
2611
  }
2648
- async forkSessionAt(agentSessionId, projectPath, assistantMessageId, title) {
2612
+ async forkSessionAt(agentSessionId, projectPath, assistantMessageId, title, context) {
2649
2613
  const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId), 'forkAt');
2650
2614
  try {
2651
- const appServer = this.getAppServerClientForThread(agentSessionId);
2615
+ const appServer = this.getAppServerClient();
2652
2616
  const history = await this.readForkHistory(appServer, agentSessionId);
2653
2617
  const messages = this.mapThreadToSessionMessages(history, agentSessionId);
2654
2618
  const turns = buildSessionTurnList(messages);
@@ -2658,7 +2622,7 @@ export class CodexRunner {
2658
2622
  const targetTurnId = this.findTurnIdForAssistantMessage(history, assistantMessageId);
2659
2623
  if (!targetTurnId)
2660
2624
  throw new Error('fork point turn not found');
2661
- return await this.forkSessionLocked(agentSessionId, projectPath, title, targetTurnId);
2625
+ return await this.forkSessionLocked(agentSessionId, projectPath, title, targetTurnId, context);
2662
2626
  }
2663
2627
  finally {
2664
2628
  release();
@@ -2730,7 +2694,7 @@ export class CodexRunner {
2730
2694
  return this.getAppServerClientForThread(agentSessionId).threadMetadataUpdate(agentSessionId, gitInfo);
2731
2695
  }
2732
2696
  async getSessionMessages(agentSessionId, projectPath) {
2733
- const response = await this.getAppServerClientForThread(agentSessionId).threadRead(agentSessionId, true);
2697
+ const response = await this.readForkHistory(this.getAppServerClientForThread(agentSessionId), agentSessionId);
2734
2698
  return this.mapThreadToSessionMessages(response, agentSessionId);
2735
2699
  }
2736
2700
  approvalItemKey(threadId, turnId, itemId) {
@@ -3070,9 +3034,9 @@ export class CodexRunner {
3070
3034
  const isCommandApproval = request.method === 'item/commandExecution/requestApproval'
3071
3035
  || request.method === 'execCommandApproval';
3072
3036
  const managedEcIntent = isCommandApproval && this.isManagedEvolcoreCommandIntent(toolInput);
3073
- const delegationCarrier = this.delegationCarrierForSession(sessionKey, typeof params.threadId === 'string' ? params.threadId : undefined);
3037
+ const sessionCredential = this.sessionTokenForSession(sessionKey, typeof params.threadId === 'string' ? params.threadId : undefined);
3074
3038
  const canonicalEcApproval = isCommandApproval
3075
- ? this.resolveCanonicalEcApproval(toolInput, this.getManagedTempDir(sessionKey), delegationCarrier)
3039
+ ? this.resolveCanonicalEcApproval(toolInput, this.getManagedTempDir(sessionKey), sessionCredential)
3076
3040
  : undefined;
3077
3041
  const approvedCommand = canonicalEcApproval?.argv;
3078
3042
  const denyInfrastructure = async (policyCode, message) => {
@@ -3115,6 +3079,9 @@ export class CodexRunner {
3115
3079
  }
3116
3080
  return await denyInfrastructure('ec_command_not_canonical', canonicalEcApproval?.parseFailure
3117
3081
  ? `EC command approval denied because canonical argv parsing failed (${canonicalEcApproval.parseFailure.issue})`
3082
+ + (canonicalEcApproval.parseFailure.dialect === 'powershell' && canonicalEcApproval.parseFailure.issue === 'invalid-quote'
3083
+ ? '. PowerShell treats smart quotes as delimiters too; escape quotes for PowerShell or use --text-from-file for message text.'
3084
+ : '')
3118
3085
  : 'EC command approval denied because no canonical argv could be derived');
3119
3086
  }
3120
3087
  try {
@@ -3122,23 +3089,23 @@ export class CodexRunner {
3122
3089
  const decision = typeof resolution === 'string' ? resolution : resolution.decision;
3123
3090
  const resolutionMeta = typeof resolution === 'string' ? undefined : resolution;
3124
3091
  if (decision !== 'deny' && managedEcIntent && approvedCommand) {
3125
- const carrierThreadIds = [
3092
+ const credentialThreadIds = [
3126
3093
  this.activeSessions.get(sessionKey),
3127
3094
  this.provisionalSessions.get(sessionKey),
3128
3095
  typeof params.threadId === 'string' ? params.threadId : undefined,
3129
3096
  typeof params.conversationId === 'string' ? params.conversationId : undefined,
3130
3097
  ];
3131
- const hasDelegationCarrier = carrierThreadIds.some(threadId => !!threadId && this.threadDelegationCarriers.has(threadId));
3098
+ const hasSessionCredential = credentialThreadIds.some(threadId => !!threadId && !!this.ensureThreadCredentialLoaded(threadId));
3132
3099
  const canArmDelegation = typeof permissionContext?.armApprovedDelegationCommand === 'function';
3133
- // A carrier-bearing thread is managed even if its permission context
3100
+ // A credentialed thread is managed even if its permission context
3134
3101
  // was not wired with an arm callback; it must fail closed here rather
3135
- // than reaching daemon IPC with an unarmed carrier.
3136
- if ((hasDelegationCarrier || canArmDelegation)
3137
- && canonicalEcApproval?.delegationToken
3138
- && canonicalEcApproval.delegationToken !== delegationCarrier) {
3139
- return await denyInfrastructure('delegation_token_mismatch', 'EC command approval contains a delegation token different from the active thread carrier');
3102
+ // than reaching daemon IPC with an unarmed session credential.
3103
+ if ((hasSessionCredential || canArmDelegation)
3104
+ && canonicalEcApproval?.credentialToken
3105
+ && canonicalEcApproval.credentialToken !== sessionCredential) {
3106
+ return await denyInfrastructure('delegation_token_mismatch', 'EC command approval contains a delegation token different from the active thread credential');
3140
3107
  }
3141
- if ((hasDelegationCarrier || canArmDelegation)
3108
+ if ((hasSessionCredential || canArmDelegation)
3142
3109
  && !this.armApprovedDelegationCommand(sessionKey, approvedCommand)) {
3143
3110
  return await denyInfrastructure('delegation_not_armed', 'EC command approval failed to arm delegation');
3144
3111
  }
@@ -3793,14 +3760,7 @@ export class CodexRunner {
3793
3760
  if (paths.length === 0)
3794
3761
  return true;
3795
3762
  const workspace = resolveProtectedCandidate(projectPath);
3796
- const managedTempDir = this.sessionTempDirs.get(sessionKey);
3797
- const trustedTempDir = managedTempDir ? this.isTrustedSessionTempDir(managedTempDir) : false;
3798
- if (managedTempDir && !trustedTempDir) {
3799
- logger.warn(`[CodexRunner] managed TMPDIR lost its trusted ownership or mode: session=${sessionKey} path=${managedTempDir}`);
3800
- }
3801
- const canonicalTempDir = managedTempDir && trustedTempDir
3802
- ? resolveProtectedCandidate(managedTempDir)
3803
- : undefined;
3763
+ const canonicalTempDir = this.getManagedTempDir(sessionKey);
3804
3764
  return paths.some(candidate => {
3805
3765
  const absolute = path.isAbsolute(candidate) ? path.resolve(candidate) : path.resolve(workspace, candidate);
3806
3766
  const canonical = resolveProtectedCandidate(absolute);
@@ -3895,7 +3855,7 @@ export class CodexRunner {
3895
3855
  // distinguishes this explicit empty value from runners without a
3896
3856
  // session-temp capability.
3897
3857
  managedTempDir: permissionContext?.managedTempDir ?? this.getManagedTempDir(sessionKey) ?? '',
3898
- delegationCarrier: this.delegationCarrierForSession(sessionKey),
3858
+ sessionCredential: this.sessionTokenForSession(sessionKey),
3899
3859
  allowProtectedMetadata: false,
3900
3860
  selfAid: permissionContext?.selfAid,
3901
3861
  channel: permissionContext?.channel,
@@ -4252,17 +4212,17 @@ export class CodexRunner {
4252
4212
  return;
4253
4213
  this.childThreadSessions.set(childThreadId, sessionId);
4254
4214
  this.threadProjectPaths.set(childThreadId, path.resolve(projectPath));
4255
- // Child approvals use the parent's capability boundary and delegation
4256
- // carrier. This is inheritance, not a second permission decision.
4215
+ // Child approvals use the parent's capability boundary and session
4216
+ // credential. This is inheritance, not a second permission decision.
4257
4217
  const parentBoundary = parentThreadId ? this.threadExternalToolBoundaries.get(parentThreadId) : undefined;
4258
4218
  const parentFingerprint = parentThreadId ? this.threadExternalToolFingerprints.get(parentThreadId) : undefined;
4259
4219
  if (parentBoundary)
4260
4220
  this.threadExternalToolBoundaries.set(childThreadId, parentBoundary);
4261
4221
  if (parentFingerprint)
4262
4222
  this.threadExternalToolFingerprints.set(childThreadId, parentFingerprint);
4263
- const parentCarrier = parentThreadId ? this.threadDelegationCarriers.get(parentThreadId) : undefined;
4264
- if (parentCarrier)
4265
- this.threadDelegationCarriers.set(childThreadId, parentCarrier);
4223
+ const parentToken = parentThreadId ? this.ensureThreadCredentialLoaded(parentThreadId) : undefined;
4224
+ if (parentToken)
4225
+ this.ensureThreadCredential(childThreadId, parentToken);
4266
4226
  }
4267
4227
  unregisterChildThread(childThreadId) {
4268
4228
  this.childThreadSessions.delete(childThreadId);
@@ -4270,8 +4230,8 @@ export class CodexRunner {
4270
4230
  this.threadProjectPaths.delete(childThreadId);
4271
4231
  this.threadExternalToolBoundaries.delete(childThreadId);
4272
4232
  this.threadExternalToolFingerprints.delete(childThreadId);
4273
- this.threadDelegationCarriers.delete(childThreadId);
4274
4233
  this.clearTrackedApprovalItemsForThread(childThreadId);
4234
+ this.dropThreadCredential(childThreadId);
4275
4235
  }
4276
4236
  cleanupChildThreadsForSession(sessionId) {
4277
4237
  for (const [childThreadId, ownerSessionId] of this.childThreadSessions) {
@@ -4411,10 +4371,14 @@ export class CodexRunner {
4411
4371
  }
4412
4372
  case 'item/agentMessage/delta': {
4413
4373
  const itemId = typeof params.itemId === 'string' ? params.itemId : undefined;
4414
- if (itemId)
4415
- state.streamedAgentMessageIds.add(itemId);
4416
- if (itemId && typeof params.delta === 'string' && params.delta) {
4417
- state.agentMessageDeltaText.set(itemId, (state.agentMessageDeltaText.get(itemId) || '') + params.delta);
4374
+ const delta = typeof params.delta === 'string' ? params.delta : '';
4375
+ if (itemId && delta) {
4376
+ if (!state.streamedAgentMessageIds.has(itemId)) {
4377
+ state.streamedAgentMessageIds.add(itemId);
4378
+ yield { type: 'text_start', contentIndex: 0 };
4379
+ }
4380
+ state.agentMessageDeltaText.set(itemId, (state.agentMessageDeltaText.get(itemId) || '') + delta);
4381
+ yield { type: 'heartbeat', source: 'text' };
4418
4382
  }
4419
4383
  break;
4420
4384
  }
@@ -4715,10 +4679,38 @@ export class CodexRunner {
4715
4679
  switch (item.type) {
4716
4680
  case 'agentMessage':
4717
4681
  {
4682
+ if (item.questions != null) {
4683
+ if (item.id)
4684
+ state.agentMessageDeltaText.delete(item.id);
4685
+ try {
4686
+ const questions = parseAsyncUserQuestions(item.questions);
4687
+ if (!item.id || !state.turnId)
4688
+ throw new Error('Async question is missing its item or turn id');
4689
+ const key = JSON.stringify([state.threadId, state.turnId, item.id]);
4690
+ if (this.asyncQuestionItems.has(key))
4691
+ break;
4692
+ this.asyncQuestionItems.add(key);
4693
+ if (this.asyncQuestionItems.size > 2048)
4694
+ this.asyncQuestionItems.delete(this.asyncQuestionItems.values().next().value);
4695
+ yield { type: 'user_question', request: {
4696
+ providerThreadId: state.threadId, providerTurnId: state.turnId, itemId: item.id, questions,
4697
+ } };
4698
+ }
4699
+ catch (error) {
4700
+ yield { type: 'notice', severity: 'warn', subtype: 'invalid_async_question',
4701
+ message: `Codex 异步问题无法显示:${error instanceof Error ? error.message : String(error)}` };
4702
+ }
4703
+ break;
4704
+ }
4718
4705
  const buffered = item.id ? state.agentMessageDeltaText.get(item.id) : undefined;
4719
4706
  const text = typeof item.text === 'string' && item.text ? item.text : buffered;
4720
- if (text)
4721
- yield { type: 'text', text };
4707
+ if (text) {
4708
+ state.lastAssistantUuid = item.id || state.turnId;
4709
+ yield {
4710
+ type: 'text', text, assistantUuid: state.lastAssistantUuid,
4711
+ ...(item.phase === 'commentary' || item.phase === 'final_answer' ? { phase: item.phase } : {}),
4712
+ };
4713
+ }
4722
4714
  if (item.id)
4723
4715
  state.agentMessageDeltaText.delete(item.id);
4724
4716
  }
@@ -4956,6 +4948,7 @@ export class CodexRunner {
4956
4948
  return {
4957
4949
  type: 'complete',
4958
4950
  subtype: status === 'completed' ? 'success' : outputLimit ? 'max_output_tokens' : status,
4951
+ assistantUuid: state.lastAssistantUuid,
4959
4952
  isError: status === 'failed',
4960
4953
  errors: turn.error?.message ? [turn.error.message] : undefined,
4961
4954
  terminalReason,
@@ -4969,126 +4962,36 @@ export class CodexRunner {
4969
4962
  contextUsage,
4970
4963
  };
4971
4964
  }
4972
- managedTempParent() {
4973
- const uid = typeof process.getuid === 'function' ? process.getuid() : 'user';
4974
- const base = ensureProcessManagedTempDir();
4975
- return path.join(resolveProtectedCandidate(base), `evolcore-codex-${uid}`);
4976
- }
4977
- ensurePrivateDirectory(directory) {
4978
- try {
4979
- fs.mkdirSync(directory, { mode: 0o700 });
4980
- }
4981
- catch (error) {
4982
- if (error?.code !== 'EEXIST')
4983
- throw error;
4984
- }
4985
- const stat = fs.lstatSync(directory);
4986
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
4987
- throw new Error(`managed temp path is not a real directory: ${directory}`);
4988
- }
4989
- if (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
4990
- throw new Error(`managed temp path is owned by another uid: ${directory}`);
4991
- }
4992
- if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)
4993
- fs.chmodSync(directory, 0o700);
4994
- }
4995
- ensureSessionTempDir(sessionId) {
4996
- const existing = this.sessionTempDirs.get(sessionId);
4997
- if (existing) {
4998
- try {
4999
- this.ensurePrivateDirectory(this.managedTempParent());
5000
- this.ensurePrivateDirectory(existing);
5001
- return existing;
5002
- }
5003
- catch (error) {
5004
- this.sessionTempDirs.delete(sessionId);
5005
- logger.warn(`[CodexRunner] existing managed TMPDIR is no longer usable: session=${sessionId} path=${existing} error=${error instanceof Error ? error.message : String(error)}`);
5006
- return undefined;
5007
- }
5008
- }
5009
- try {
5010
- const parent = this.managedTempParent();
5011
- this.ensurePrivateDirectory(parent);
5012
- this.ensurePrivateDirectory(path.join(parent, 'evolcore-locks'));
5013
- // Include a per-runner nonce so a daemon restart never reuses contents
5014
- // left behind by an unclean shutdown for the same EvolCore session id.
5015
- const digest = createHash('sha256')
5016
- .update(this.managedTempNonce)
5017
- .update('\0')
5018
- .update(sessionId)
5019
- .digest('hex')
5020
- .slice(0, 24);
5021
- const directory = path.join(parent, digest);
5022
- this.ensurePrivateDirectory(directory);
5023
- this.sessionTempDirs.set(sessionId, directory);
5024
- registerManagedTempDirForExitCleanup(directory, parent);
5025
- return directory;
5026
- }
5027
- catch (error) {
5028
- logger.warn(`[CodexRunner] managed TMPDIR unavailable for session=${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
5029
- return undefined;
5030
- }
5031
- }
5032
- isTrustedSessionTempDir(directory) {
5033
- return isTrustedManagedTempDirectory(directory, this.managedTempParent());
5034
- }
5035
4965
  getManagedTempDir(sessionId) {
5036
- const directory = this.ensureSessionTempDir(sessionId);
5037
- if (!directory || !this.isTrustedSessionTempDir(directory))
5038
- return undefined;
5039
- return resolveProtectedCandidate(directory);
5040
- }
5041
- cleanupSessionTempDir(sessionId) {
5042
- const directory = this.sessionTempDirs.get(sessionId);
5043
- this.sessionTempDirs.delete(sessionId);
5044
- if (!directory)
5045
- return;
5046
- // Reuse the same ownership, mode, lexical-parent, and canonicalization
5047
- // checks used at execution time. If the path changed, leave it in place
5048
- // rather than recursively removing an attacker-selected target.
5049
- if (!this.isTrustedSessionTempDir(directory)) {
5050
- managedTempDirsForExitCleanup.delete(directory);
5051
- logger.warn(`[CodexRunner] refused to clean unmanaged TMPDIR: session=${sessionId} path=${directory}`);
5052
- return;
5053
- }
5054
- const candidate = path.resolve(directory);
5055
- try {
5056
- fs.rmSync(candidate, { recursive: true, force: true });
5057
- managedTempDirsForExitCleanup.delete(directory);
5058
- }
5059
- catch (error) {
5060
- logger.warn(`[CodexRunner] failed to clean managed TMPDIR: session=${sessionId} path=${candidate} error=${error instanceof Error ? error.message : String(error)}`);
5061
- }
4966
+ const directory = ensureSessionRuntimeDir(sessionId);
4967
+ return directory ? resolveProtectedCandidate(directory) : undefined;
5062
4968
  }
5063
4969
  managedTempInstruction(sessionId) {
5064
- const managedTempDir = this.ensureSessionTempDir(sessionId);
4970
+ const managedTempDir = ensureSessionRuntimeDir(sessionId);
5065
4971
  if (!managedTempDir)
5066
4972
  return undefined;
5067
- return `临时文件必须使用环境变量 \`$TMPDIR\`。当前值是 \`${managedTempDir}\`,它是当前 EvolCore 会话唯一允许的临时工作区;不要直接使用 \`/tmp\`,也不要使用其他会话的临时目录。当前是可持久化 thread 的恢复轮次,早先上下文中的 \`/tmp/evolcore-codex-*\` 绝对路径可能已经失效;不要重用旧路径。若文件不存在,请先在当前 \`$TMPDIR\` 重新创建,再继续执行后续 \`ec\` 命令。`;
4973
+ return `临时文件必须使用环境变量 \`$TMPDIR\`。当前值是 \`${managedTempDir}\`,它是当前 EvolCore 会话唯一允许的临时工作区;不要直接使用 \`/tmp\`,也不要使用其他会话的临时目录。该目录随 EvolCore sessionId 保持稳定;若文件不存在,请先在当前 \`$TMPDIR\` 重新创建,再继续执行后续 \`ec\` 命令。`;
5068
4974
  }
5069
- buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrier, taskDelegationToken, threadId) {
5070
- const managedTempDir = this.ensureSessionTempDir(sessionId);
4975
+ buildEvolcoreShellEnvironmentConfig(sessionId, sessionToken, taskDelegationToken) {
4976
+ const managedTempDir = ensureSessionRuntimeDir(sessionId);
5071
4977
  // The managed-hook credential rides the thread's shell environment so the
5072
4978
  // PreToolUse hook can prove the call belongs to an EvolCore-owned thread.
5073
4979
  // It follows the thread (start AND resume) rather than the daemon process.
5074
- const sessionToken = threadId ? this.ensureThreadCredential(threadId) : undefined;
5075
4980
  return {
5076
4981
  shell_environment_policy: {
5077
4982
  set: {
5078
4983
  EVOLCORE_SESSION_ID: sessionId,
5079
4984
  EVOLCORE_HOME: resolveRoot(),
5080
4985
  ...(managedTempDir ? { EVOLCORE_SESSION_RUNTIME_DIR: managedTempDir } : {}),
5081
- ...(managedTempDir ? { EVOLCORE_RUNTIME_LOCK_DIR: path.join(path.dirname(managedTempDir), 'evolcore-locks') } : {}),
5082
- ...(taskDelegationToken || delegationCarrier
5083
- ? { [AGENT_DELEGATION_TOKEN_ENV]: taskDelegationToken ?? delegationCarrier }
5084
- : {}),
4986
+ ...(managedTempDir ? { EVOLCORE_RUNTIME_LOCK_DIR: path.join(managedTempDir, 'locks') } : {}),
4987
+ ...(taskDelegationToken ? { [AGENT_DELEGATION_TOKEN_ENV]: taskDelegationToken } : {}),
5085
4988
  ...(sessionToken ? { [CODEX_SESSION_TOKEN_ENV]: sessionToken } : {}),
5086
4989
  ...(managedTempDir ? { TMPDIR: managedTempDir } : {}),
5087
4990
  },
5088
4991
  },
5089
4992
  };
5090
4993
  }
5091
- sanitizeThreadEnvironmentConfig(config, permissionMode) {
4994
+ sanitizeThreadEnvironmentConfig(config, permissionMode, taskDelegationToken) {
5092
4995
  if (!config)
5093
4996
  return null;
5094
4997
  const policy = this.configRecord(config.shell_environment_policy);
@@ -5098,11 +5001,40 @@ export class CodexRunner {
5098
5001
  const safeSet = sanitizeShellExecutionEnvironment(set, {
5099
5002
  lockExecutableResolution: permissionMode === 'readonly',
5100
5003
  });
5004
+ const sessionToken = typeof safeSet[CODEX_SESSION_TOKEN_ENV] === 'string'
5005
+ ? safeSet[CODEX_SESSION_TOKEN_ENV]
5006
+ : undefined;
5007
+ for (const name of Object.keys(safeSet)) {
5008
+ const normalized = name.toUpperCase();
5009
+ if (normalized === AGENT_DELEGATION_TOKEN_ENV || normalized === CODEX_SESSION_TOKEN_ENV) {
5010
+ delete safeSet[name];
5011
+ }
5012
+ }
5013
+ if (sessionToken)
5014
+ safeSet[CODEX_SESSION_TOKEN_ENV] = sessionToken;
5015
+ const existingExclude = Array.isArray(policy.exclude)
5016
+ ? policy.exclude.filter((name) => typeof name === 'string')
5017
+ : [];
5018
+ const carriesTaskDelegation = permissionMode === 'fullaccess'
5019
+ && !!taskDelegationToken;
5020
+ if (carriesTaskDelegation)
5021
+ safeSet[AGENT_DELEGATION_TOKEN_ENV] = taskDelegationToken;
5022
+ const filteredExclude = existingExclude.filter(name => {
5023
+ const normalized = name.toUpperCase();
5024
+ return normalized !== AGENT_DELEGATION_TOKEN_ENV
5025
+ && normalized !== CODEX_SESSION_TOKEN_ENV;
5026
+ });
5027
+ const exclude = carriesTaskDelegation
5028
+ ? filteredExclude
5029
+ : [...new Set([...filteredExclude, AGENT_DELEGATION_TOKEN_ENV])];
5101
5030
  return {
5102
5031
  ...config,
5103
5032
  shell_environment_policy: {
5104
5033
  ...policy,
5105
5034
  set: safeSet,
5035
+ // The task token is invocation-scoped. Persistent non-fullaccess
5036
+ // threads must not inherit a stale value from the app-server process.
5037
+ exclude,
5106
5038
  },
5107
5039
  };
5108
5040
  }
@@ -5119,21 +5051,23 @@ export class CodexRunner {
5119
5051
  this.activeSessions.clear();
5120
5052
  this.provisionalSessions.clear();
5121
5053
  this.activeTurns.clear();
5054
+ for (const childThreadId of [...this.childThreadSessions.keys()])
5055
+ this.unregisterChildThread(childThreadId);
5122
5056
  this.activeSubagentTurns.clear();
5123
5057
  this.childThreadSessions.clear();
5124
5058
  this.threadProjectPaths.clear();
5125
5059
  this.threadExternalToolFingerprints.clear();
5126
5060
  this.threadExternalToolBoundaries.clear();
5127
5061
  this.trackedApprovalItems.clear();
5128
- this.threadDelegationCarriers.clear();
5129
5062
  this.pendingInterrupts.clear();
5063
+ for (const record of this.unconfirmedInterrupts.values())
5064
+ record.unsubscribe?.();
5065
+ this.unconfirmedInterrupts.clear();
5130
5066
  this.steeringTails.clear();
5131
5067
  this.permissionContexts.clear();
5132
5068
  this.chatModes.clear();
5133
5069
  this.pruneThreadOperationLocks();
5134
5070
  this.threadOperationLocks.clear();
5135
- for (const sessionId of [...this.sessionTempDirs.keys()])
5136
- this.cleanupSessionTempDir(sessionId);
5137
5071
  await Promise.all([
5138
5072
  this.appServerClient?.close(),
5139
5073
  this.fullAccessAppServerClient?.close(),
@@ -5172,6 +5106,14 @@ export class CodexAgentPlugin {
5172
5106
  },
5173
5107
  },
5174
5108
  };
5175
- return { evolagentName: agent.name, baseagent: 'codex', agent: new CodexRunner(merged, callbacks, { agentAid: agent.config.aid, agentConfig: agent.config }) };
5109
+ return {
5110
+ evolagentName: agent.name,
5111
+ baseagent: 'codex',
5112
+ agent: new CodexRunner(merged, callbacks, {
5113
+ agentAid: agent.config.aid,
5114
+ agentConfig: agent.config,
5115
+ codexVersion: availability.version,
5116
+ }),
5117
+ };
5176
5118
  }
5177
5119
  }