evolcore 0.0.23 → 0.0.25

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