evolcore 0.0.25 → 0.0.27

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 (304) hide show
  1. package/CHANGELOG.md +632 -565
  2. package/LICENSE +21 -21
  3. package/README.md +455 -455
  4. package/bin/codex-managed-acl.ps1 +203 -160
  5. package/bin/codex-managed-hook.mjs +208 -208
  6. package/bin/ec-safe-output.js +327 -327
  7. package/bin/ec.js +40 -29
  8. package/bin/install-codex-managed-hooks.mjs +582 -555
  9. package/dist/agents/baseagent-availability.js +68 -0
  10. package/dist/agents/baseagent.js +96 -46
  11. package/dist/agents/claude-runner.js +172 -33
  12. package/dist/agents/codex/app-server-client.js +76 -31
  13. package/dist/agents/codex/catalog.js +1 -1
  14. package/dist/agents/codex/diagnostics.js +34 -1
  15. package/dist/agents/codex-runner.js +434 -446
  16. package/dist/agents/ecagent-runner.js +174 -34
  17. package/dist/agents/gemini-runner.js +8 -7
  18. package/dist/agents/runner-types.js +75 -19
  19. package/dist/aun/aid/agentmd.js +151 -36
  20. package/dist/aun/aid/client.js +1 -1
  21. package/dist/aun/aid/control-aid.js +180 -1
  22. package/dist/aun/aid/identity.js +4 -4
  23. package/dist/aun/aid/index.js +1 -1
  24. package/dist/aun/aid/store.js +17 -13
  25. package/dist/aun/group/history.js +303 -0
  26. package/dist/aun/group/identity.js +43 -0
  27. package/dist/aun/group/index.js +5 -0
  28. package/dist/aun/{msg/group.js → group/operations.js} +162 -39
  29. package/dist/aun/{menu-client.js → menu/client.js} +85 -6
  30. package/dist/aun/menu/index.js +2 -0
  31. package/dist/aun/menu/transport.js +49 -0
  32. package/dist/aun/msg/index.js +2 -1
  33. package/dist/aun/msg/managed-operation.js +85 -19
  34. package/dist/aun/msg/mention-schema.js +51 -0
  35. package/dist/aun/msg/p2p.js +19 -9
  36. package/dist/aun/msg/upload.js +43 -80
  37. package/dist/aun/outbox.js +29 -7
  38. package/dist/aun/rpc/client.js +1 -0
  39. package/dist/aun/storage/client.js +39 -50
  40. package/dist/channels/aun.js +842 -318
  41. package/dist/channels/daemon.js +10 -6
  42. package/dist/cli/agent-command.js +212 -89
  43. package/dist/cli/agent-create.js +626 -11
  44. package/dist/cli/agent.js +125 -13
  45. package/dist/cli/aun-commands.js +487 -337
  46. package/dist/cli/bench.js +20 -20
  47. package/dist/cli/config.js +55 -33
  48. package/dist/cli/contact.js +72 -15
  49. package/dist/cli/ctl-command.js +24 -24
  50. package/dist/cli/daemon-commands.js +140 -63
  51. package/dist/cli/diag.js +31 -0
  52. package/dist/cli/fs-command.js +70 -70
  53. package/dist/cli/handoff-command.js +46 -14
  54. package/dist/cli/index.js +91 -79
  55. package/dist/cli/init.js +34 -1
  56. package/dist/cli/link-rules.js +21 -21
  57. package/dist/cli/managed-command-guard.js +5 -1
  58. package/dist/cli/menu-client.js +51 -32
  59. package/dist/cli/model.js +3 -3
  60. package/dist/cli/net-check.js +22 -22
  61. package/dist/cli/queue-command.js +31 -31
  62. package/dist/cli/response.js +32 -32
  63. package/dist/cli/restart-monitor.js +103 -42
  64. package/dist/cli/stats.js +51 -51
  65. package/dist/cli/task-command.js +59 -0
  66. package/dist/cli/task-context.js +97 -49
  67. package/dist/cli/trigger-command.js +74 -64
  68. package/dist/config/builtin-roles.js +11 -0
  69. package/dist/config/config-field-policy.js +11 -0
  70. package/dist/config/config-manager.js +32 -4
  71. package/dist/config/config-operation-service.js +160 -0
  72. package/dist/config/contact-book-store.js +1 -1
  73. package/dist/config/contact-book.js +1 -1
  74. package/dist/config/contact-operation-service.js +97 -8
  75. package/dist/config/peer-role-resolver.js +1 -1
  76. package/dist/config/resolved-config-op.js +30 -7
  77. package/dist/config-store.js +1 -1
  78. package/dist/core/agent-application-service.js +7 -1
  79. package/dist/core/audit/event-key.js +17 -2
  80. package/dist/core/audit/log-integrity.js +34 -8
  81. package/dist/core/auth/agent-delegation.js +48 -14
  82. package/dist/core/auth/auth-gateway.js +2 -0
  83. package/dist/core/auth/authorization-audit.js +53 -49
  84. package/dist/core/auth/operation-authorizer.js +90 -7
  85. package/dist/core/auth/operation-catalog.js +145 -4
  86. package/dist/core/auth/remote-access-store.js +155 -10
  87. package/dist/core/bootstrap-messages.js +66 -7
  88. package/dist/core/bootstrap-service.js +111 -20
  89. package/dist/core/command/agent-control.js +88 -7
  90. package/dist/core/command/agent-storage-menu.js +443 -0
  91. package/dist/core/command/cli-intent-parser.js +19 -0
  92. package/dist/core/command/command-handler.js +159 -65
  93. package/dist/core/command/data-root-resolver.js +273 -0
  94. package/dist/core/command/diag-service.js +118 -0
  95. package/dist/core/command/group-menu.js +7 -2
  96. package/dist/core/command/menu-catalog.js +46 -7
  97. package/dist/core/command/menu-handler.js +392 -100
  98. package/dist/core/command/menu-protocol.js +45 -6
  99. package/dist/core/command/menu-token-store.js +41 -8
  100. package/dist/core/command/role-menu.js +2 -2
  101. package/dist/core/command/slash-gate.js +34 -4
  102. package/dist/core/command/slash-handler.js +123 -43
  103. package/dist/core/event-catalog.js +4 -0
  104. package/dist/core/evolagent-registry.js +28 -2
  105. package/dist/core/group/invite-service.js +924 -0
  106. package/dist/core/handoff/runtime.js +205 -27
  107. package/dist/core/handoff/store.js +193 -4
  108. package/dist/core/interaction-router.js +22 -8
  109. package/dist/core/message/im-renderer.js +146 -34
  110. package/dist/core/message/message-bridge.js +163 -20
  111. package/dist/core/message/message-log.js +5 -3
  112. package/dist/core/message/message-queue.js +99 -41
  113. package/dist/core/message/message-utils.js +1 -0
  114. package/dist/core/message/response-engine.js +755 -144
  115. package/dist/core/model/catalog-service.js +79 -49
  116. package/dist/core/model/context-window-suffix.js +33 -0
  117. package/dist/core/model/model-catalog.js +1 -1
  118. package/dist/core/model/model-permission.js +12 -3
  119. package/dist/core/permission/approval-gateway.js +36 -3
  120. package/dist/core/permission/ec-command-parser.js +112 -41
  121. package/dist/core/permission/protected-paths.js +134 -16
  122. package/dist/core/permission/sandbox-runtime.js +108 -17
  123. package/dist/core/permission/tool-error-code.js +15 -0
  124. package/dist/core/permission/tool-policy.js +9 -8
  125. package/dist/core/relation/peer-identity.js +57 -6
  126. package/dist/core/relation/shared-data.js +106 -0
  127. package/dist/core/role/runtime-policy.js +64 -0
  128. package/dist/core/runtime-lock.js +10 -26
  129. package/dist/core/session/runner-task-history.js +170 -0
  130. package/dist/core/session/session-manager.js +34 -6
  131. package/dist/core/session/session-mapper.js +41 -0
  132. package/dist/core/session/session-turn-coordinator.js +37 -7
  133. package/dist/core/session/session-turns.js +48 -7
  134. package/dist/core/task/task-recovery-service.js +176 -0
  135. package/dist/core/task/task-resume-operation.js +32 -0
  136. package/dist/eck/group-rules-sync.js +5 -3
  137. package/dist/eck/kit-renderer.js +1 -0
  138. package/dist/index.js +588 -217
  139. package/dist/ipc.js +193 -15
  140. package/dist/paths.js +17 -4
  141. package/dist/product.js +2 -0
  142. package/dist/response-system/engines/v1/proactive-flow.js +59 -2
  143. package/dist/stats/billing.js +41 -15
  144. package/dist/stats/db.js +159 -159
  145. package/dist/stats/eck-vars.js +9 -9
  146. package/dist/stats/price-resolver.js +28 -10
  147. package/dist/stats/query.js +139 -139
  148. package/dist/stats/role-budget.js +6 -6
  149. package/dist/stats/writer.js +51 -51
  150. package/dist/trigger/event-source.js +19 -0
  151. package/dist/trigger/feedback.js +233 -40
  152. package/dist/trigger/parser.js +55 -5
  153. package/dist/trigger/patch.js +9 -0
  154. package/dist/trigger/scheduler.js +30 -4
  155. package/dist/trigger/validation.js +20 -3
  156. package/dist/utils/cross-platform.js +30 -1
  157. package/dist/utils/ecweb-utils.js +1 -0
  158. package/dist/utils/error-dict.json +160 -160
  159. package/dist/utils/error-utils.js +61 -2
  160. package/dist/utils/evolcore-version.js +129 -0
  161. package/dist/utils/logger.js +26 -14
  162. package/dist/utils/macos-autostart.js +22 -22
  163. package/dist/utils/model-prices.jsonl +23 -23
  164. package/dist/utils/npm-ops.js +176 -35
  165. package/dist/utils/tool-summary.js +59 -3
  166. package/dist/utils/welcome.js +196 -23
  167. package/ecagent/LICENSE +21 -21
  168. package/ecagent/NOTICE +11 -11
  169. package/ecagent/README.md +61 -61
  170. package/ecagent/dist/harness/agent-harness.js +3 -0
  171. package/ecagent/dist/harness/compaction/branch-summarization.js +30 -30
  172. package/ecagent/dist/harness/compaction/compaction.js +83 -83
  173. package/ecagent/dist/harness/messages.js +7 -7
  174. package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
  175. package/kits/docs/GUIDE.md +20 -20
  176. package/kits/docs/INDEX.md +60 -66
  177. package/kits/docs/aun/CHEATSHEET.md +19 -19
  178. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -15
  179. package/kits/docs/channels/aun.md +69 -56
  180. package/kits/docs/channels/feishu.md +56 -56
  181. package/kits/docs/context-assembly.md +379 -367
  182. package/kits/docs/eck_templates/GUIDE.template.md +22 -22
  183. package/kits/docs/eck_templates/INDEX.template.md +28 -28
  184. package/kits/docs/eck_templates/path-registry.template.md +33 -33
  185. package/kits/docs/eck_templates/runtime.template.md +19 -19
  186. package/kits/docs/evolcore/INDEX.md +76 -71
  187. package/kits/docs/evolcore/agent-create.md +202 -165
  188. package/kits/docs/evolcore/agent.md +99 -67
  189. package/kits/docs/evolcore/aid.md +68 -60
  190. package/kits/docs/evolcore/codex-windows-sandbox.md +146 -146
  191. package/kits/docs/evolcore/config.md +241 -195
  192. package/kits/docs/evolcore/contact.md +75 -68
  193. package/kits/docs/evolcore/ctl.md +46 -46
  194. package/kits/docs/evolcore/event.md +216 -216
  195. package/kits/docs/evolcore/fs-architecture.md +1215 -1215
  196. package/kits/docs/evolcore/fs.md +110 -110
  197. package/kits/docs/evolcore/group-collaboration.md +244 -244
  198. package/kits/docs/evolcore/group-fs.md +17 -17
  199. package/kits/docs/evolcore/group-rules.md +251 -251
  200. package/kits/docs/evolcore/group.md +267 -152
  201. package/kits/docs/evolcore/menu-client.md +58 -34
  202. package/kits/docs/evolcore/model.md +50 -50
  203. package/kits/docs/evolcore/msg.md +169 -165
  204. package/kits/docs/evolcore/response.md +75 -75
  205. package/kits/docs/evolcore/rpc.md +37 -37
  206. package/kits/docs/evolcore/self-summary.md +29 -29
  207. package/kits/docs/evolcore/stats.md +83 -83
  208. package/kits/docs/evolcore/storage.md +53 -50
  209. package/kits/docs/evolcore/task.md +14 -0
  210. package/kits/docs/evolcore/trigger.md +623 -575
  211. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -26
  212. package/kits/docs/identity/PATH_OPS.md +16 -16
  213. package/kits/docs/identity/ROLE_DETAIL.md +25 -25
  214. package/kits/docs/identity/identity-tools.md +26 -26
  215. package/kits/docs/path-registry.md +62 -62
  216. package/kits/docs/prompt-loading-architecture.md +269 -269
  217. package/kits/docs/venues/aun-group.md +45 -45
  218. package/kits/docs/venues/aun-private.md +10 -10
  219. package/kits/docs/venues/client-desktop.md +10 -10
  220. package/kits/docs/venues/client-mobile.md +10 -10
  221. package/kits/docs/venues/feishu-group.md +13 -13
  222. package/kits/docs/venues/feishu-private.md +9 -9
  223. package/kits/docs/venues/group.md +42 -25
  224. package/kits/docs/venues/private.md +10 -10
  225. package/kits/eck_manifest.auxiliary.json +43 -43
  226. package/kits/eck_manifest.json +217 -190
  227. package/kits/eck_message_manifest.json +74 -74
  228. package/kits/migrations/README-role-config-v4.md +32 -32
  229. package/kits/migrations/migrate-contact-book-v2.mjs +748 -748
  230. package/kits/migrations/migrate-role-config-v4.mjs +614 -614
  231. package/kits/migrations/migrate-role-config-v5.mjs +365 -365
  232. package/kits/migrations/rename-config-file.mjs +99 -99
  233. package/kits/rules/01-overview.md +166 -162
  234. package/kits/rules/02-navigation.md +96 -96
  235. package/kits/rules/03-identity.md +36 -36
  236. package/kits/rules/04-relation.md +76 -75
  237. package/kits/rules/05-venue.md +60 -60
  238. package/kits/rules/06-channel.md +65 -63
  239. package/kits/schemas/_meta.json +53 -53
  240. package/kits/schemas/agent-config.schema.1.json +177 -177
  241. package/kits/schemas/agent-config.schema.10.json +395 -395
  242. package/kits/schemas/agent-config.schema.11.json +421 -421
  243. package/kits/schemas/agent-config.schema.12.json +451 -427
  244. package/kits/schemas/agent-config.schema.2.json +239 -239
  245. package/kits/schemas/agent-config.schema.3.json +119 -119
  246. package/kits/schemas/agent-config.schema.4.json +208 -208
  247. package/kits/schemas/agent-config.schema.5.json +326 -326
  248. package/kits/schemas/agent-config.schema.6.json +322 -322
  249. package/kits/schemas/agent-config.schema.7.json +303 -303
  250. package/kits/schemas/agent-config.schema.8.json +304 -304
  251. package/kits/schemas/agent-config.schema.9.json +364 -364
  252. package/kits/schemas/contact-book.schema.1.json +36 -36
  253. package/kits/schemas/contact-book.schema.2.json +43 -43
  254. package/kits/schemas/contact-book.schema.3.json +67 -67
  255. package/kits/schemas/daemon.schema.1.json +91 -91
  256. package/kits/schemas/daemon.schema.2.json +101 -101
  257. package/kits/schemas/daemon.schema.3.json +123 -123
  258. package/kits/schemas/daemon.schema.4.json +136 -136
  259. package/kits/schemas/daemon.schema.5.json +135 -135
  260. package/kits/schemas/daemon.schema.6.json +131 -131
  261. package/kits/schemas/defaults.schema.1.json +81 -81
  262. package/kits/schemas/defaults.schema.2.json +85 -85
  263. package/kits/schemas/defaults.schema.3.json +73 -73
  264. package/kits/schemas/defaults.schema.4.json +107 -107
  265. package/kits/schemas/defaults.schema.5.json +119 -119
  266. package/kits/schemas/menu-exec-schema-commands.md +208 -208
  267. package/kits/schemas/migrations/README.md +30 -30
  268. package/kits/schemas/relation-config.schema.1.json +158 -158
  269. package/kits/schemas/relation-config.schema.2.json +73 -73
  270. package/kits/schemas/relation-config.schema.3.json +50 -50
  271. package/kits/schemas/relation-config.schema.4.json +47 -47
  272. package/kits/schemas/relation-config.schema.5.json +47 -47
  273. package/kits/schemas/relation-config.schema.6.json +46 -46
  274. package/kits/schemas/relation-config.schema.7.json +59 -59
  275. package/kits/schemas/relation-config.schema.8.json +103 -103
  276. package/kits/schemas/role-config.schema.1.json +215 -201
  277. package/kits/schemas/role-registry.schema.1.json +35 -35
  278. package/kits/schemas/single-session.schema.1.json +31 -31
  279. package/kits/schemas/single-session.schema.2.json +57 -57
  280. package/kits/schemas/single-session.schema.3.json +32 -32
  281. package/kits/templates/bootstrap-welcome.md +17 -17
  282. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -9
  283. package/kits/templates/message-fragments/handoff-request-to-target.md +19 -19
  284. package/kits/templates/message-fragments/handoff-response-to-origin.md +9 -9
  285. package/kits/templates/message-fragments/inject-default.md +2 -2
  286. package/kits/templates/message-fragments/item.md +2 -2
  287. package/kits/templates/roles/admin.json +92 -92
  288. package/kits/templates/roles/member.json +101 -96
  289. package/kits/templates/roles/owner.json +9 -9
  290. package/kits/templates/roles/visitor.json +90 -85
  291. package/kits/templates/system-fragments/baseagent.md +14 -14
  292. package/kits/templates/system-fragments/bootstrap.md +53 -34
  293. package/kits/templates/system-fragments/channel.md +28 -64
  294. package/kits/templates/system-fragments/commands.md +36 -28
  295. package/kits/templates/system-fragments/group-unread-history.md +7 -0
  296. package/kits/templates/system-fragments/identity.md +13 -13
  297. package/kits/templates/system-fragments/relation.md +19 -19
  298. package/kits/templates/system-fragments/reply.md +56 -0
  299. package/kits/templates/system-fragments/session.md +59 -59
  300. package/kits/templates/system-fragments/venue.md +31 -31
  301. package/package.json +104 -98
  302. package/dist/aun/group-identity.js +0 -9
  303. /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
  304. /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
@@ -1,24 +1,26 @@
1
1
  import { getRunnerCatalog, catalogEfforts, assertCatalogSelectable } from '../core/model/model-catalog.js';
2
- import { hasTrustedFullAccessContext } from './runner-types.js';
2
+ // Codex/OpenAI API 不认识 Claude Code SDK 的窗口后缀,必须传入基础模型 ID。
3
+ import { stripContextWindowSuffix } from '../core/model/context-window-suffix.js';
4
+ import { formatAsyncQuestionAnswer, hasTrustedFullAccessContext, parseAsyncUserQuestions, unsettledInterruptError, withInterruptDeadline } from './runner-types.js';
3
5
  import { requestDangerousCommandPermission } from '../core/permission/approval-gateway.js';
4
6
  import { checkReadonly, checkDangerousCommand, evaluateToolPreflight } from '../core/permission/tool-policy.js';
5
7
  import { normalizeExecutionPermissionMode, normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
6
- import { buildCodexProtectedFilesystemRules, isSameOrDescendant, resolveProtectedCandidate } from '../core/permission/protected-paths.js';
8
+ import { assertWindowsCodexFilesystemRuleBudget, buildCodexProtectedFilesystemRules, isSameOrDescendant, resolveProtectedCandidate, } from '../core/permission/protected-paths.js';
7
9
  import { CODEX_THREAD_ACTIVE_WRITER, CodexAppServerClient, } from './codex/app-server-client.js';
8
10
  import { resolveOpenaiConfig, resolveCodexCliPath } from './baseagent.js';
9
11
  import { logger } from '../utils/logger.js';
10
12
  import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
11
13
  import { auditCodexApprovalDecision, auditPermissionLifecycle, auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
12
- import { isRetryableError } from '../utils/error-utils.js';
14
+ import { classifyError, ErrorType, isOutputTokenLimitError, isRetryableError } from '../utils/error-utils.js';
13
15
  import { renderActionAsText } from '../core/interaction-router.js';
14
16
  import { buildEnvelope, isInteractionSendAccepted, sendInteractionPayload } from '../core/message/message-utils.js';
15
17
  import { resolveCodexCapabilityThreadConfigForProject } from '../core/capability/capability-manager.js';
16
- 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';
17
19
  import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
18
- 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';
19
21
  import { compareVersions } from '../utils/npm-ops.js';
20
22
  import { agentSessionsDir, resolvePaths, resolveRoot } from '../paths.js';
21
- import { ensureProcessManagedTempDir, getManagedTaskTempDir } from '../cli/task-context.js';
23
+ import { ensureSessionRuntimeDir, getManagedTaskTempDir } from '../cli/task-context.js';
22
24
  import { buildSessionTurnList } from '../core/session/session-turns.js';
23
25
  import { CodexSessionFileAdapter } from '../core/session/adapters/codex-session-file-adapter.js';
24
26
  import { execFileSync } from 'child_process';
@@ -36,19 +38,23 @@ const MIME_EXT = {
36
38
  'image/webp': '.webp',
37
39
  };
38
40
  const APPROVAL_LOG_SUMMARY_MAX_LENGTH = 256;
41
+ const CODEX_INTERRUPT_TIMEOUT_MS = 5_000;
42
+ function toAgentErrorType(message) {
43
+ switch (classifyError(new Error(message))) {
44
+ case ErrorType.CONTEXT_TOO_LONG: return 'context_too_long';
45
+ case ErrorType.AUTH_ERROR: return 'auth';
46
+ case ErrorType.API_ERROR:
47
+ case ErrorType.STREAM_ERROR:
48
+ case ErrorType.SDK_TIMEOUT: return 'network';
49
+ default: return 'unknown';
50
+ }
51
+ }
39
52
  export function formatApprovalLogSummary(summary) {
40
53
  const singleLine = summary.replace(/\s+/gu, ' ').trim();
41
54
  if (singleLine.length <= APPROVAL_LOG_SUMMARY_MAX_LENGTH)
42
55
  return singleLine;
43
56
  return singleLine.slice(0, APPROVAL_LOG_SUMMARY_MAX_LENGTH - 3).trimEnd() + '...';
44
57
  }
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
- }
52
58
  // ── Codex 模型目录(动态获取,含 effort) ──
53
59
  class AsyncEventQueue {
54
60
  queue = [];
@@ -103,60 +109,6 @@ function isCodexReasoningNotification(method) {
103
109
  || method === 'item/reasoning/summaryPartAdded';
104
110
  }
105
111
  const CODEX_THREAD_HANDOFF_TIMEOUT_MS = 2_000;
106
- const managedTempDirsForExitCleanup = new Map();
107
- let managedTempExitCleanupRegistered = false;
108
- function isTrustedManagedTempDirectory(directory, parent) {
109
- try {
110
- const parentPath = path.resolve(parent);
111
- const directoryPath = path.resolve(directory);
112
- // The session directory must be a direct child of the private parent.
113
- // This lexical check is required before canonicalization so a replaced
114
- // parent symlink cannot turn an old session path into another tree.
115
- if (path.dirname(directoryPath) !== parentPath)
116
- return false;
117
- const trustedDirectory = (candidate) => {
118
- const stat = fs.lstatSync(candidate);
119
- if (!stat.isDirectory() || stat.isSymbolicLink())
120
- return false;
121
- if (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid())
122
- return false;
123
- return process.platform === 'win32' || (stat.mode & 0o077) === 0;
124
- };
125
- if (!trustedDirectory(parentPath) || !trustedDirectory(directoryPath))
126
- return false;
127
- const canonicalParent = resolveProtectedCandidate(parentPath);
128
- const canonicalDirectory = resolveProtectedCandidate(directoryPath);
129
- // Keep the canonical and lexical paths identical after the lstat checks;
130
- // this rejects symlink replacement in either node or an existing ancestor.
131
- return canonicalParent === parentPath
132
- && canonicalDirectory === directoryPath
133
- && canonicalDirectory !== canonicalParent
134
- && path.dirname(canonicalDirectory) === canonicalParent
135
- && isSameOrDescendant(canonicalDirectory, canonicalParent);
136
- }
137
- catch {
138
- return false;
139
- }
140
- }
141
- function registerManagedTempDirForExitCleanup(directory, parent) {
142
- managedTempDirsForExitCleanup.set(directory, parent);
143
- if (managedTempExitCleanupRegistered)
144
- return;
145
- managedTempExitCleanupRegistered = true;
146
- process.once('exit', () => {
147
- for (const [candidatePath, parentPath] of managedTempDirsForExitCleanup) {
148
- try {
149
- if (isTrustedManagedTempDirectory(candidatePath, parentPath)) {
150
- fs.rmSync(path.resolve(candidatePath), { recursive: true, force: true });
151
- }
152
- }
153
- catch {
154
- // Process exit cleanup is best effort; session cleanup logs failures
155
- // while the runner is still active.
156
- }
157
- }
158
- });
159
- }
160
112
  function stableSecurityValue(value) {
161
113
  if (value === undefined)
162
114
  return 'undefined';
@@ -198,11 +150,13 @@ function parseManagedPreToolUseDenial(value) {
198
150
  .trim();
199
151
  if (!reason)
200
152
  reason = 'EvolCore PreToolUse policy denied this tool call';
201
- const policyCode = /工具调用已达到\s*\d+\s*次/.test(reason)
202
- ? 'proactive_tool_report_required'
203
- : /请先用\s+ec\s+(?:group\s+send|msg\s+send)/.test(reason)
204
- ? 'proactive_first_send_required'
205
- : 'managed_pre_tool_use_denied';
153
+ const policyCode = /当前汇报命令(?:未通过规范解析|不符合当前会话)/.test(reason)
154
+ ? 'proactive_send_invalid'
155
+ : /工具调用已达到\s*\d+\s*次/.test(reason)
156
+ ? 'proactive_tool_report_required'
157
+ : /请先用\s+ec\s+(?:group\s+send|msg\s+send)/.test(reason)
158
+ ? 'proactive_first_send_required'
159
+ : 'managed_pre_tool_use_denied';
206
160
  return { policyCode, reason };
207
161
  }
208
162
  function outputTextForManagedHook(value) {
@@ -227,7 +181,6 @@ function outputTextForManagedHook(value) {
227
181
  }
228
182
  return String(value);
229
183
  }
230
- const CODEX_DELEGATION_CARRIER_METADATA_KEY = '__codexDelegationCarrierV1';
231
184
  const CODEX_NO_ROLLOUT_RE = /no rollout found for thread id/i;
232
185
  const CODEX_ACTIVE_WRITER_RE = /already has an active writer/i;
233
186
  function isCodexNoRolloutError(error) {
@@ -354,7 +307,6 @@ export async function getCodexAppServerAvailabilityAsync() {
354
307
  : `Codex CLI 探测失败:${error instanceof Error ? error.message : String(error)}` };
355
308
  }
356
309
  }
357
- export const CODEX_SESSION_TOKEN_ENV = 'EVOLCORE_CODEX_SESSION_TOKEN';
358
310
  export class CodexRunner {
359
311
  name = 'codex';
360
312
  // turn/start persists a new user turn before runQuery resolves. App-server
@@ -373,14 +325,12 @@ export class CodexRunner {
373
325
  threadProjectPaths = new Map();
374
326
  /** Child Codex threads inherit the EvolCore session that started them. */
375
327
  childThreadSessions = new Map();
376
- sessionTempDirs = new Map();
377
- managedTempNonce = randomBytes(16).toString('hex');
378
328
  threadExternalToolFingerprints = new Map();
379
329
  threadExternalToolBoundaries = new Map();
380
330
  trackedApprovalItems = new Map();
381
331
  pendingToolDenials = new Map();
382
- threadDelegationCarriers = new Map();
383
332
  pendingInterrupts = new Map();
333
+ unconfirmedInterrupts = new Map();
384
334
  steeringTails = new Map();
385
335
  threadOperationLocks = new Map();
386
336
  activeOperationReleases = new Map();
@@ -403,6 +353,9 @@ export class CodexRunner {
403
353
  codexSessionFileAdapter = new CodexSessionFileAdapter();
404
354
  /** threadId → managed-hook credential, issued by this runner's agent only. */
405
355
  threadCredentials = new Map();
356
+ /** @deprecated Compatibility alias for direct runner integrations. */
357
+ threadDelegationCarriers = this.threadCredentials;
358
+ asyncQuestionItems = new Set();
406
359
  constructor(config, callbacks, runtime) {
407
360
  // Compatibility for legacy direct callers that still pass the removed
408
361
  // internal fields in a synthetic config. AgentLoader uses runtime binding.
@@ -419,6 +372,7 @@ export class CodexRunner {
419
372
  configuredHeaders: this.resolvedConfig.headers,
420
373
  });
421
374
  this.capabilities = {
375
+ taskRecovery: true,
422
376
  clear: false,
423
377
  compact: true,
424
378
  fork: true,
@@ -442,6 +396,7 @@ export class CodexRunner {
442
396
  return existing;
443
397
  const client = new CodexAppServerClient({
444
398
  apiKey: this.resolvedConfig.apiKey,
399
+ authType: this.resolvedConfig.authType,
445
400
  baseUrl: this.resolvedConfig.baseUrl,
446
401
  provider: this.resolvedConfig.provider,
447
402
  headers: this.resolvedConfig.headers,
@@ -634,16 +589,13 @@ export class CodexRunner {
634
589
  }
635
590
  this.threadSubscriptions.set(threadId, { client: targetClient, state: 'subscribed' });
636
591
  }
637
- createDelegationCarrier() {
638
- return randomBytes(32).toString('base64url');
639
- }
640
- approvedLiteralEvolcoreCommand(command, managedTempDir, dialect = 'posix', expectedDelegationToken, executable) {
592
+ approvedLiteralEvolcoreCommand(command, managedTempDir, dialect = 'posix', expectedCredentialToken, executable) {
641
593
  const parseOptions = {
642
594
  allowManagedTmpDir: true,
643
595
  dialect,
644
596
  verifyShellExecutable: process.platform === 'win32',
645
597
  ...(managedTempDir ? { managedTempDir } : {}),
646
- ...(expectedDelegationToken !== undefined ? { expectedDelegationToken } : {}),
598
+ ...(expectedCredentialToken !== undefined ? { expectedCredentialToken } : {}),
647
599
  };
648
600
  const classification = classifyEvolcoreShellCommand(command, parseOptions);
649
601
  if (classification.kind === 'bounded-output')
@@ -670,10 +622,10 @@ export class CodexRunner {
670
622
  }
671
623
  return {
672
624
  argv: normalizeEvolcoreCommandArgv(parsed.argv),
673
- ...(parsed.delegationToken ? { delegationToken: parsed.delegationToken } : {}),
625
+ ...(parsed.credentialToken ? { credentialToken: parsed.credentialToken } : {}),
674
626
  };
675
627
  }
676
- resolveCanonicalEcApproval(toolInput, managedTempDir, expectedDelegationToken) {
628
+ resolveCanonicalEcApproval(toolInput, managedTempDir, expectedCredentialToken) {
677
629
  const explicitArgv = Array.isArray(toolInput.commandArgv)
678
630
  && toolInput.commandArgv.every(value => typeof value === 'string')
679
631
  ? toolInput.commandArgv
@@ -694,12 +646,12 @@ export class CodexRunner {
694
646
  ? resolveCodexShellCarrierArgv(explicitArgv)
695
647
  : undefined;
696
648
  if (explicitCarrier !== undefined) {
697
- return this.approvedLiteralEvolcoreCommand(explicitCarrier.command, managedTempDir, explicitCarrier.dialect, expectedDelegationToken, explicitCarrier.executable);
649
+ return this.approvedLiteralEvolcoreCommand(explicitCarrier.command, managedTempDir, explicitCarrier.dialect, expectedCredentialToken, explicitCarrier.executable);
698
650
  }
699
651
  if (hasCodexCmdCarrierEcIntent(toolInput)) {
700
652
  const parsed = parseCodexToolCommand(toolInput, {
701
653
  verifyShellExecutable: process.platform === 'win32',
702
- ...(expectedDelegationToken !== undefined ? { expectedDelegationToken } : {}),
654
+ ...(expectedCredentialToken !== undefined ? { expectedCredentialToken } : {}),
703
655
  });
704
656
  if (!parsed.ok)
705
657
  return { parseFailure: parsed };
@@ -711,7 +663,7 @@ export class CodexRunner {
711
663
  } }
712
664
  : {
713
665
  argv: normalizeEvolcoreCommandArgv(parsed.argv),
714
- ...(parsed.delegationToken ? { delegationToken: parsed.delegationToken } : {}),
666
+ ...(parsed.credentialToken ? { credentialToken: parsed.credentialToken } : {}),
715
667
  };
716
668
  }
717
669
  const command = typeof toolInput.command === 'string' ? toolInput.command : '';
@@ -731,7 +683,7 @@ export class CodexRunner {
731
683
  allowManagedTmpDir: true,
732
684
  verifyShellExecutable: process.platform === 'win32',
733
685
  ...(managedTempDir ? { managedTempDir } : {}),
734
- ...(expectedDelegationToken !== undefined ? { expectedDelegationToken } : {}),
686
+ ...(expectedCredentialToken !== undefined ? { expectedCredentialToken } : {}),
735
687
  });
736
688
  if (canonicalCommand?.[0] === 'ec') {
737
689
  return containsLiteralManagedTmpDirOutsideSendContent(canonicalCommand)
@@ -766,7 +718,7 @@ export class CodexRunner {
766
718
  return true;
767
719
  const shellCarrier = resolveCodexShellCarrierArgv(rawArgv);
768
720
  if (shellCarrier?.dialect === 'powershell'
769
- && hasPowerShellDelegationAssignment(shellCarrier.command))
721
+ && hasPowerShellCredentialAssignment(shellCarrier.command))
770
722
  return true;
771
723
  return hasCodexCmdCarrierEcIntent(toolInput)
772
724
  || shellCarrier !== undefined
@@ -779,20 +731,20 @@ export class CodexRunner {
779
731
  }
780
732
  armApprovedDelegationCommand(sessionId, argv) {
781
733
  const threadId = this.activeSessions.get(sessionId) ?? this.provisionalSessions.get(sessionId);
782
- const carrierToken = threadId ? this.threadDelegationCarriers.get(threadId) : undefined;
734
+ const sessionToken = threadId ? this.ensureThreadCredentialLoaded(threadId) : undefined;
783
735
  const commandHash = hashDelegatedCommandArgv(argv);
784
736
  const arm = this.permissionContexts.get(sessionId)?.armApprovedDelegationCommand;
785
- if (!carrierToken || !commandHash || !arm)
737
+ if (!sessionToken || !commandHash || !arm)
786
738
  return false;
787
739
  try {
788
- return arm(carrierToken, commandHash);
740
+ return arm(sessionToken, commandHash);
789
741
  }
790
742
  catch (error) {
791
743
  logger.warn(`[CodexRunner] delegation arm callback failed: session=${sessionId} error=${error instanceof Error ? error.message : String(error)}`);
792
744
  return false;
793
745
  }
794
746
  }
795
- delegationCarrierForSession(sessionId, threadId) {
747
+ sessionTokenForSession(sessionId, threadId) {
796
748
  const candidates = [
797
749
  threadId,
798
750
  this.activeSessions.get(sessionId),
@@ -801,45 +753,13 @@ export class CodexRunner {
801
753
  for (const candidate of candidates) {
802
754
  if (!candidate)
803
755
  continue;
804
- const carrier = this.threadDelegationCarriers.get(candidate);
805
- if (carrier)
806
- return carrier;
756
+ const token = this.ensureThreadCredentialLoaded(candidate);
757
+ if (token)
758
+ return token;
807
759
  }
808
760
  return undefined;
809
761
  }
810
- async readThreadDelegationCarrier(appServer, threadId) {
811
- try {
812
- const result = await appServer.threadShellCommand(threadId, `node -e "process.stdout.write(process.env.${AGENT_DELEGATION_TOKEN_ENV}||String())"`);
813
- const carrier = result.output.trim();
814
- if (result.exitCode !== undefined && result.exitCode !== 0)
815
- return undefined;
816
- return /^[A-Za-z0-9_-]{32,128}$/.test(carrier) ? carrier : undefined;
817
- }
818
- catch (error) {
819
- logger.warn(`[CodexRunner] failed to recover delegation carrier for loaded thread ${threadId}: ${error instanceof Error ? error.message : String(error)}`);
820
- return undefined;
821
- }
822
- }
823
- async loadPersistedDelegationCarrier(sessionManager, sessionId, threadId) {
824
- if (typeof sessionManager?.getSessionById !== 'function')
825
- return undefined;
826
- try {
827
- const session = await sessionManager.getSessionById(sessionId);
828
- const persisted = session?.metadata?.[CODEX_DELEGATION_CARRIER_METADATA_KEY];
829
- if (!persisted || typeof persisted !== 'object' || Array.isArray(persisted))
830
- return undefined;
831
- if (persisted.threadId !== threadId || typeof persisted.carrier !== 'string')
832
- return undefined;
833
- return /^[A-Za-z0-9_-]{32,128}$/.test(persisted.carrier)
834
- ? persisted.carrier
835
- : undefined;
836
- }
837
- catch (error) {
838
- logger.debug(`[CodexRunner] failed to load persisted delegation carrier: session=${sessionId} error=${error}`);
839
- return undefined;
840
- }
841
- }
842
- async commitProvisionalThread(sessionId, threadId, carrierToPublish, sessionManager, turn) {
762
+ async commitProvisionalThread(sessionId, threadId, turn) {
843
763
  if (this.provisionalSessions.get(sessionId) !== threadId)
844
764
  return false;
845
765
  const bindingResult = this.onSessionIdUpdate
@@ -863,10 +783,6 @@ export class CodexRunner {
863
783
  return false;
864
784
  this.provisionalSessions.delete(sessionId);
865
785
  this.activeSessions.set(sessionId, threadId);
866
- if (carrierToPublish) {
867
- this.threadDelegationCarriers.set(carrierToPublish.threadId, carrierToPublish.carrier);
868
- await this.persistDelegationCarrier(sessionManager, sessionId, carrierToPublish.threadId, carrierToPublish.carrier);
869
- }
870
786
  this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${threadId}`);
871
787
  return true;
872
788
  }
@@ -899,9 +815,9 @@ export class CodexRunner {
899
815
  return;
900
816
  this.provisionalSessions.delete(sessionId);
901
817
  this.threadProjectPaths.delete(threadId);
902
- this.threadDelegationCarriers.delete(threadId);
903
818
  this.unbindExternalToolBoundary(threadId);
904
819
  this.clearTrackedApprovalItemsForThread(threadId);
820
+ this.dropThreadCredential(threadId);
905
821
  // A stale provisional start can finish after a replacement run has
906
822
  // already activated another parent thread for this session. Do not tear
907
823
  // down the replacement run's child mappings in that race.
@@ -911,31 +827,6 @@ export class CodexRunner {
911
827
  }
912
828
  this.pruneThreadOperationLocks();
913
829
  }
914
- async persistDelegationCarrier(sessionManager, sessionId, threadId, carrier) {
915
- if (typeof sessionManager?.getSessionById !== 'function')
916
- return;
917
- try {
918
- const session = await sessionManager.getSessionById(sessionId);
919
- if (!session)
920
- return;
921
- const current = session.metadata?.[CODEX_DELEGATION_CARRIER_METADATA_KEY];
922
- if (current?.threadId === threadId && current?.carrier === carrier)
923
- return;
924
- if (typeof sessionManager.patchSessionMetadata === 'function') {
925
- await sessionManager.patchSessionMetadata(sessionId, {
926
- [CODEX_DELEGATION_CARRIER_METADATA_KEY]: { threadId, carrier },
927
- });
928
- }
929
- else if (typeof sessionManager.updateSession === 'function') {
930
- await sessionManager.updateSession(sessionId, {
931
- metadata: { [CODEX_DELEGATION_CARRIER_METADATA_KEY]: { threadId, carrier } },
932
- });
933
- }
934
- }
935
- catch (error) {
936
- logger.warn(`[CodexRunner] failed to persist delegation carrier: session=${sessionId} thread=${threadId} error=${error}`);
937
- }
938
- }
939
830
  resetAppServerClient() {
940
831
  const clients = [this.appServerClient, this.fullAccessAppServerClient]
941
832
  .filter((client) => !!client);
@@ -944,8 +835,8 @@ export class CodexRunner {
944
835
  this.threadSubscriptions.clear();
945
836
  this.threadExternalToolFingerprints.clear();
946
837
  this.threadExternalToolBoundaries.clear();
947
- for (const childThreadId of this.childThreadSessions.keys())
948
- this.threadProjectPaths.delete(childThreadId);
838
+ for (const childThreadId of [...this.childThreadSessions.keys()])
839
+ this.unregisterChildThread(childThreadId);
949
840
  // Child turn ids belong to the discarded app-server connection. Keeping
950
841
  // them would make a later session interrupt attempt target stale threads
951
842
  // through the replacement client.
@@ -953,7 +844,6 @@ export class CodexRunner {
953
844
  this.childThreadSessions.clear();
954
845
  this.trackedApprovalItems.clear();
955
846
  this.pendingToolDenials.clear();
956
- this.threadDelegationCarriers.clear();
957
847
  for (const client of clients) {
958
848
  client.close().catch(error => {
959
849
  logger.debug(`[CodexRunner] Failed to close stale app-server client: ${error}`);
@@ -1300,47 +1190,53 @@ export class CodexRunner {
1300
1190
  }
1301
1191
  buildPermissionProfileConfig(sessionId, mode, projectPath, bootstrapAgentMdPath, personalMdPath) {
1302
1192
  const profileName = this.permissionProfileName(sessionId);
1303
- const managedTempDir = this.ensureSessionTempDir(sessionId);
1193
+ const managedTempDir = ensureSessionRuntimeDir(sessionId);
1304
1194
  const network = {
1305
1195
  enabled: true,
1306
1196
  unix_sockets: { [resolvePaths().instanceSocket]: 'allow' },
1307
1197
  };
1198
+ const filesystem = {
1199
+ ...buildCodexProtectedFilesystemRules(resolveRoot(), {
1200
+ denyLClassRead: mode === 'readonly',
1201
+ // The app-server itself is launched behind EvolCore's Linux
1202
+ // H-class guard, which already mounts every L-class directory
1203
+ // read-only. Avoid Codex's per-file glob expansion inside that
1204
+ // nested mount namespace; it can fail while creating a missing
1205
+ // target such as triggers/history.jsonl.
1206
+ expandLClassReadDeny: process.platform !== 'linux',
1207
+ }),
1208
+ ...this.resolveGitMetadataWriteRules(projectPath, mode),
1209
+ ...(bootstrapAgentMdPath ? {
1210
+ [path.resolve(bootstrapAgentMdPath)]: mode === 'readonly' ? 'read' : 'write',
1211
+ } : {}),
1212
+ ...(personalMdPath ? {
1213
+ [path.resolve(personalMdPath)]: mode === 'readonly' ? 'read' : 'write',
1214
+ } : {}),
1215
+ ...(mode !== 'readonly' && managedTempDir ? {
1216
+ [managedTempDir]: 'write',
1217
+ [managedTempDir + '/**']: 'write',
1218
+ [path.join(managedTempDir, 'locks')]: 'write',
1219
+ [path.join(managedTempDir, 'locks') + '/**']: 'write',
1220
+ } : {}),
1221
+ };
1222
+ // Codex resolves this complete profile before serializing the Windows
1223
+ // helper payload. Check after all runner-owned additions, not merely the
1224
+ // protected-path base projection, so Git and managed-temp rules cannot
1225
+ // push an otherwise valid profile back over CreateProcess' limit.
1226
+ assertWindowsCodexFilesystemRuleBudget(filesystem);
1308
1227
  return {
1309
1228
  default_permissions: profileName,
1310
1229
  permissions: {
1311
1230
  [profileName]: {
1312
1231
  extends: mode === 'readonly' ? ':read-only' : ':workspace',
1313
- filesystem: {
1314
- ...buildCodexProtectedFilesystemRules(resolveRoot(), {
1315
- denyLClassRead: mode === 'readonly',
1316
- // The app-server itself is launched behind EvolCore's Linux
1317
- // H-class guard, which already mounts every L-class directory
1318
- // read-only. Avoid Codex's per-file glob expansion inside that
1319
- // nested mount namespace; it can fail while creating a missing
1320
- // target such as triggers/history.jsonl.
1321
- expandLClassReadDeny: process.platform !== 'linux',
1322
- }),
1323
- ...this.resolveGitMetadataWriteRules(projectPath, mode),
1324
- ...(bootstrapAgentMdPath ? {
1325
- [path.resolve(bootstrapAgentMdPath)]: mode === 'readonly' ? 'read' : 'write',
1326
- } : {}),
1327
- ...(personalMdPath ? {
1328
- [path.resolve(personalMdPath)]: mode === 'readonly' ? 'read' : 'write',
1329
- } : {}),
1330
- ...(mode !== 'readonly' && managedTempDir ? {
1331
- [managedTempDir]: 'write',
1332
- [managedTempDir + '/**']: 'write',
1333
- [path.join(path.dirname(managedTempDir), 'evolcore-locks')]: 'write',
1334
- [path.join(path.dirname(managedTempDir), 'evolcore-locks') + '/**']: 'write',
1335
- } : {}),
1336
- },
1232
+ filesystem,
1337
1233
  network,
1338
1234
  },
1339
1235
  },
1340
1236
  };
1341
1237
  }
1342
- buildExecutionSandboxOptions(sessionId, mode, projectPath, bootstrapAgentMdPath, personalMdPath) {
1343
- const decision = resolvePhaseOneExecutionSandbox(mode, this.permissionContexts.get(sessionId)?.role);
1238
+ buildExecutionSandboxOptions(sessionId, mode, projectPath, bootstrapAgentMdPath, personalMdPath, permissionContext = this.permissionContexts.get(sessionId) ?? {}) {
1239
+ const decision = resolvePhaseOneExecutionSandbox(mode, permissionContext.role);
1344
1240
  return {
1345
1241
  decision,
1346
1242
  permissionProfileConfig: decision.state === 'active'
@@ -1378,6 +1274,26 @@ export class CodexRunner {
1378
1274
  }
1379
1275
  setSendPrompt(fn) { this.sendPromptFn = fn; }
1380
1276
  setPermissionContext(sessionId, context) { this.permissionContexts.set(sessionId, context); }
1277
+ async steerAsyncQuestionAnswer(sessionId, answer) {
1278
+ const active = this.activeTurns.get(sessionId);
1279
+ if (!active)
1280
+ return 'inactive';
1281
+ if (active.threadId !== answer.providerThreadId)
1282
+ throw new Error('Async question backend has changed');
1283
+ const client = this.getAppServerClientForThread(active.threadId);
1284
+ try {
1285
+ await client.turnSteer(active.threadId, active.turnId, [{
1286
+ type: 'text', text: formatAsyncQuestionAnswer(answer), text_elements: [],
1287
+ }]);
1288
+ }
1289
+ catch (error) {
1290
+ if (error.code === 'CODEX_NO_ACTIVE_TURN')
1291
+ return 'inactive';
1292
+ // Transport failures are ambiguous: never risk submitting the same answer twice.
1293
+ throw error;
1294
+ }
1295
+ return 'accepted';
1296
+ }
1381
1297
  injectUserMessage(sessionId, text) {
1382
1298
  // Bind queued reminders to the turn that generated them. A completed
1383
1299
  // turn may be replaced before the serialized steer reaches app-server;
@@ -1714,9 +1630,11 @@ export class CodexRunner {
1714
1630
  const knownThreadId = topicBinding
1715
1631
  ? this.cachedThreadForTopic(sessionId, topicAgentSessionId)
1716
1632
  : initialAgentSessionId || this.activeSessions.get(sessionId);
1717
- const operationKey = this.threadOperationKey(knownThreadId, sessionId);
1633
+ const operationKey = this.threadOperationKey(modelOverride?.recovery?.agentSessionId || knownThreadId, sessionId);
1634
+ await this.assertInterruptSettled(sessionId, modelOverride?.recovery?.agentSessionId || knownThreadId);
1718
1635
  const release = await this.acquireThreadOperation(operationKey, 'thread/resume+turn/start');
1719
1636
  try {
1637
+ await this.assertInterruptSettled(sessionId, modelOverride?.recovery?.agentSessionId || knownThreadId);
1720
1638
  const stream = await this.runQueryLocked(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv);
1721
1639
  this.activeOperationReleases.set(sessionId, release);
1722
1640
  return this.holdThreadOperation(stream, sessionId, release);
@@ -1746,6 +1664,14 @@ export class CodexRunner {
1746
1664
  logger.debug(`[CodexRunner] persisted main backend lookup failed; using local cache: session=${sessionId} error=${error instanceof Error ? error.message : String(error)}`);
1747
1665
  }
1748
1666
  }
1667
+ const recoverySessionId = modelOverride?.recovery?.agentSessionId;
1668
+ if (modelOverride?.recovery && (!recoverySessionId || agentSessionId !== recoverySessionId)) {
1669
+ throw new Error('Task recovery requires the original Codex thread binding');
1670
+ }
1671
+ if (modelOverride?.asyncQuestionAnswer && agentSessionId !== modelOverride.asyncQuestionAnswer.providerThreadId) {
1672
+ throw new Error('Async question reply requires its original Codex backend');
1673
+ }
1674
+ await this.assertInterruptSettled(sessionId, agentSessionId);
1749
1675
  let resumingThread = !!agentSessionId;
1750
1676
  const callModel = stripContextWindowSuffix(modelOverride?.model || this.model);
1751
1677
  const callEffort = modelOverride?.effortMode === 'model_default'
@@ -1766,6 +1692,10 @@ export class CodexRunner {
1766
1692
  this.dropExpiredPendingInterrupt(sessionId);
1767
1693
  const effectiveApprovalPolicy = callApprovalPolicy;
1768
1694
  const capabilityConfig = await this.resolveCapabilityThreadConfig(projectPath);
1695
+ const userInputConfig = { features: {
1696
+ default_mode_request_user_input: modelOverride?.enableRequestUserInput
1697
+ ?? this.resolvedConfig.enableRequestUserInput ?? true,
1698
+ } };
1769
1699
  const externalToolConfig = callMode === 'fullaccess'
1770
1700
  ? {}
1771
1701
  : await this.resolveExternalToolApprovalConfig(appServer, projectPath, capabilityConfig, callMode);
@@ -1773,25 +1703,21 @@ export class CodexRunner {
1773
1703
  await this.assertProactiveHookAvailability(sessionId, appServer);
1774
1704
  const managedTempInstruction = this.managedTempInstruction(sessionId);
1775
1705
  const developerInstructions = [systemPromptAppend, managedTempInstruction].filter(Boolean).join('\n\n') || undefined;
1776
- let knownDelegationCarrier = agentSessionId
1777
- ? this.threadDelegationCarriers.get(agentSessionId)
1706
+ let threadSessionToken = agentSessionId
1707
+ ? this.ensureThreadCredential(agentSessionId)
1708
+ : this.createThreadCredential();
1709
+ if (!threadSessionToken)
1710
+ throw new Error('Codex thread session credential is unavailable');
1711
+ const persistentRuntimeEnv = runtimeEnv
1712
+ ? Object.fromEntries(Object.entries(runtimeEnv).filter(([name]) => callMode === 'fullaccess' || name.toUpperCase() !== AGENT_DELEGATION_TOKEN_ENV))
1778
1713
  : undefined;
1779
- if (agentSessionId && !knownDelegationCarrier) {
1780
- knownDelegationCarrier = await this.loadPersistedDelegationCarrier(sessionManager, sessionId, agentSessionId);
1781
- }
1782
- let delegationCarrierCandidate = knownDelegationCarrier ?? this.createDelegationCarrier();
1783
- // Keep newly discovered carrier state local until generation-aware
1784
- // activation accepts the backend. A stale run must not publish a carrier
1785
- // that a later generation could mistake for its own thread.
1786
- let carrierToPublish;
1787
- const hasTaskDelegation = typeof runtimeEnv?.[AGENT_DELEGATION_TOKEN_ENV] === 'string';
1788
1714
  let threadOptions = {
1789
1715
  model: callModel,
1790
1716
  effort: callEffort,
1791
1717
  approvalPolicy: effectiveApprovalPolicy,
1792
1718
  approvalsReviewer: 'user',
1793
1719
  ...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
1794
- 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),
1720
+ 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),
1795
1721
  ...(developerInstructions ? { developerInstructions } : {}),
1796
1722
  };
1797
1723
  logger.info(`[CodexRunner] runQuery permMode=${requestedPermissionMode}->${callMode} ` +
@@ -1817,12 +1743,12 @@ export class CodexRunner {
1817
1743
  // A legacy main session can retain a provider ID after its rollout was
1818
1744
  // removed externally. It is safe to recover before input submission;
1819
1745
  // topic bindings remain authoritative and fail closed.
1820
- if (topicBinding || !isCodexNoRolloutError(error))
1746
+ if (topicBinding || recoverySessionId || modelOverride?.asyncQuestionAnswer || !isCodexNoRolloutError(error))
1821
1747
  throw error;
1822
1748
  if (typeof sessionManager?.clearMainSessionBackendIfMatches !== 'function')
1823
1749
  throw error;
1824
1750
  const missingThreadId = agentSessionId;
1825
- const cleared = await sessionManager.clearMainSessionBackendIfMatches(sessionId, missingThreadId, [CODEX_DELEGATION_CARRIER_METADATA_KEY]);
1751
+ const cleared = await sessionManager.clearMainSessionBackendIfMatches(sessionId, missingThreadId);
1826
1752
  if (cleared !== 'cleared' && cleared !== 'already_clear') {
1827
1753
  const recoveryError = new Error(`Codex missing-rollout recovery rejected: ${cleared}`);
1828
1754
  recoveryError.code = 'CODEX_MISSING_ROLLOUT_RECOVERY_REJECTED';
@@ -1831,17 +1757,15 @@ export class CodexRunner {
1831
1757
  if (this.activeSessions.get(sessionId) === missingThreadId)
1832
1758
  this.activeSessions.delete(sessionId);
1833
1759
  this.threadProjectPaths.delete(missingThreadId);
1834
- this.threadDelegationCarriers.delete(missingThreadId);
1835
1760
  this.unbindExternalToolBoundary(missingThreadId);
1836
1761
  this.clearTrackedApprovalItemsForThread(missingThreadId);
1837
1762
  this.dropThreadCredential(missingThreadId);
1838
1763
  agentSessionId = undefined;
1839
1764
  resumingThread = false;
1840
- knownDelegationCarrier = undefined;
1841
- delegationCarrierCandidate = this.createDelegationCarrier();
1765
+ threadSessionToken = this.createThreadCredential();
1842
1766
  threadOptions = {
1843
1767
  ...threadOptions,
1844
- 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),
1768
+ 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),
1845
1769
  };
1846
1770
  logger.warn(`[CodexRunner] recovered missing rollout before input submission: session=${sessionId} thread=${missingThreadId}`);
1847
1771
  threadResponse = await appServer.threadStart(projectPath, threadOptions);
@@ -1854,43 +1778,12 @@ export class CodexRunner {
1854
1778
  let threadId = threadResponse.thread?.id || agentSessionId;
1855
1779
  if (!threadId)
1856
1780
  throw new Error('Codex app-server did not return a thread id');
1857
- this.markThreadSubscribed(appServer, threadId);
1858
- if (!resumingThread) {
1859
- carrierToPublish = { threadId, carrier: delegationCarrierCandidate };
1860
- }
1861
- else if (hasTaskDelegation && !knownDelegationCarrier) {
1862
- // A BOUND topic cannot silently fork to migrate carrier metadata: its
1863
- // active-turn activation is allowed to confirm only the authoritative
1864
- // backend ID, so a fork would deterministically conflict. threadResume
1865
- // already received the candidate in this turn's environment config;
1866
- // publish it for the same thread after binding confirmation instead.
1867
- let recoveredCarrier = topicBinding
1868
- ? delegationCarrierCandidate
1869
- : await this.readThreadDelegationCarrier(appServer, threadId);
1870
- if (!recoveredCarrier) {
1871
- const staleThreadId = threadId;
1872
- logger.warn(`[CodexRunner] loaded thread has no recoverable delegation carrier; forking once to preserve history: thread=${staleThreadId}`);
1873
- const forked = await appServer.threadFork(staleThreadId, projectPath, undefined, {
1874
- model: callModel,
1875
- effort: callEffort,
1876
- approvalPolicy: effectiveApprovalPolicy,
1877
- approvalsReviewer: 'user',
1878
- ...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
1879
- config: threadOptions.config,
1880
- });
1881
- const forkedThreadId = forked.thread?.id;
1882
- if (!forkedThreadId)
1883
- throw new Error('Codex delegation-carrier recovery fork did not return a thread id');
1884
- this.markThreadSubscribed(appServer, forkedThreadId);
1885
- await this.releaseThreadSubscription(appServer, staleThreadId, 'carrier-recovery-fork-source');
1886
- await this.releaseThreadSubscription(appServer, forkedThreadId, 'carrier-recovery-fork');
1887
- threadId = forkedThreadId;
1888
- recoveredCarrier = delegationCarrierCandidate;
1889
- }
1890
- carrierToPublish = { threadId, carrier: recoveredCarrier };
1781
+ if (recoverySessionId && threadId !== recoverySessionId) {
1782
+ throw new Error('Task recovery cannot replace the original Codex thread');
1891
1783
  }
1892
- else if (knownDelegationCarrier) {
1893
- carrierToPublish = { threadId, carrier: knownDelegationCarrier };
1784
+ this.markThreadSubscribed(appServer, threadId);
1785
+ if (!resumingThread && this.ensureThreadCredential(threadId, threadSessionToken) !== threadSessionToken) {
1786
+ throw new Error('Codex thread session credential could not be bound');
1894
1787
  }
1895
1788
  const turn = modelOverride?.turn;
1896
1789
  if (!resumingThread) {
@@ -1898,9 +1791,6 @@ export class CodexRunner {
1898
1791
  // has been accepted. In particular, a pending interrupt must not leave
1899
1792
  // a durable ID for a thread that never produced a rollout.
1900
1793
  this.provisionalSessions.set(sessionId, threadId);
1901
- if (carrierToPublish) {
1902
- this.threadDelegationCarriers.set(carrierToPublish.threadId, carrierToPublish.carrier);
1903
- }
1904
1794
  }
1905
1795
  else {
1906
1796
  agentSessionId = threadId;
@@ -1920,10 +1810,6 @@ export class CodexRunner {
1920
1810
  throw error;
1921
1811
  }
1922
1812
  this.activeSessions.set(sessionId, threadId);
1923
- if (carrierToPublish) {
1924
- this.threadDelegationCarriers.set(carrierToPublish.threadId, carrierToPublish.carrier);
1925
- await this.persistDelegationCarrier(sessionManager, sessionId, carrierToPublish.threadId, carrierToPublish.carrier);
1926
- }
1927
1813
  this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${threadId}`);
1928
1814
  }
1929
1815
  this.threadProjectPaths.set(threadId, path.resolve(projectPath));
@@ -1931,7 +1817,11 @@ export class CodexRunner {
1931
1817
  const controller = new AbortController();
1932
1818
  this.activeAbortControllers.set(sessionId, controller);
1933
1819
  const tempFiles = [];
1934
- const input = this.buildAppServerInput(prompt, images, tempFiles, sessionId, runtimeEnv);
1820
+ const answer = modelOverride?.asyncQuestionAnswer;
1821
+ if (answer && answer.providerThreadId !== threadId) {
1822
+ throw new Error('Async question reply cannot resume a different Codex thread');
1823
+ }
1824
+ const input = this.buildAppServerInput(answer ? formatAsyncQuestionAnswer(answer) : prompt, images, tempFiles, sessionId, runtimeEnv);
1935
1825
  const queue = new AsyncEventQueue();
1936
1826
  controller.signal.addEventListener('abort', () => queue.end(), { once: true });
1937
1827
  const state = {
@@ -2045,7 +1935,7 @@ export class CodexRunner {
2045
1935
  if (!persisted)
2046
1936
  return false;
2047
1937
  try {
2048
- const committed = await this.commitProvisionalThread(sessionId, threadId, carrierToPublish, sessionManager, turn);
1938
+ const committed = await this.commitProvisionalThread(sessionId, threadId, turn);
2049
1939
  if (!committed) {
2050
1940
  const staleError = new Error('Codex provisional thread was superseded before persistence confirmation');
2051
1941
  staleError.code = 'RUNNER_START_SUPERSEDED';
@@ -2156,16 +2046,65 @@ export class CodexRunner {
2156
2046
  }
2157
2047
  }
2158
2048
  async interruptAppServerTurn(threadId, turnId) {
2049
+ const sessionId = this.findSessionKeyByThread(threadId);
2050
+ const subscribed = this.threadSubscriptions.get(threadId);
2051
+ const client = subscribed
2052
+ ? subscribed.client
2053
+ : this.getAppServerClient(sessionId ? this.chatModes.get(sessionId) === 'fullaccess' : false);
2054
+ const key = JSON.stringify([threadId, turnId]);
2055
+ const previous = this.unconfirmedInterrupts.get(key);
2056
+ previous?.unsubscribe?.();
2057
+ const record = { sessionId, threadId, turnId, client };
2058
+ this.unconfirmedInterrupts.set(key, record);
2059
+ let confirm;
2060
+ const confirmed = new Promise(resolve => { confirm = resolve; });
2061
+ const clear = () => {
2062
+ if (this.unconfirmedInterrupts.get(key) === record)
2063
+ this.unconfirmedInterrupts.delete(key);
2064
+ record.unsubscribe?.();
2065
+ confirm();
2066
+ };
2067
+ record.unsubscribe = client.onNotification?.(notification => {
2068
+ const params = notification.params ?? {};
2069
+ if ((params.threadId ?? params.thread_id) !== threadId)
2070
+ return;
2071
+ if (notification.method === 'thread/closed'
2072
+ || (notification.method === 'turn/completed'
2073
+ && (params.turn?.id ?? params.turnId ?? params.turn_id) === turnId))
2074
+ clear();
2075
+ });
2159
2076
  try {
2160
- const sessionId = this.findSessionKeyByThread(threadId);
2161
- const subscribed = this.threadSubscriptions.get(threadId);
2162
- const client = subscribed
2163
- ? subscribed.client
2164
- : this.getAppServerClient(sessionId ? this.chatModes.get(sessionId) === 'fullaccess' : false);
2165
- await client.turnInterrupt(threadId, turnId);
2077
+ await withInterruptDeadline(Promise.race([
2078
+ client.turnInterrupt(threadId, turnId).then(accepted => {
2079
+ if (!accepted)
2080
+ throw new Error('Codex interrupt was not accepted');
2081
+ }),
2082
+ confirmed,
2083
+ ]), 'Codex turn/interrupt', CODEX_INTERRUPT_TIMEOUT_MS);
2166
2084
  }
2167
2085
  catch (error) {
2168
- logger.debug(`[CodexRunner] app-server turn interrupt failed: ${error}`);
2086
+ logger.warn(`[CodexRunner] Unconfirmed interrupt: thread=${threadId} turn=${turnId} error=${String(error)}`);
2087
+ }
2088
+ }
2089
+ async assertInterruptSettled(sessionId, threadId) {
2090
+ const pending = [...this.unconfirmedInterrupts.entries()].filter(([, record]) => record.sessionId === sessionId || (threadId && record.threadId === threadId));
2091
+ await Promise.all(pending.map(async ([key, record]) => {
2092
+ try {
2093
+ // Local stream closure and unsubscribe do not prove the backend turn
2094
+ // stopped. Check its exact identity before allowing another writer.
2095
+ const response = await withInterruptDeadline(record.client.threadRead(record.threadId, true, 2_000), 'Codex interrupt verification', 2_000);
2096
+ const turn = response.thread?.turns?.find(turn => turn.id === record.turnId);
2097
+ if (turn && ['completed', 'failed', 'interrupted'].includes(String(turn.status))
2098
+ && !response.thread?.turns?.some(turn => turn.status === 'inProgress')
2099
+ && this.unconfirmedInterrupts.get(key) === record) {
2100
+ this.unconfirmedInterrupts.delete(key);
2101
+ record.unsubscribe?.();
2102
+ }
2103
+ }
2104
+ catch { /* Ambiguous execution remains fenced; report it below. */ }
2105
+ }));
2106
+ if ([...this.unconfirmedInterrupts.values()].some(record => record.sessionId === sessionId || (threadId && record.threadId === threadId))) {
2107
+ throw unsettledInterruptError('Codex');
2169
2108
  }
2170
2109
  }
2171
2110
  // ── Session commands ──
@@ -2177,7 +2116,6 @@ export class CodexRunner {
2177
2116
  return;
2178
2117
  if (previousThreadId && previousThreadId !== agentSessionId) {
2179
2118
  this.threadProjectPaths.delete(previousThreadId);
2180
- this.threadDelegationCarriers.delete(previousThreadId);
2181
2119
  this.unbindExternalToolBoundary(previousThreadId);
2182
2120
  this.clearTrackedApprovalItemsForThread(previousThreadId);
2183
2121
  }
@@ -2261,10 +2199,8 @@ export class CodexRunner {
2261
2199
  if (this.activeSessions.get(sessionId) === capturedThreadId)
2262
2200
  this.activeSessions.delete(sessionId);
2263
2201
  this.chatModes.delete(sessionId);
2264
- this.cleanupSessionTempDir(sessionId);
2265
2202
  if (capturedThreadId) {
2266
2203
  this.threadProjectPaths.delete(capturedThreadId);
2267
- this.threadDelegationCarriers.delete(capturedThreadId);
2268
2204
  this.unbindExternalToolBoundary(capturedThreadId);
2269
2205
  }
2270
2206
  this.clearTrackedApprovalItemsForThread(capturedThreadId);
@@ -2304,9 +2240,7 @@ export class CodexRunner {
2304
2240
  this.activeTurns.delete(sessionId);
2305
2241
  this.steeringTails.delete(sessionId);
2306
2242
  this.chatModes.delete(sessionId);
2307
- this.cleanupSessionTempDir(sessionId);
2308
2243
  this.threadProjectPaths.delete(threadId);
2309
- this.threadDelegationCarriers.delete(threadId);
2310
2244
  this.unbindExternalToolBoundary(threadId);
2311
2245
  this.clearTrackedApprovalItemsForThread(threadId);
2312
2246
  this.pruneThreadOperationLocks();
@@ -2347,19 +2281,19 @@ export class CodexRunner {
2347
2281
  : await this.resolveExternalToolApprovalConfig(appServer, _projectPath, capabilityConfig, compactMode);
2348
2282
  if (compactMode !== 'fullaccess')
2349
2283
  await this.assertProactiveHookAvailability(_sessionId, appServer);
2350
- const compactDelegationCarrier = this.threadDelegationCarriers.get(agentSessionId)
2351
- ?? this.createDelegationCarrier();
2284
+ const compactSessionToken = this.ensureThreadCredential(agentSessionId);
2285
+ if (!compactSessionToken)
2286
+ throw new Error('Codex thread session credential is unavailable');
2352
2287
  await appServer.threadResume(agentSessionId, _projectPath, {
2353
2288
  model: compactModel,
2354
2289
  effort: compactEffort,
2355
2290
  approvalPolicy: compactPolicy,
2356
2291
  approvalsReviewer: 'user',
2357
2292
  ...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
2358
- config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(_sessionId, compactDelegationCarrier, undefined, agentSessionId), this.buildLifecycleLockdownConfig(compactMode)), compactMode),
2293
+ config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(_sessionId, compactSessionToken), this.buildLifecycleLockdownConfig(compactMode)), compactMode),
2359
2294
  });
2360
2295
  this.markThreadSubscribed(appServer, agentSessionId);
2361
2296
  this.threadProjectPaths.set(agentSessionId, path.resolve(_projectPath));
2362
- this.threadDelegationCarriers.set(agentSessionId, compactDelegationCarrier);
2363
2297
  this.bindExternalToolBoundary(agentSessionId, externalToolConfig);
2364
2298
  await this.startAndWaitForCompact(appServer, agentSessionId);
2365
2299
  return finish({ ok: true, durationMs: Date.now() - startedAt });
@@ -2520,23 +2454,32 @@ export class CodexRunner {
2520
2454
  const dir = this.threadCredentialsDir();
2521
2455
  return dir ? path.join(dir, `${encodeURIComponent(threadId)}.json`) : undefined;
2522
2456
  }
2457
+ createThreadCredential() {
2458
+ return randomBytes(32).toString('hex');
2459
+ }
2523
2460
  /** Issue or re-read this agent's credential for a thread. */
2524
- ensureThreadCredential(threadId) {
2461
+ ensureThreadCredential(threadId, candidate) {
2525
2462
  const cached = this.threadCredentials.get(threadId);
2526
2463
  if (cached)
2527
- return cached;
2464
+ return !candidate || cached === candidate ? cached : undefined;
2528
2465
  const file = this.threadCredentialPath(threadId);
2529
- if (!file)
2530
- return undefined;
2466
+ // Low-level runner callers may not have an agent binding (for example a
2467
+ // standalone integration test). Keep those calls functional in memory;
2468
+ // daemon-owned runners always have an AID and persist the credential.
2469
+ if (!file) {
2470
+ const token = candidate ?? this.createThreadCredential();
2471
+ this.threadCredentials.set(threadId, token);
2472
+ return token;
2473
+ }
2531
2474
  try {
2532
2475
  const record = JSON.parse(fs.readFileSync(file, 'utf8'));
2533
2476
  if (record && typeof record.token === 'string' && record.token && record.threadId === threadId) {
2534
2477
  this.threadCredentials.set(threadId, record.token);
2535
- return record.token;
2478
+ return !candidate || record.token === candidate ? record.token : undefined;
2536
2479
  }
2537
2480
  }
2538
2481
  catch { /* missing or unreadable: fall through to issuance */ }
2539
- const token = randomBytes(32).toString('hex');
2482
+ const token = candidate ?? this.createThreadCredential();
2540
2483
  const record = {
2541
2484
  token,
2542
2485
  threadId,
@@ -2598,70 +2541,128 @@ export class CodexRunner {
2598
2541
  }
2599
2542
  catch { /* best effort */ }
2600
2543
  }
2601
- async forkSession(agentSessionId, projectPath, title) {
2544
+ async forkSession(agentSessionId, projectPath, title, context) {
2602
2545
  const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId), 'fork');
2603
2546
  try {
2604
- return await this.forkSessionLocked(agentSessionId, projectPath, title);
2547
+ return await this.forkSessionLocked(agentSessionId, projectPath, title, undefined, context);
2605
2548
  }
2606
2549
  finally {
2607
2550
  release();
2608
2551
  }
2609
2552
  }
2610
- async forkSessionLocked(agentSessionId, projectPath, title) {
2611
- const sessionKey = this.findSessionKeyByThread(agentSessionId);
2612
- const mode = this.chatModes.get(sessionKey) ?? this.currentMode;
2613
- const executionSandbox = this.buildExecutionSandboxOptions(sessionKey, mode, projectPath);
2553
+ async forkSessionLocked(agentSessionId, projectPath, title, lastTurnId, context) {
2554
+ // Historical threads may have no live runner binding after idle cleanup or
2555
+ // restart. Cached turn permissions can also outlive a role change. Derive
2556
+ // this management operation solely from its current authenticated request;
2557
+ // callers without that context remain readonly, including old fullaccess
2558
+ // threads. Do not mutate shared turn/session permission maps while forking.
2559
+ const sessionKey = context?.sessionId ?? this.findSessionKeyByThread(agentSessionId);
2560
+ const mode = normalizePermissionMode(context?.permissionMode).mode;
2561
+ const executionSandbox = this.buildExecutionSandboxOptions(sessionKey, mode, projectPath, undefined, undefined, context ?? {});
2614
2562
  const capabilityConfig = await this.resolveCapabilityThreadConfig(projectPath);
2615
- const appServer = this.threadSubscriptions.get(agentSessionId)?.client
2616
- ?? this.getAppServerClient(mode === 'fullaccess');
2563
+ const appServer = this.getAppServerClient();
2617
2564
  const externalToolConfig = await this.resolveExternalToolApprovalConfig(appServer, projectPath, capabilityConfig, mode);
2618
2565
  if (sessionKey)
2619
2566
  await this.assertProactiveHookAvailability(sessionKey, appServer);
2620
- const delegationCarrier = this.createDelegationCarrier();
2567
+ const forkSessionToken = this.createThreadCredential();
2621
2568
  const response = await appServer.threadFork(agentSessionId, projectPath, title, {
2622
2569
  model: this.model,
2623
2570
  effort: this.effort,
2624
2571
  approvalPolicy: this.toApprovalPolicy(mode),
2625
2572
  approvalsReviewer: 'user',
2626
2573
  ...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
2627
- config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionKey, delegationCarrier), this.buildLifecycleLockdownConfig(mode)), mode),
2574
+ config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionKey, forkSessionToken), this.buildLifecycleLockdownConfig(mode)), mode),
2575
+ ...(lastTurnId ? { lastTurnId } : {}),
2628
2576
  });
2629
2577
  const forkedThreadId = response.thread?.id;
2630
2578
  if (!forkedThreadId)
2631
2579
  throw new Error('Codex fork did not return a thread id');
2580
+ if (this.ensureThreadCredential(forkedThreadId, forkSessionToken) !== forkSessionToken) {
2581
+ throw new Error('Codex fork session credential could not be bound');
2582
+ }
2632
2583
  this.markThreadSubscribed(appServer, forkedThreadId);
2633
- this.threadDelegationCarriers.set(forkedThreadId, delegationCarrier);
2634
2584
  this.threadProjectPaths.set(forkedThreadId, path.resolve(projectPath));
2635
2585
  this.bindExternalToolBoundary(forkedThreadId, externalToolConfig);
2636
2586
  await this.releaseThreadSubscription(appServer, forkedThreadId, 'fork-finished');
2637
2587
  return forkedThreadId;
2638
2588
  }
2639
- async forkSessionAt(agentSessionId, projectPath, assistantMessageId, title) {
2589
+ async forkSessionAt(agentSessionId, projectPath, assistantMessageId, title, context) {
2640
2590
  const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId), 'forkAt');
2641
2591
  try {
2642
- const turns = buildSessionTurnList(await this.getSessionMessages(agentSessionId, projectPath));
2592
+ const appServer = this.getAppServerClient();
2593
+ const history = await this.readForkHistory(appServer, agentSessionId);
2594
+ const messages = this.mapThreadToSessionMessages(history, agentSessionId);
2595
+ const turns = buildSessionTurnList(messages);
2643
2596
  const targetIndex = turns.findIndex(turn => turn.assistantUuid === assistantMessageId);
2644
2597
  if (targetIndex < 0)
2645
2598
  throw new Error('fork point not found');
2646
- const forkedThreadId = await this.forkSessionLocked(agentSessionId, projectPath, title);
2647
- const turnsToDiscard = turns.length - targetIndex - 1;
2648
- if (turnsToDiscard > 0) {
2649
- const rollbackRelease = await this.acquireThreadOperation(this.threadOperationKey(forkedThreadId), 'rollback');
2650
- try {
2651
- const rolledBack = await this.rollbackSessionTurnsLocked(forkedThreadId, turnsToDiscard);
2652
- if (!rolledBack)
2653
- throw new Error('forked thread rollback failed');
2654
- }
2655
- finally {
2656
- rollbackRelease();
2657
- }
2658
- }
2659
- return forkedThreadId;
2599
+ const targetTurnId = this.findTurnIdForAssistantMessage(history, assistantMessageId);
2600
+ if (!targetTurnId)
2601
+ throw new Error('fork point turn not found');
2602
+ return await this.forkSessionLocked(agentSessionId, projectPath, title, targetTurnId, context);
2660
2603
  }
2661
2604
  finally {
2662
2605
  release();
2663
2606
  }
2664
2607
  }
2608
+ findTurnIdForAssistantMessage(response, assistantMessageId) {
2609
+ for (const turn of response.thread?.turns ?? []) {
2610
+ if (typeof turn.id !== 'string' || !turn.id)
2611
+ continue;
2612
+ for (const item of this.getTurnItems(turn)) {
2613
+ if (item.type === 'agentMessage' && item.id === assistantMessageId)
2614
+ return turn.id;
2615
+ }
2616
+ // Older app-server histories may omit item ids; the mapper then uses the
2617
+ // turn id as the assistant message id fallback.
2618
+ if (turn.id === assistantMessageId)
2619
+ return turn.id;
2620
+ }
2621
+ return undefined;
2622
+ }
2623
+ async readForkHistory(appServer, threadId) {
2624
+ let directRead;
2625
+ let directReadError;
2626
+ try {
2627
+ directRead = await appServer.threadRead(threadId, true);
2628
+ if ((directRead.thread?.turns?.length ?? 0) > 0)
2629
+ return directRead;
2630
+ }
2631
+ catch (error) {
2632
+ directReadError = error;
2633
+ }
2634
+ // Paginated threads reject full-history thread/read. Hydrate turns through
2635
+ // the experimental paging RPC, retaining full items so assistant message
2636
+ // IDs can be mapped to their containing turn.
2637
+ const turns = [];
2638
+ let cursor;
2639
+ const seenCursors = new Set();
2640
+ try {
2641
+ do {
2642
+ const page = await appServer.threadTurnsList(threadId, 100, cursor);
2643
+ if (Array.isArray(page.data)) {
2644
+ turns.push(...page.data.filter((turn) => !!turn && typeof turn === 'object'));
2645
+ }
2646
+ const nextCursor = typeof page.nextCursor === 'string' && page.nextCursor ? page.nextCursor : undefined;
2647
+ if (!nextCursor || seenCursors.has(nextCursor))
2648
+ break;
2649
+ seenCursors.add(nextCursor);
2650
+ cursor = nextCursor;
2651
+ } while (true);
2652
+ }
2653
+ catch (error) {
2654
+ if (directReadError)
2655
+ throw directReadError;
2656
+ throw error;
2657
+ }
2658
+ // Pages are returned newest-first when following `nextCursor`; restore
2659
+ // chronological order before mapping turns to session messages.
2660
+ if (turns.length > 0)
2661
+ return { thread: { ...(directRead?.thread ?? {}), id: threadId, turns: turns.reverse() } };
2662
+ if (directRead)
2663
+ return directRead;
2664
+ throw directReadError instanceof Error ? directReadError : new Error('Codex thread history unavailable');
2665
+ }
2665
2666
  async setSessionName(agentSessionId, name) {
2666
2667
  return this.getAppServerClientForThread(agentSessionId).threadSetName(agentSessionId, name);
2667
2668
  }
@@ -2670,7 +2671,7 @@ export class CodexRunner {
2670
2671
  return this.getAppServerClientForThread(agentSessionId).threadMetadataUpdate(agentSessionId, gitInfo);
2671
2672
  }
2672
2673
  async getSessionMessages(agentSessionId, projectPath) {
2673
- const response = await this.getAppServerClientForThread(agentSessionId).threadRead(agentSessionId, true);
2674
+ const response = await this.readForkHistory(this.getAppServerClientForThread(agentSessionId), agentSessionId);
2674
2675
  return this.mapThreadToSessionMessages(response, agentSessionId);
2675
2676
  }
2676
2677
  approvalItemKey(threadId, turnId, itemId) {
@@ -3010,9 +3011,9 @@ export class CodexRunner {
3010
3011
  const isCommandApproval = request.method === 'item/commandExecution/requestApproval'
3011
3012
  || request.method === 'execCommandApproval';
3012
3013
  const managedEcIntent = isCommandApproval && this.isManagedEvolcoreCommandIntent(toolInput);
3013
- const delegationCarrier = this.delegationCarrierForSession(sessionKey, typeof params.threadId === 'string' ? params.threadId : undefined);
3014
+ const sessionCredential = this.sessionTokenForSession(sessionKey, typeof params.threadId === 'string' ? params.threadId : undefined);
3014
3015
  const canonicalEcApproval = isCommandApproval
3015
- ? this.resolveCanonicalEcApproval(toolInput, this.getManagedTempDir(sessionKey), delegationCarrier)
3016
+ ? this.resolveCanonicalEcApproval(toolInput, this.getManagedTempDir(sessionKey), sessionCredential)
3016
3017
  : undefined;
3017
3018
  const approvedCommand = canonicalEcApproval?.argv;
3018
3019
  const denyInfrastructure = async (policyCode, message) => {
@@ -3062,23 +3063,23 @@ export class CodexRunner {
3062
3063
  const decision = typeof resolution === 'string' ? resolution : resolution.decision;
3063
3064
  const resolutionMeta = typeof resolution === 'string' ? undefined : resolution;
3064
3065
  if (decision !== 'deny' && managedEcIntent && approvedCommand) {
3065
- const carrierThreadIds = [
3066
+ const credentialThreadIds = [
3066
3067
  this.activeSessions.get(sessionKey),
3067
3068
  this.provisionalSessions.get(sessionKey),
3068
3069
  typeof params.threadId === 'string' ? params.threadId : undefined,
3069
3070
  typeof params.conversationId === 'string' ? params.conversationId : undefined,
3070
3071
  ];
3071
- const hasDelegationCarrier = carrierThreadIds.some(threadId => !!threadId && this.threadDelegationCarriers.has(threadId));
3072
+ const hasSessionCredential = credentialThreadIds.some(threadId => !!threadId && !!this.ensureThreadCredentialLoaded(threadId));
3072
3073
  const canArmDelegation = typeof permissionContext?.armApprovedDelegationCommand === 'function';
3073
- // A carrier-bearing thread is managed even if its permission context
3074
+ // A credentialed thread is managed even if its permission context
3074
3075
  // was not wired with an arm callback; it must fail closed here rather
3075
- // than reaching daemon IPC with an unarmed carrier.
3076
- if ((hasDelegationCarrier || canArmDelegation)
3077
- && canonicalEcApproval?.delegationToken
3078
- && canonicalEcApproval.delegationToken !== delegationCarrier) {
3079
- return await denyInfrastructure('delegation_token_mismatch', 'EC command approval contains a delegation token different from the active thread carrier');
3076
+ // than reaching daemon IPC with an unarmed session credential.
3077
+ if ((hasSessionCredential || canArmDelegation)
3078
+ && canonicalEcApproval?.credentialToken
3079
+ && canonicalEcApproval.credentialToken !== sessionCredential) {
3080
+ return await denyInfrastructure('delegation_token_mismatch', 'EC command approval contains a delegation token different from the active thread credential');
3080
3081
  }
3081
- if ((hasDelegationCarrier || canArmDelegation)
3082
+ if ((hasSessionCredential || canArmDelegation)
3082
3083
  && !this.armApprovedDelegationCommand(sessionKey, approvedCommand)) {
3083
3084
  return await denyInfrastructure('delegation_not_armed', 'EC command approval failed to arm delegation');
3084
3085
  }
@@ -3733,14 +3734,7 @@ export class CodexRunner {
3733
3734
  if (paths.length === 0)
3734
3735
  return true;
3735
3736
  const workspace = resolveProtectedCandidate(projectPath);
3736
- const managedTempDir = this.sessionTempDirs.get(sessionKey);
3737
- const trustedTempDir = managedTempDir ? this.isTrustedSessionTempDir(managedTempDir) : false;
3738
- if (managedTempDir && !trustedTempDir) {
3739
- logger.warn(`[CodexRunner] managed TMPDIR lost its trusted ownership or mode: session=${sessionKey} path=${managedTempDir}`);
3740
- }
3741
- const canonicalTempDir = managedTempDir && trustedTempDir
3742
- ? resolveProtectedCandidate(managedTempDir)
3743
- : undefined;
3737
+ const canonicalTempDir = this.getManagedTempDir(sessionKey);
3744
3738
  return paths.some(candidate => {
3745
3739
  const absolute = path.isAbsolute(candidate) ? path.resolve(candidate) : path.resolve(workspace, candidate);
3746
3740
  const canonical = resolveProtectedCandidate(absolute);
@@ -3835,7 +3829,7 @@ export class CodexRunner {
3835
3829
  // distinguishes this explicit empty value from runners without a
3836
3830
  // session-temp capability.
3837
3831
  managedTempDir: permissionContext?.managedTempDir ?? this.getManagedTempDir(sessionKey) ?? '',
3838
- delegationCarrier: this.delegationCarrierForSession(sessionKey),
3832
+ sessionCredential: this.sessionTokenForSession(sessionKey),
3839
3833
  allowProtectedMetadata: false,
3840
3834
  selfAid: permissionContext?.selfAid,
3841
3835
  channel: permissionContext?.channel,
@@ -4192,17 +4186,17 @@ export class CodexRunner {
4192
4186
  return;
4193
4187
  this.childThreadSessions.set(childThreadId, sessionId);
4194
4188
  this.threadProjectPaths.set(childThreadId, path.resolve(projectPath));
4195
- // Child approvals use the parent's capability boundary and delegation
4196
- // carrier. This is inheritance, not a second permission decision.
4189
+ // Child approvals use the parent's capability boundary and session
4190
+ // credential. This is inheritance, not a second permission decision.
4197
4191
  const parentBoundary = parentThreadId ? this.threadExternalToolBoundaries.get(parentThreadId) : undefined;
4198
4192
  const parentFingerprint = parentThreadId ? this.threadExternalToolFingerprints.get(parentThreadId) : undefined;
4199
4193
  if (parentBoundary)
4200
4194
  this.threadExternalToolBoundaries.set(childThreadId, parentBoundary);
4201
4195
  if (parentFingerprint)
4202
4196
  this.threadExternalToolFingerprints.set(childThreadId, parentFingerprint);
4203
- const parentCarrier = parentThreadId ? this.threadDelegationCarriers.get(parentThreadId) : undefined;
4204
- if (parentCarrier)
4205
- this.threadDelegationCarriers.set(childThreadId, parentCarrier);
4197
+ const parentToken = parentThreadId ? this.ensureThreadCredentialLoaded(parentThreadId) : undefined;
4198
+ if (parentToken)
4199
+ this.ensureThreadCredential(childThreadId, parentToken);
4206
4200
  }
4207
4201
  unregisterChildThread(childThreadId) {
4208
4202
  this.childThreadSessions.delete(childThreadId);
@@ -4210,8 +4204,8 @@ export class CodexRunner {
4210
4204
  this.threadProjectPaths.delete(childThreadId);
4211
4205
  this.threadExternalToolBoundaries.delete(childThreadId);
4212
4206
  this.threadExternalToolFingerprints.delete(childThreadId);
4213
- this.threadDelegationCarriers.delete(childThreadId);
4214
4207
  this.clearTrackedApprovalItemsForThread(childThreadId);
4208
+ this.dropThreadCredential(childThreadId);
4215
4209
  }
4216
4210
  cleanupChildThreadsForSession(sessionId) {
4217
4211
  for (const [childThreadId, ownerSessionId] of this.childThreadSessions) {
@@ -4351,10 +4345,14 @@ export class CodexRunner {
4351
4345
  }
4352
4346
  case 'item/agentMessage/delta': {
4353
4347
  const itemId = typeof params.itemId === 'string' ? params.itemId : undefined;
4354
- if (itemId)
4355
- state.streamedAgentMessageIds.add(itemId);
4356
- if (itemId && typeof params.delta === 'string' && params.delta) {
4357
- state.agentMessageDeltaText.set(itemId, (state.agentMessageDeltaText.get(itemId) || '') + params.delta);
4348
+ const delta = typeof params.delta === 'string' ? params.delta : '';
4349
+ if (itemId && delta) {
4350
+ if (!state.streamedAgentMessageIds.has(itemId)) {
4351
+ state.streamedAgentMessageIds.add(itemId);
4352
+ yield { type: 'text_start', contentIndex: 0 };
4353
+ }
4354
+ state.agentMessageDeltaText.set(itemId, (state.agentMessageDeltaText.get(itemId) || '') + delta);
4355
+ yield { type: 'heartbeat', source: 'text' };
4358
4356
  }
4359
4357
  break;
4360
4358
  }
@@ -4415,24 +4413,46 @@ export class CodexRunner {
4415
4413
  this.clearPendingToolDenialsForTurn(state.threadId, turnId);
4416
4414
  yield* this.reconcileOpenAppServerToolCalls(state, turnId);
4417
4415
  if (turn.status === 'failed' && turn.error?.message) {
4416
+ logger.warn(`[CodexRunner] turn failed: ${JSON.stringify({
4417
+ sessionId, threadId: state.threadId, turnId, model: state.model,
4418
+ errorType: classifyError(new Error(turn.error.message)),
4419
+ message: formatApprovalLogSummary(turn.error.message),
4420
+ lastReportedUsage: state.tokenUsage?.last,
4421
+ })}`);
4418
4422
  if (isRetryableError(new Error(turn.error.message))) {
4419
4423
  throw new Error(turn.error.message);
4420
4424
  }
4421
- yield { type: 'error', error: turn.error.message, errorType: 'unknown' };
4425
+ yield { type: 'error', error: turn.error.message, errorType: toAgentErrorType(turn.error.message) };
4422
4426
  }
4423
4427
  yield this.mapAppServerTurnComplete(turn, state);
4424
4428
  break;
4425
4429
  }
4426
4430
  case 'error': {
4427
- if (!params.message) {
4428
- // SSE idle timeout reconnect — not a real task error, suppress
4429
- logger.debug(`[CodexRunner] app-server SSE reconnect (no message)`);
4431
+ // Current app-server uses error.message; older builds used message.
4432
+ const message = typeof params.error?.message === 'string' ? params.error.message : params.message;
4433
+ if (typeof message !== 'string' || !message.trim()) {
4434
+ logger.debug('[CodexRunner] app-server error notification without a diagnostic message');
4430
4435
  break;
4431
4436
  }
4432
- if (isRetryableError(new Error(params.message))) {
4433
- throw new Error(params.message);
4437
+ logger.warn(`[CodexRunner] error notification: ${JSON.stringify({
4438
+ sessionId, threadId: state.threadId, turnId: state.turnId, model: state.model,
4439
+ willRetry: params.willRetry ?? null,
4440
+ errorType: classifyError(new Error(message)), message: formatApprovalLogSummary(message),
4441
+ })}`);
4442
+ // Codex owns retries within this turn. Throwing here closes our subscription
4443
+ // before its recovery can finish; runQuery must not submit the input again.
4444
+ if (params.willRetry === true) {
4445
+ yield {
4446
+ type: 'notice', severity: 'warn', subtype: 'codex-model-retry',
4447
+ message: isOutputTokenLimitError(new Error(message))
4448
+ ? '模型响应达到输出上限,Codex 正在当前轮内重试。'
4449
+ : 'Codex 正在当前轮内重试模型请求。',
4450
+ };
4451
+ break;
4434
4452
  }
4435
- yield { type: 'error', error: params.message, errorType: 'unknown' };
4453
+ if (isRetryableError(new Error(message)))
4454
+ throw new Error(message);
4455
+ yield { type: 'error', error: message, errorType: toAgentErrorType(message) };
4436
4456
  break;
4437
4457
  }
4438
4458
  }
@@ -4633,10 +4653,35 @@ export class CodexRunner {
4633
4653
  switch (item.type) {
4634
4654
  case 'agentMessage':
4635
4655
  {
4656
+ if (item.questions != null) {
4657
+ if (item.id)
4658
+ state.agentMessageDeltaText.delete(item.id);
4659
+ try {
4660
+ const questions = parseAsyncUserQuestions(item.questions);
4661
+ if (!item.id || !state.turnId)
4662
+ throw new Error('Async question is missing its item or turn id');
4663
+ const key = JSON.stringify([state.threadId, state.turnId, item.id]);
4664
+ if (this.asyncQuestionItems.has(key))
4665
+ break;
4666
+ this.asyncQuestionItems.add(key);
4667
+ if (this.asyncQuestionItems.size > 2048)
4668
+ this.asyncQuestionItems.delete(this.asyncQuestionItems.values().next().value);
4669
+ yield { type: 'user_question', request: {
4670
+ providerThreadId: state.threadId, providerTurnId: state.turnId, itemId: item.id, questions,
4671
+ } };
4672
+ }
4673
+ catch (error) {
4674
+ yield { type: 'notice', severity: 'warn', subtype: 'invalid_async_question',
4675
+ message: `Codex 异步问题无法显示:${error instanceof Error ? error.message : String(error)}` };
4676
+ }
4677
+ break;
4678
+ }
4636
4679
  const buffered = item.id ? state.agentMessageDeltaText.get(item.id) : undefined;
4637
4680
  const text = typeof item.text === 'string' && item.text ? item.text : buffered;
4638
- if (text)
4639
- yield { type: 'text', text };
4681
+ if (text) {
4682
+ state.lastAssistantUuid = item.id || state.turnId;
4683
+ yield { type: 'text', text, assistantUuid: state.lastAssistantUuid };
4684
+ }
4640
4685
  if (item.id)
4641
4686
  state.agentMessageDeltaText.delete(item.id);
4642
4687
  }
@@ -4863,16 +4908,18 @@ export class CodexRunner {
4863
4908
  }
4864
4909
  mapAppServerTurnComplete(turn, state) {
4865
4910
  const status = turn.status || 'completed';
4911
+ const outputLimit = status === 'failed' && isOutputTokenLimitError(new Error(turn.error?.message ?? ''));
4866
4912
  const tokenUsage = this.mapCodexTokenUsage(state.tokenUsage?.last ?? turn.tokenUsage ?? turn.usage);
4867
4913
  const contextUsage = this.mapCodexContextUsage(turn.contextUsage ?? state.tokenUsage?.contextUsage ?? state.tokenUsage?.context);
4868
4914
  const terminalReason = status === 'completed'
4869
4915
  ? undefined
4870
4916
  : status === 'interrupted'
4871
4917
  ? 'aborted_streaming'
4872
- : status;
4918
+ : outputLimit ? 'max_output_tokens' : status;
4873
4919
  return {
4874
4920
  type: 'complete',
4875
- subtype: status === 'completed' ? 'success' : status,
4921
+ subtype: status === 'completed' ? 'success' : outputLimit ? 'max_output_tokens' : status,
4922
+ assistantUuid: state.lastAssistantUuid,
4876
4923
  isError: status === 'failed',
4877
4924
  errors: turn.error?.message ? [turn.error.message] : undefined,
4878
4925
  terminalReason,
@@ -4886,126 +4933,36 @@ export class CodexRunner {
4886
4933
  contextUsage,
4887
4934
  };
4888
4935
  }
4889
- managedTempParent() {
4890
- const uid = typeof process.getuid === 'function' ? process.getuid() : 'user';
4891
- const base = ensureProcessManagedTempDir();
4892
- return path.join(resolveProtectedCandidate(base), `evolcore-codex-${uid}`);
4893
- }
4894
- ensurePrivateDirectory(directory) {
4895
- try {
4896
- fs.mkdirSync(directory, { mode: 0o700 });
4897
- }
4898
- catch (error) {
4899
- if (error?.code !== 'EEXIST')
4900
- throw error;
4901
- }
4902
- const stat = fs.lstatSync(directory);
4903
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
4904
- throw new Error(`managed temp path is not a real directory: ${directory}`);
4905
- }
4906
- if (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
4907
- throw new Error(`managed temp path is owned by another uid: ${directory}`);
4908
- }
4909
- if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)
4910
- fs.chmodSync(directory, 0o700);
4911
- }
4912
- ensureSessionTempDir(sessionId) {
4913
- const existing = this.sessionTempDirs.get(sessionId);
4914
- if (existing) {
4915
- try {
4916
- this.ensurePrivateDirectory(this.managedTempParent());
4917
- this.ensurePrivateDirectory(existing);
4918
- return existing;
4919
- }
4920
- catch (error) {
4921
- this.sessionTempDirs.delete(sessionId);
4922
- logger.warn(`[CodexRunner] existing managed TMPDIR is no longer usable: session=${sessionId} path=${existing} error=${error instanceof Error ? error.message : String(error)}`);
4923
- return undefined;
4924
- }
4925
- }
4926
- try {
4927
- const parent = this.managedTempParent();
4928
- this.ensurePrivateDirectory(parent);
4929
- this.ensurePrivateDirectory(path.join(parent, 'evolcore-locks'));
4930
- // Include a per-runner nonce so a daemon restart never reuses contents
4931
- // left behind by an unclean shutdown for the same EvolCore session id.
4932
- const digest = createHash('sha256')
4933
- .update(this.managedTempNonce)
4934
- .update('\0')
4935
- .update(sessionId)
4936
- .digest('hex')
4937
- .slice(0, 24);
4938
- const directory = path.join(parent, digest);
4939
- this.ensurePrivateDirectory(directory);
4940
- this.sessionTempDirs.set(sessionId, directory);
4941
- registerManagedTempDirForExitCleanup(directory, parent);
4942
- return directory;
4943
- }
4944
- catch (error) {
4945
- logger.warn(`[CodexRunner] managed TMPDIR unavailable for session=${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
4946
- return undefined;
4947
- }
4948
- }
4949
- isTrustedSessionTempDir(directory) {
4950
- return isTrustedManagedTempDirectory(directory, this.managedTempParent());
4951
- }
4952
4936
  getManagedTempDir(sessionId) {
4953
- const directory = this.ensureSessionTempDir(sessionId);
4954
- if (!directory || !this.isTrustedSessionTempDir(directory))
4955
- return undefined;
4956
- return resolveProtectedCandidate(directory);
4957
- }
4958
- cleanupSessionTempDir(sessionId) {
4959
- const directory = this.sessionTempDirs.get(sessionId);
4960
- this.sessionTempDirs.delete(sessionId);
4961
- if (!directory)
4962
- return;
4963
- // Reuse the same ownership, mode, lexical-parent, and canonicalization
4964
- // checks used at execution time. If the path changed, leave it in place
4965
- // rather than recursively removing an attacker-selected target.
4966
- if (!this.isTrustedSessionTempDir(directory)) {
4967
- managedTempDirsForExitCleanup.delete(directory);
4968
- logger.warn(`[CodexRunner] refused to clean unmanaged TMPDIR: session=${sessionId} path=${directory}`);
4969
- return;
4970
- }
4971
- const candidate = path.resolve(directory);
4972
- try {
4973
- fs.rmSync(candidate, { recursive: true, force: true });
4974
- managedTempDirsForExitCleanup.delete(directory);
4975
- }
4976
- catch (error) {
4977
- logger.warn(`[CodexRunner] failed to clean managed TMPDIR: session=${sessionId} path=${candidate} error=${error instanceof Error ? error.message : String(error)}`);
4978
- }
4937
+ const directory = ensureSessionRuntimeDir(sessionId);
4938
+ return directory ? resolveProtectedCandidate(directory) : undefined;
4979
4939
  }
4980
4940
  managedTempInstruction(sessionId) {
4981
- const managedTempDir = this.ensureSessionTempDir(sessionId);
4941
+ const managedTempDir = ensureSessionRuntimeDir(sessionId);
4982
4942
  if (!managedTempDir)
4983
4943
  return undefined;
4984
- return `临时文件必须使用环境变量 \`$TMPDIR\`。当前值是 \`${managedTempDir}\`,它是当前 EvolCore 会话唯一允许的临时工作区;不要直接使用 \`/tmp\`,也不要使用其他会话的临时目录。当前是可持久化 thread 的恢复轮次,早先上下文中的 \`/tmp/evolcore-codex-*\` 绝对路径可能已经失效;不要重用旧路径。若文件不存在,请先在当前 \`$TMPDIR\` 重新创建,再继续执行后续 \`ec\` 命令。`;
4944
+ return `临时文件必须使用环境变量 \`$TMPDIR\`。当前值是 \`${managedTempDir}\`,它是当前 EvolCore 会话唯一允许的临时工作区;不要直接使用 \`/tmp\`,也不要使用其他会话的临时目录。该目录随 EvolCore sessionId 保持稳定;若文件不存在,请先在当前 \`$TMPDIR\` 重新创建,再继续执行后续 \`ec\` 命令。`;
4985
4945
  }
4986
- buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrier, taskDelegationToken, threadId) {
4987
- const managedTempDir = this.ensureSessionTempDir(sessionId);
4946
+ buildEvolcoreShellEnvironmentConfig(sessionId, sessionToken, taskDelegationToken) {
4947
+ const managedTempDir = ensureSessionRuntimeDir(sessionId);
4988
4948
  // The managed-hook credential rides the thread's shell environment so the
4989
4949
  // PreToolUse hook can prove the call belongs to an EvolCore-owned thread.
4990
4950
  // It follows the thread (start AND resume) rather than the daemon process.
4991
- const sessionToken = threadId ? this.ensureThreadCredential(threadId) : undefined;
4992
4951
  return {
4993
4952
  shell_environment_policy: {
4994
4953
  set: {
4995
4954
  EVOLCORE_SESSION_ID: sessionId,
4996
4955
  EVOLCORE_HOME: resolveRoot(),
4997
4956
  ...(managedTempDir ? { EVOLCORE_SESSION_RUNTIME_DIR: managedTempDir } : {}),
4998
- ...(managedTempDir ? { EVOLCORE_RUNTIME_LOCK_DIR: path.join(path.dirname(managedTempDir), 'evolcore-locks') } : {}),
4999
- ...(taskDelegationToken || delegationCarrier
5000
- ? { [AGENT_DELEGATION_TOKEN_ENV]: taskDelegationToken ?? delegationCarrier }
5001
- : {}),
4957
+ ...(managedTempDir ? { EVOLCORE_RUNTIME_LOCK_DIR: path.join(managedTempDir, 'locks') } : {}),
4958
+ ...(taskDelegationToken ? { [AGENT_DELEGATION_TOKEN_ENV]: taskDelegationToken } : {}),
5002
4959
  ...(sessionToken ? { [CODEX_SESSION_TOKEN_ENV]: sessionToken } : {}),
5003
4960
  ...(managedTempDir ? { TMPDIR: managedTempDir } : {}),
5004
4961
  },
5005
4962
  },
5006
4963
  };
5007
4964
  }
5008
- sanitizeThreadEnvironmentConfig(config, permissionMode) {
4965
+ sanitizeThreadEnvironmentConfig(config, permissionMode, taskDelegationToken) {
5009
4966
  if (!config)
5010
4967
  return null;
5011
4968
  const policy = this.configRecord(config.shell_environment_policy);
@@ -5015,11 +4972,40 @@ export class CodexRunner {
5015
4972
  const safeSet = sanitizeShellExecutionEnvironment(set, {
5016
4973
  lockExecutableResolution: permissionMode === 'readonly',
5017
4974
  });
4975
+ const sessionToken = typeof safeSet[CODEX_SESSION_TOKEN_ENV] === 'string'
4976
+ ? safeSet[CODEX_SESSION_TOKEN_ENV]
4977
+ : undefined;
4978
+ for (const name of Object.keys(safeSet)) {
4979
+ const normalized = name.toUpperCase();
4980
+ if (normalized === AGENT_DELEGATION_TOKEN_ENV || normalized === CODEX_SESSION_TOKEN_ENV) {
4981
+ delete safeSet[name];
4982
+ }
4983
+ }
4984
+ if (sessionToken)
4985
+ safeSet[CODEX_SESSION_TOKEN_ENV] = sessionToken;
4986
+ const existingExclude = Array.isArray(policy.exclude)
4987
+ ? policy.exclude.filter((name) => typeof name === 'string')
4988
+ : [];
4989
+ const carriesTaskDelegation = permissionMode === 'fullaccess'
4990
+ && !!taskDelegationToken;
4991
+ if (carriesTaskDelegation)
4992
+ safeSet[AGENT_DELEGATION_TOKEN_ENV] = taskDelegationToken;
4993
+ const filteredExclude = existingExclude.filter(name => {
4994
+ const normalized = name.toUpperCase();
4995
+ return normalized !== AGENT_DELEGATION_TOKEN_ENV
4996
+ && normalized !== CODEX_SESSION_TOKEN_ENV;
4997
+ });
4998
+ const exclude = carriesTaskDelegation
4999
+ ? filteredExclude
5000
+ : [...new Set([...filteredExclude, AGENT_DELEGATION_TOKEN_ENV])];
5018
5001
  return {
5019
5002
  ...config,
5020
5003
  shell_environment_policy: {
5021
5004
  ...policy,
5022
5005
  set: safeSet,
5006
+ // The task token is invocation-scoped. Persistent non-fullaccess
5007
+ // threads must not inherit a stale value from the app-server process.
5008
+ exclude,
5023
5009
  },
5024
5010
  };
5025
5011
  }
@@ -5036,21 +5022,23 @@ export class CodexRunner {
5036
5022
  this.activeSessions.clear();
5037
5023
  this.provisionalSessions.clear();
5038
5024
  this.activeTurns.clear();
5025
+ for (const childThreadId of [...this.childThreadSessions.keys()])
5026
+ this.unregisterChildThread(childThreadId);
5039
5027
  this.activeSubagentTurns.clear();
5040
5028
  this.childThreadSessions.clear();
5041
5029
  this.threadProjectPaths.clear();
5042
5030
  this.threadExternalToolFingerprints.clear();
5043
5031
  this.threadExternalToolBoundaries.clear();
5044
5032
  this.trackedApprovalItems.clear();
5045
- this.threadDelegationCarriers.clear();
5046
5033
  this.pendingInterrupts.clear();
5034
+ for (const record of this.unconfirmedInterrupts.values())
5035
+ record.unsubscribe?.();
5036
+ this.unconfirmedInterrupts.clear();
5047
5037
  this.steeringTails.clear();
5048
5038
  this.permissionContexts.clear();
5049
5039
  this.chatModes.clear();
5050
5040
  this.pruneThreadOperationLocks();
5051
5041
  this.threadOperationLocks.clear();
5052
- for (const sessionId of [...this.sessionTempDirs.keys()])
5053
- this.cleanupSessionTempDir(sessionId);
5054
5042
  await Promise.all([
5055
5043
  this.appServerClient?.close(),
5056
5044
  this.fullAccessAppServerClient?.close(),