evolcore 0.0.32 → 0.0.34

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 (214) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/bin/ec.js +3 -2
  3. package/bin/install-codex-managed-hooks.mjs +135 -37
  4. package/dist/agents/baseagent-availability.js +20 -36
  5. package/dist/agents/claude-runner.js +111 -15
  6. package/dist/agents/codex/app-server-client.js +3 -0
  7. package/dist/agents/codex/diagnostics.js +112 -54
  8. package/dist/agents/codex-runner.js +241 -63
  9. package/dist/agents/ecagent-runner.js +78 -33
  10. package/dist/agents/runner-types.js +5 -6
  11. package/dist/aun/aid/identity.js +1 -1
  12. package/dist/aun/group/operations.js +0 -1
  13. package/dist/aun/msg/history.js +1 -0
  14. package/dist/aun/msg/p2p.js +0 -1
  15. package/dist/aun/outbox.js +50 -1
  16. package/dist/channels/aun.js +44 -8
  17. package/dist/channels/daemon.js +261 -3
  18. package/dist/cli/agent-command.js +58 -24
  19. package/dist/cli/agent.js +1 -1
  20. package/dist/cli/aun-commands.js +368 -31
  21. package/dist/cli/contact.js +8 -2
  22. package/dist/cli/ctl-command.js +3 -1
  23. package/dist/cli/daemon-commands.js +197 -27
  24. package/dist/cli/debug-mode.js +154 -0
  25. package/dist/cli/fs-command.js +2 -1
  26. package/dist/cli/handoff-command.js +3 -0
  27. package/dist/cli/index.js +98 -67
  28. package/dist/cli/init.js +2 -2
  29. package/dist/cli/integrity-command.js +196 -0
  30. package/dist/cli/managed-command-guard.js +5 -0
  31. package/dist/cli/memory-command.js +166 -146
  32. package/dist/cli/queue-command.js +9 -4
  33. package/dist/cli/session-command.js +194 -0
  34. package/dist/cli/skill-command.js +78 -2
  35. package/dist/cli/task-context.js +1 -0
  36. package/dist/cli/thread-command.js +622 -0
  37. package/dist/cli/trigger-command.js +10 -3
  38. package/dist/config/builtin-roles.js +1 -0
  39. package/dist/config/config-field-policy.js +6 -3
  40. package/dist/config/config-manager.js +26 -2
  41. package/dist/config/contact-book-store.js +5 -0
  42. package/dist/config/resolved-config-op.js +3 -0
  43. package/dist/config/role-service.js +13 -0
  44. package/dist/config/runtime-env.js +16 -0
  45. package/dist/config-store.js +45 -0
  46. package/dist/core/audit/log-integrity.js +3 -1
  47. package/dist/core/auth/agent-delegation.js +133 -36
  48. package/dist/core/auth/debug-context.js +139 -0
  49. package/dist/core/auth/delegation-diagnostics.js +414 -0
  50. package/dist/core/auth/operation-authorizer.js +25 -2
  51. package/dist/core/auth/operation-catalog.js +49 -0
  52. package/dist/core/background-session/session-cleanup.js +212 -0
  53. package/dist/core/background-session/types.js +10 -0
  54. package/dist/core/bootstrap-messages.js +1 -1
  55. package/dist/core/bootstrap-service.js +6 -3
  56. package/dist/core/command/agent-control.js +18 -26
  57. package/dist/core/command/command-handler.js +197 -27
  58. package/dist/core/command/daemon-diagnostics.js +1 -0
  59. package/dist/core/command/memory-slash.js +6 -2
  60. package/dist/core/command/menu-catalog.js +51 -1
  61. package/dist/core/command/menu-handler.js +114 -23
  62. package/dist/core/command/menu-protocol.js +25 -1
  63. package/dist/core/command/skill-slash.js +322 -0
  64. package/dist/core/command/slash-gate.js +6 -4
  65. package/dist/core/command/slash-handler.js +195 -13
  66. package/dist/core/command/topic-slash.js +528 -0
  67. package/dist/core/daemon-bootstrap.js +4 -7
  68. package/dist/core/evolagent-registry.js +20 -5
  69. package/dist/core/file-integrity/read.js +61 -0
  70. package/dist/core/file-integrity/store.js +262 -0
  71. package/dist/core/handoff/runtime.js +5 -11
  72. package/dist/core/handoff/store.js +5 -0
  73. package/dist/core/ipc-registration.js +175 -3
  74. package/dist/core/memory/batch-meta.js +132 -0
  75. package/dist/core/memory/consolidation-context.js +83 -29
  76. package/dist/core/memory/consolidation-ops-records.js +49 -16
  77. package/dist/core/memory/consolidation-queue-format.js +21 -0
  78. package/dist/core/memory/consolidation.js +264 -205
  79. package/dist/core/memory/index-manager.js +19 -28
  80. package/dist/core/memory/memory-admin.js +19 -14
  81. package/dist/core/memory/memory-document.js +53 -9
  82. package/dist/core/memory/memory-writer.js +30 -15
  83. package/dist/core/message/aun-ipc-sender.js +161 -6
  84. package/dist/core/message/im-renderer.js +169 -10
  85. package/dist/core/message/message-bridge.js +15 -4
  86. package/dist/core/message/message-log.js +1 -0
  87. package/dist/core/message/peer-mode.js +6 -1
  88. package/dist/core/message/response-engine.js +929 -110
  89. package/dist/core/message/subtask-tool-projection.js +140 -0
  90. package/dist/core/model/catalog-service.js +17 -12
  91. package/dist/core/permission/ec-command-parser.js +217 -9
  92. package/dist/core/permission/protected-paths.js +5 -1
  93. package/dist/core/permission/readonly-shell-query.js +43 -5
  94. package/dist/core/permission/session-path-grants.js +334 -0
  95. package/dist/core/permission/tool-canonical.js +226 -0
  96. package/dist/core/permission/tool-policy.js +30 -62
  97. package/dist/core/role/runtime-policy.js +21 -10
  98. package/dist/core/session/session-fs-store.js +123 -8
  99. package/dist/core/session/session-idle-monitor.js +249 -22
  100. package/dist/core/session/session-manager.js +129 -14
  101. package/dist/core/skill/command-parser.js +13 -0
  102. package/dist/core/skill/index-generator.js +21 -4
  103. package/dist/core/skill/startup.js +41 -0
  104. package/dist/core/skill/switch-resolver.js +55 -29
  105. package/dist/core/system-session/grants.js +40 -0
  106. package/dist/core/system-session/memory.js +222 -0
  107. package/dist/core/system-session/registry.js +74 -0
  108. package/dist/core/system-session/types.js +14 -0
  109. package/dist/core/thread/command-parser.js +125 -0
  110. package/dist/core/thread/index.js +1 -0
  111. package/dist/core/thread/ipc-operation.js +95 -0
  112. package/dist/core/thread/thread-context-storage.js +131 -0
  113. package/dist/core/thread/thread-operation.js +258 -0
  114. package/dist/core/thread/thread-route.js +33 -0
  115. package/dist/core/thread/thread-service.js +770 -0
  116. package/dist/core/thread/types.js +1 -0
  117. package/dist/eck/kit-renderer.js +7 -2
  118. package/dist/eck/manifest-engine.js +24 -4
  119. package/dist/eck/message-renderer.js +35 -0
  120. package/dist/index.js +19 -7
  121. package/dist/ipc.js +140 -50
  122. package/dist/paths.js +93 -0
  123. package/dist/response-system/engines/v1/proactive-flow.js +169 -80
  124. package/dist/stats/billing.js +20 -8
  125. package/dist/trigger/authorization.js +57 -28
  126. package/dist/trigger/feedback.js +13 -2
  127. package/dist/trigger/parser.js +70 -1
  128. package/dist/trigger/system-definitions.js +3 -3
  129. package/dist/trigger/validation.js +14 -0
  130. package/dist/utils/cross-platform.js +11 -2
  131. package/dist/utils/error-dict.json +18 -4
  132. package/dist/utils/error-utils.js +143 -1
  133. package/dist/utils/instance-registry.js +13 -8
  134. package/dist/utils/log-writer.js +12 -0
  135. package/dist/utils/process-introspect.js +11 -0
  136. package/kits/docs/context-assembly.md +1 -1
  137. package/kits/docs/evolcore/INDEX.md +1 -0
  138. package/kits/docs/evolcore/agent-create.md +40 -28
  139. package/kits/docs/evolcore/agent.md +21 -6
  140. package/kits/docs/evolcore/config.md +2 -2
  141. package/kits/docs/evolcore/ctl.md +2 -2
  142. package/kits/docs/evolcore/ec-capabilities.md +5 -1
  143. package/kits/docs/evolcore/fs-architecture.md +4 -5
  144. package/kits/docs/evolcore/fs.md +1 -1
  145. package/kits/docs/evolcore/group-collaboration.md +4 -6
  146. package/kits/docs/evolcore/group.md +11 -12
  147. package/kits/docs/evolcore/memory-architecture.md +4 -5
  148. package/kits/docs/evolcore/memory-consolidation.md +19 -15
  149. package/kits/docs/evolcore/memory-tags.md +3 -1
  150. package/kits/docs/evolcore/memory.md +24 -12
  151. package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
  152. package/kits/docs/evolcore/menu-protocol.md +7 -0
  153. package/kits/docs/evolcore/model.md +16 -0
  154. package/kits/docs/evolcore/msg.md +8 -11
  155. package/kits/docs/evolcore/thread-chain.md +267 -0
  156. package/kits/docs/prompt-loading-architecture.md +1 -1
  157. package/kits/eck_manifest.json +55 -40
  158. package/kits/eck_manifest.memory.json +0 -12
  159. package/kits/eck_message_manifest.json +169 -10
  160. package/kits/rules/01-overview.md +11 -0
  161. package/kits/rules/03-identity.md +6 -3
  162. package/kits/rules/05-venue.md +1 -1
  163. package/kits/rules/06-channel.md +1 -1
  164. package/kits/rules/07-memory-and-thread.md +199 -0
  165. package/kits/schemas/_meta.json +7 -1
  166. package/kits/schemas/agent-config.schema.13.json +1 -1
  167. package/kits/schemas/daemon.schema.7.json +140 -0
  168. package/kits/schemas/relation-config.schema.9.json +18 -3
  169. package/kits/schemas/role-config.schema.1.json +6 -6
  170. package/kits/schemas/single-session.schema.1.json +6 -6
  171. package/kits/schemas/single-session.schema.2.json +6 -6
  172. package/kits/schemas/single-session.schema.3.json +6 -6
  173. package/kits/skills/.switches.json +12 -1
  174. package/kits/skills/ec-group/SKILL.md +234 -0
  175. package/kits/skills/ec-skill/SKILL.md +10 -1
  176. package/kits/skills/ec-skill/scripts/cli.mjs +16 -2
  177. package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
  178. package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
  179. package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
  180. package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
  181. package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
  182. package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
  183. package/kits/skills/thread-ops/SKILL.md +492 -0
  184. package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
  185. package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
  186. package/kits/skills/wechat-article-reader/SKILL.md +97 -0
  187. package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
  188. package/kits/templates/bootstrap-welcome.md +7 -7
  189. package/kits/templates/memory-consolidation/forget.md +7 -2
  190. package/kits/templates/memory-consolidation/generalize.md +9 -4
  191. package/kits/templates/memory-consolidation/notes.md +11 -11
  192. package/kits/templates/memory-consolidation/profile.md +8 -3
  193. package/kits/templates/memory-consolidation/promote.md +8 -3
  194. package/kits/templates/memory-consolidation/workflow.md +6 -6
  195. package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
  196. package/kits/templates/message-fragments/item.md +2 -1
  197. package/kits/templates/message-fragments/thread-context.md +6 -0
  198. package/kits/templates/message-fragments/trigger-session.md +2 -0
  199. package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
  200. package/kits/templates/roles/member.json +2 -0
  201. package/kits/templates/roles/visitor.json +2 -0
  202. package/kits/templates/system-fragments/bootstrap.md +67 -72
  203. package/kits/templates/system-fragments/channel.md +3 -6
  204. package/kits/templates/system-fragments/commands.md +1 -0
  205. package/kits/templates/system-fragments/managed-runtime.md +15 -0
  206. package/kits/templates/system-fragments/memory.md +16 -9
  207. package/kits/templates/system-fragments/reply.md +7 -6
  208. package/kits/templates/system-fragments/session.md +5 -14
  209. package/package.json +6 -3
  210. package/dist/core/memory/memory-session-policy.js +0 -41
  211. package/dist/core/memory/memory-signature.js +0 -257
  212. package/kits/docs/evolcore/group-fs.md +0 -17
  213. package/kits/docs/venues/aun-group.md +0 -45
  214. package/kits/docs/venues/group.md +0 -43
@@ -1,9 +1,10 @@
1
1
  import { getRunnerCatalog, catalogEfforts, assertCatalogSelectable } from '../core/model/model-catalog.js';
2
2
  // Codex/OpenAI API 不认识 Claude Code SDK 的窗口后缀,必须传入基础模型 ID。
3
- import { stripContextWindowSuffix } from '../core/model/context-window-suffix.js';
3
+ import { parseModelId, stripContextWindowSuffix } from '../core/model/context-window-suffix.js';
4
4
  import { formatAsyncQuestionAnswer, hasTrustedFullAccessContext, parseAsyncUserQuestions, unsettledInterruptError, withInterruptDeadline } from './runner-types.js';
5
5
  import { requestDangerousCommandPermission } from '../core/permission/approval-gateway.js';
6
6
  import { checkReadonly, checkDangerousCommand, evaluateToolPreflight } from '../core/permission/tool-policy.js';
7
+ import { isShellToolName } from '../core/permission/tool-canonical.js';
7
8
  import { normalizeExecutionPermissionMode, normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
8
9
  import { assertWindowsCodexFilesystemRuleBudget, buildCodexProtectedFilesystemRules, getExistingRelationAttachmentDirs, isSameOrDescendant, resolveProtectedCandidate, } from '../core/permission/protected-paths.js';
9
10
  import { CODEX_THREAD_ACTIVE_WRITER, CodexAppServerClient, } from './codex/app-server-client.js';
@@ -11,7 +12,7 @@ import { resolveOpenaiConfig, resolveCodexCliPath, resolveCodexPowerShellShimPat
11
12
  import { logger } from '../utils/logger.js';
12
13
  import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
13
14
  import { auditCodexApprovalDecision, auditPermissionLifecycle, auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
14
- import { classifyError, ErrorType, isOutputTokenLimitError, isRetryableError } from '../utils/error-utils.js';
15
+ import { classifyError, ErrorType, getToolResultErrorText, isOutputTokenLimitError, isRetryableError } from '../utils/error-utils.js';
15
16
  import { renderActionAsText } from '../core/interaction-router.js';
16
17
  import { buildEnvelope, isInteractionSendAccepted, sendInteractionPayload } from '../core/message/message-utils.js';
17
18
  import { resolveCodexCapabilityThreadConfigForProject } from '../core/capability/capability-manager.js';
@@ -20,6 +21,7 @@ import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-envi
20
21
  import { classifyEvolcoreShellCommand, containsLiteralManagedTmpDirOutsideSendContent, containsUnresolvedManagedTmpDirReference, hasCodexCmdCarrierEcIntent, hasPowerShellCredentialAssignment, normalizeEvolcoreCommandArgv, canonicalizeEvolcoreCommandArgv, parseCodexToolCommand, parseCodexCarrierCommand, parseLiteralShellCommand, parseCodexShellCommandArgv, resolveCodexShellCarrierArgv, parseShellApplyPatchInvocation, } from '../core/permission/ec-command-parser.js';
21
22
  import { compareVersions } from '../utils/npm-ops.js';
22
23
  import { agentCodexTokenPath, resolvePaths, resolveRoot } from '../paths.js';
24
+ import { findModelSpec } from '../stats/billing.js';
23
25
  import { ensureSessionRuntimeDir, getManagedTaskTempDir } from '../cli/task-context.js';
24
26
  import { buildTranscriptTurns } from '../core/session/session-transcript.js';
25
27
  import { CodexSessionFileAdapter } from '../core/session/adapters/codex-session-file-adapter.js';
@@ -128,6 +130,24 @@ function codexItemErrorFields(item) {
128
130
  const errorDetailCode = normalizeToolErrorDetailCode(rawCode);
129
131
  return errorDetailCode ? { errorDetailCode } : {};
130
132
  }
133
+ function waitForCodexOperation(operation, signal) {
134
+ return new Promise((resolve, reject) => {
135
+ const onAbort = () => {
136
+ signal.removeEventListener('abort', onAbort);
137
+ reject(signal.reason);
138
+ };
139
+ signal.addEventListener('abort', onAbort, { once: true });
140
+ operation.then(value => {
141
+ signal.removeEventListener('abort', onAbort);
142
+ resolve(value);
143
+ }, error => {
144
+ signal.removeEventListener('abort', onAbort);
145
+ reject(error);
146
+ });
147
+ if (signal.aborted)
148
+ onAbort();
149
+ });
150
+ }
131
151
  function isCodexReasoningNotification(method) {
132
152
  return method === 'item/reasoning/textDelta'
133
153
  || method === 'item/reasoning/summaryTextDelta'
@@ -177,10 +197,10 @@ function parseManagedPreToolUseDenial(value) {
177
197
  reason = 'EvolCore PreToolUse policy denied this tool call';
178
198
  const policyCode = /当前汇报命令(?:未通过规范解析|不符合当前会话)/.test(reason)
179
199
  ? 'proactive_send_invalid'
180
- : /工具调用已达到\s*\d+\s*次/.test(reason)
181
- ? 'proactive_tool_report_required'
182
- : /请先用\s+ec\s+(?:group\s+send|msg\s+send)/.test(reason)
183
- ? 'proactive_first_send_required'
200
+ : /连续违规\s*\d+\s*次,任务已中断/.test(reason)
201
+ ? 'proactive_circuit_break'
202
+ : /未包含意图表达,本轮只允许\s+ec\s+(?:group\s+send|msg\s+send)/.test(reason)
203
+ ? 'proactive_intent_required'
184
204
  : 'managed_pre_tool_use_denied';
185
205
  return { policyCode, reason };
186
206
  }
@@ -335,6 +355,11 @@ export async function getCodexAppServerAvailabilityAsync() {
335
355
  }
336
356
  let version;
337
357
  try {
358
+ // Each probe is a full Codex CLI launch (~0.3-0.45s on Windows). Run the
359
+ // app-server probe concurrently; its result is only consulted after the
360
+ // version gate passes, so the decision order is unchanged.
361
+ const appServerProbe = probeCli('codex', ['app-server', '--help'], options);
362
+ appServerProbe.catch(() => { });
338
363
  version = parseCodexCliVersion(await probeCli('codex', ['--version'], options)) ?? undefined;
339
364
  if (!version)
340
365
  return { available: false, reason: '无法读取 Codex CLI 版本。' };
@@ -342,7 +367,7 @@ export async function getCodexAppServerAvailabilityAsync() {
342
367
  return { available: false, version, requiresUpgrade: true,
343
368
  reason: `Codex CLI ${version} 低于最低要求 ${MIN_CODEX_CLI_VERSION}。请升级 Codex CLI:npm install -g @openai/codex@latest` };
344
369
  }
345
- await probeCli('codex', ['app-server', '--help'], options);
370
+ await appServerProbe;
346
371
  return { available: true, version };
347
372
  }
348
373
  catch (error) {
@@ -359,6 +384,7 @@ export class CodexRunner {
359
384
  retryInputSemantics = 'at_most_once';
360
385
  capabilities;
361
386
  model;
387
+ modelContextId;
362
388
  effort;
363
389
  activeAbortControllers = new Map();
364
390
  activeStreams = new Map();
@@ -378,6 +404,7 @@ export class CodexRunner {
378
404
  unconfirmedInterrupts = new Map();
379
405
  steeringTails = new Map();
380
406
  threadOperationLocks = new Map();
407
+ queryOperations = new Map();
381
408
  activeOperationReleases = new Map();
382
409
  /** Last app-server client that loaded each thread, including unsubscribed threads. */
383
410
  threadSubscriptions = new Map();
@@ -431,7 +458,8 @@ export class CodexRunner {
431
458
  fileRewind: 'git-head',
432
459
  fullaccess: true,
433
460
  };
434
- this.model = stripContextWindowSuffix(this.resolvedConfig.model);
461
+ this.modelContextId = this.resolvedConfig.model;
462
+ this.model = stripContextWindowSuffix(this.modelContextId);
435
463
  if (this.resolvedConfig.effort)
436
464
  this.effort = this.resolvedConfig.effort;
437
465
  this.onSessionIdUpdate = callbacks.onSessionIdUpdate;
@@ -1179,7 +1207,11 @@ export class CodexRunner {
1179
1207
  }
1180
1208
  }
1181
1209
  // ── ModelSwitcher ──
1182
- setModel(model) { this.model = stripContextWindowSuffix(model); this.resetAppServerClient(); }
1210
+ setModel(model) {
1211
+ this.modelContextId = model;
1212
+ this.model = stripContextWindowSuffix(model);
1213
+ this.resetAppServerClient();
1214
+ }
1183
1215
  getModel() { return this.model; }
1184
1216
  getModelCatalog() {
1185
1217
  return getRunnerCatalog('codex', this.resolvedConfig.agentAid, this.resolvedConfig);
@@ -1222,6 +1254,16 @@ export class CodexRunner {
1222
1254
  const digest = createHash('sha256').update(sessionId).digest('hex').slice(0, 24);
1223
1255
  return `__evolcore_${label}_${digest}_hclass_v1`;
1224
1256
  }
1257
+ /**
1258
+ * 系统会话的写入洞:只对模块声明的可写文件逐个开 write 规则,不开目录、不开 glob。
1259
+ * 声明为空(缺批次目录等)时返回空表,沙箱保持 :read-only。
1260
+ */
1261
+ resolveSystemSessionWriteRules(sessionId) {
1262
+ const writeFiles = this.permissionContexts.get(sessionId)?.systemSessionGrants?.writeFiles ?? [];
1263
+ return Object.fromEntries(writeFiles
1264
+ .filter(file => path.isAbsolute(file))
1265
+ .map(file => [path.resolve(file), 'write']));
1266
+ }
1225
1267
  resolveGitMetadataWriteRules(projectPath, mode) {
1226
1268
  if (mode === 'readonly')
1227
1269
  return {};
@@ -1298,6 +1340,7 @@ export class CodexRunner {
1298
1340
  expandLClassReadDeny: process.platform !== 'linux',
1299
1341
  }),
1300
1342
  ...this.resolveGitMetadataWriteRules(projectPath, mode),
1343
+ ...this.resolveSystemSessionWriteRules(sessionId),
1301
1344
  ...(bootstrapAgentMdPath ? {
1302
1345
  [path.resolve(bootstrapAgentMdPath)]: mode === 'readonly' ? 'read' : 'write',
1303
1346
  } : {}),
@@ -1330,12 +1373,18 @@ export class CodexRunner {
1330
1373
  }
1331
1374
  buildExecutionSandboxOptions(sessionId, mode, projectPath, bootstrapAgentMdPath, personalMdPath, permissionContext = this.permissionContexts.get(sessionId) ?? {}) {
1332
1375
  const decision = resolvePhaseOneExecutionSandbox(mode, permissionContext.role);
1376
+ // 系统会话明确声明不要沙箱:Codex 的 permission profile 以项目目录为中心,
1377
+ // 而系统会话的读写边界($EVOLCORE_HOME 下的批次目录与记忆目录)与 cwd 无关,
1378
+ // 硬套 profile 只会 fail closed。边界改由 systemSessionCheck 逐次调用判定。
1379
+ const sessionSandboxOff = this.permissionContexts.get(sessionId)?.systemSessionSandbox === 'off';
1380
+ const sandboxOff = decision.state === 'off' || sessionSandboxOff;
1333
1381
  return {
1334
1382
  decision,
1335
- permissionProfileConfig: decision.state === 'active'
1336
- ? this.buildPermissionProfileConfig(sessionId, decision.permissionMode, projectPath, bootstrapAgentMdPath, personalMdPath)
1337
- : undefined,
1338
- sandbox: decision.state === 'off' ? 'danger-full-access' : undefined,
1383
+ permissionProfileConfig: sandboxOff
1384
+ ? undefined
1385
+ : this.buildPermissionProfileConfig(sessionId, decision.permissionMode, projectPath, bootstrapAgentMdPath, personalMdPath),
1386
+ sandbox: sandboxOff ? 'danger-full-access' : undefined,
1387
+ sessionSandboxOff,
1339
1388
  };
1340
1389
  }
1341
1390
  isTrustedFullAccessSession(sessionKey) {
@@ -1465,7 +1514,7 @@ export class CodexRunner {
1465
1514
  const normalizedInput = toolInput && typeof toolInput === 'object' && !Array.isArray(toolInput)
1466
1515
  ? toolInput
1467
1516
  : {};
1468
- const isShellTool = toolName === 'Bash' || toolName === 'Shell' || toolName === 'exec_command';
1517
+ const isShellTool = isShellToolName(toolName); // canonical normalisation via tool-canonical.ts
1469
1518
  const recordPreToolUseDenial = async (policyCode, reason) => {
1470
1519
  const summary = summarizeToolInputForAudit(toolName, normalizedInput).slice(0, 512);
1471
1520
  auditToolPreflightDenial({
@@ -1533,6 +1582,14 @@ export class CodexRunner {
1533
1582
  return { ok: true, applicable: true, decision: 'deny', reason };
1534
1583
  }
1535
1584
  }
1585
+ // 系统会话:模块判定即最终结论,不进入 preflight / 角色 / permissionMode 分支
1586
+ if (context.systemSessionCheck) {
1587
+ const decision = context.systemSessionCheck(isShellTool ? 'Bash' : toolName, normalizedInput);
1588
+ if (decision.allow)
1589
+ return { ok: true, applicable: true, decision: 'allow' };
1590
+ await recordPreToolUseDenial(decision.code, decision.reason);
1591
+ return { ok: true, applicable: true, decision: 'deny', reason: decision.reason };
1592
+ }
1536
1593
  if ((this.chatModes.get(sessionKey) ?? this.currentMode) === 'fullaccess') {
1537
1594
  if (this.isTrustedFullAccessSession(sessionKey))
1538
1595
  return { ok: true, applicable: true, decision: 'allow' };
@@ -1623,6 +1680,19 @@ export class CodexRunner {
1623
1680
  hasActiveStream(key) {
1624
1681
  return this.activeStreams.has(key) || this.activeAbortControllers.has(key) || this.activeTurns.has(key);
1625
1682
  }
1683
+ hasActiveOperation(sessionId, taskId) {
1684
+ const operations = this.queryOperations.get(sessionId);
1685
+ return !!operations && [...operations].some(operation => !taskId || operation.taskId === taskId);
1686
+ }
1687
+ cancelQueuedTurn(sessionId, identity) {
1688
+ const operation = [...(this.queryOperations.get(sessionId) ?? [])].find(candidate => !candidate.lockAcquired
1689
+ && candidate.taskId === identity.taskId
1690
+ && candidate.generation === identity.generation);
1691
+ if (!operation)
1692
+ return false;
1693
+ operation.controller.abort(Object.assign(new Error('Codex queued operation cancelled'), { name: 'AbortError' }));
1694
+ return true;
1695
+ }
1626
1696
  threadOperationKey(threadId, sessionId) {
1627
1697
  // Once a thread id is known it is the shared identity, including when
1628
1698
  // multiple EvolCore sessions happen to address the same Codex thread.
@@ -1659,6 +1729,7 @@ export class CodexRunner {
1659
1729
  merged.then(() => {
1660
1730
  if (primary.tail === merged)
1661
1731
  primary.tail = undefined;
1732
+ this.pruneThreadOperationLocks();
1662
1733
  });
1663
1734
  }
1664
1735
  for (const alias of other.aliases) {
@@ -1694,7 +1765,8 @@ export class CodexRunner {
1694
1765
  lock.aliases.delete(key);
1695
1766
  }
1696
1767
  }
1697
- async acquireThreadOperation(key, operation) {
1768
+ async acquireThreadOperation(key, operation, signal) {
1769
+ signal?.throwIfAborted();
1698
1770
  const lock = this.getThreadOperationLock(key);
1699
1771
  const previous = lock.tail;
1700
1772
  let releaseCurrent;
@@ -1702,12 +1774,8 @@ export class CodexRunner {
1702
1774
  releaseCurrent = resolve;
1703
1775
  });
1704
1776
  lock.tail = current;
1705
- if (previous) {
1706
- logger.info(`[CodexRunner] queued ${operation} behind an active operation: ${key}`);
1707
- await previous;
1708
- }
1709
1777
  let released = false;
1710
- return () => {
1778
+ const release = () => {
1711
1779
  if (released)
1712
1780
  return;
1713
1781
  released = true;
@@ -1716,6 +1784,21 @@ export class CodexRunner {
1716
1784
  releaseCurrent();
1717
1785
  this.pruneThreadOperationLocks();
1718
1786
  };
1787
+ try {
1788
+ if (previous) {
1789
+ logger.info(`[CodexRunner] queued ${operation} behind an active operation: ${key}`);
1790
+ await (signal ? waitForCodexOperation(previous, signal) : previous);
1791
+ }
1792
+ signal?.throwIfAborted();
1793
+ return release;
1794
+ }
1795
+ catch (error) {
1796
+ if (signal?.aborted && previous)
1797
+ void previous.then(release);
1798
+ else
1799
+ release();
1800
+ throw error;
1801
+ }
1719
1802
  }
1720
1803
  async *holdThreadOperation(stream, sessionId, release) {
1721
1804
  try {
@@ -1747,11 +1830,29 @@ export class CodexRunner {
1747
1830
  ? this.cachedThreadForTopic(sessionId, topicAgentSessionId)
1748
1831
  : initialAgentSessionId || this.activeSessions.get(sessionId);
1749
1832
  const operationKey = this.threadOperationKey(modelOverride?.recovery?.agentSessionId || knownThreadId, sessionId);
1750
- await this.assertInterruptSettled(sessionId, modelOverride?.recovery?.agentSessionId || knownThreadId);
1751
- const release = await this.acquireThreadOperation(operationKey, 'thread/resume+turn/start');
1833
+ const operation = {
1834
+ controller: new AbortController(),
1835
+ taskId: modelOverride?.turn?.taskId ?? this.permissionContexts.get(sessionId)?.taskId,
1836
+ generation: modelOverride?.turn?.generation,
1837
+ lockAcquired: false,
1838
+ };
1839
+ const operations = this.queryOperations.get(sessionId) ?? new Set();
1840
+ operations.add(operation);
1841
+ this.queryOperations.set(sessionId, operations);
1842
+ let releaseLock;
1843
+ const release = () => {
1844
+ operations.delete(operation);
1845
+ if (this.queryOperations.get(sessionId) === operations && operations.size === 0) {
1846
+ this.queryOperations.delete(sessionId);
1847
+ }
1848
+ releaseLock?.();
1849
+ };
1752
1850
  try {
1753
- await this.assertInterruptSettled(sessionId, modelOverride?.recovery?.agentSessionId || knownThreadId);
1754
- const stream = await this.runQueryLocked(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv);
1851
+ await waitForCodexOperation(this.assertInterruptSettled(sessionId, modelOverride?.recovery?.agentSessionId || knownThreadId), operation.controller.signal);
1852
+ releaseLock = await this.acquireThreadOperation(operationKey, 'thread/resume+turn/start', operation.controller.signal);
1853
+ operation.lockAcquired = true;
1854
+ await waitForCodexOperation(this.assertInterruptSettled(sessionId, modelOverride?.recovery?.agentSessionId || knownThreadId), operation.controller.signal);
1855
+ const stream = await this.runQueryLocked(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv, operation.controller);
1755
1856
  this.activeOperationReleases.set(sessionId, release);
1756
1857
  return this.holdThreadOperation(stream, sessionId, release);
1757
1858
  }
@@ -1760,7 +1861,7 @@ export class CodexRunner {
1760
1861
  throw error;
1761
1862
  }
1762
1863
  }
1763
- async runQueryLocked(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv) {
1864
+ async runQueryLocked(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv, operationController) {
1764
1865
  // A daemon-managed task must never fall back to the app-server's
1765
1866
  // process-wide TMPDIR if its session namespace disappeared.
1766
1867
  getManagedTaskTempDir(runtimeEnv);
@@ -1789,7 +1890,8 @@ export class CodexRunner {
1789
1890
  }
1790
1891
  await this.assertInterruptSettled(sessionId, agentSessionId);
1791
1892
  let resumingThread = !!agentSessionId;
1792
- const callModel = stripContextWindowSuffix(modelOverride?.model || this.model);
1893
+ const modelContextId = modelOverride?.model || this.modelContextId;
1894
+ const callModel = stripContextWindowSuffix(modelContextId);
1793
1895
  const callEffort = modelOverride?.effortMode === 'model_default'
1794
1896
  ? undefined
1795
1897
  : modelOverride?.effort ?? this.effort;
@@ -1901,6 +2003,7 @@ export class CodexRunner {
1901
2003
  if (!resumingThread && this.ensureThreadCredential(threadId, threadSessionToken) !== threadSessionToken) {
1902
2004
  throw new Error('Codex thread session credential could not be bound');
1903
2005
  }
2006
+ this.permissionContexts.get(sessionId)?.bindSessionCredential?.(threadSessionToken, threadId);
1904
2007
  const turn = modelOverride?.turn;
1905
2008
  if (!resumingThread) {
1906
2009
  // Keep a newly-created backend private to this runner until turn/start
@@ -1930,7 +2033,7 @@ export class CodexRunner {
1930
2033
  }
1931
2034
  this.threadProjectPaths.set(threadId, path.resolve(projectPath));
1932
2035
  this.bindExternalToolBoundary(threadId, externalToolConfig);
1933
- const controller = new AbortController();
2036
+ const controller = operationController ?? new AbortController();
1934
2037
  this.activeAbortControllers.set(sessionId, controller);
1935
2038
  const answer = modelOverride?.asyncQuestionAnswer;
1936
2039
  if (answer && answer.providerThreadId !== threadId) {
@@ -1944,6 +2047,7 @@ export class CodexRunner {
1944
2047
  sessionId,
1945
2048
  projectPath,
1946
2049
  model: callModel,
2050
+ modelContextId,
1947
2051
  committed: resumingThread,
1948
2052
  persistenceFinalized: resumingThread,
1949
2053
  streamedAgentMessageIds: new Set(),
@@ -1957,6 +2061,11 @@ export class CodexRunner {
1957
2061
  inputId: turn?.inputId,
1958
2062
  inputAcceptedEmitted: false,
1959
2063
  };
2064
+ let startupInterrupt;
2065
+ const interruptStartedTurn = (turnId) => {
2066
+ startupInterrupt ??= this.interruptAppServerTurn(threadId, turnId);
2067
+ return startupInterrupt;
2068
+ };
1960
2069
  const unsubscribe = appServer.onNotification(notification => {
1961
2070
  // 仅从 turn/started 锁定权威 turnId — resume 时会有上一轮 turn 的残留通知
1962
2071
  // (如 thread/tokenUsage/updated)先于新 turn 到达,不能用它们 latch turnId
@@ -2000,8 +2109,8 @@ export class CodexRunner {
2000
2109
  if (startedTurnId && !state.turnId) {
2001
2110
  state.turnId = startedTurnId;
2002
2111
  this.activeTurns.set(sessionId, { threadId, turnId: startedTurnId });
2003
- if (this.consumePendingInterrupt(sessionId)) {
2004
- this.interruptAppServerTurn(threadId, startedTurnId).catch(() => { });
2112
+ if (controller.signal.aborted || this.consumePendingInterrupt(sessionId)) {
2113
+ void interruptStartedTurn(startedTurnId);
2005
2114
  this.activeTurns.delete(sessionId);
2006
2115
  }
2007
2116
  }
@@ -2018,8 +2127,9 @@ export class CodexRunner {
2018
2127
  this.endAppServerQueueWhenSubagentsDone(queue, state);
2019
2128
  }
2020
2129
  });
2021
- if (this.consumePendingInterrupt(sessionId)) {
2130
+ if (controller.signal.aborted || this.consumePendingInterrupt(sessionId)) {
2022
2131
  controller.abort('User interrupt');
2132
+ queue.end();
2023
2133
  this.activeAbortControllers.delete(sessionId);
2024
2134
  this.activeStreams.delete(sessionId);
2025
2135
  if (!resumingThread)
@@ -2086,8 +2196,10 @@ export class CodexRunner {
2086
2196
  // the short settle window below.
2087
2197
  await state.confirmProvisional?.(0);
2088
2198
  }
2089
- if (turnId && this.consumePendingInterrupt(sessionId)) {
2090
- await this.interruptAppServerTurn(threadId, turnId);
2199
+ if (turnId && (controller.signal.aborted || this.consumePendingInterrupt(sessionId))) {
2200
+ if (!state.turnCompleted && !['completed', 'failed', 'interrupted'].includes(turnResponse.turn?.status)) {
2201
+ await interruptStartedTurn(turnId);
2202
+ }
2091
2203
  controller.abort('User interrupt');
2092
2204
  this.activeAbortControllers.delete(sessionId);
2093
2205
  this.activeStreams.delete(sessionId);
@@ -2116,14 +2228,21 @@ export class CodexRunner {
2116
2228
  // ── Interrupt ──
2117
2229
  async interrupt(sessionKey) {
2118
2230
  this.permissionContexts.get(sessionKey)?.pauseController?.cancel();
2231
+ const operations = this.queryOperations.get(sessionKey);
2232
+ for (const operation of operations ?? []) {
2233
+ if (operation.lockAcquired) {
2234
+ operation.controller.abort(Object.assign(new Error('Codex operation interrupted'), { name: 'AbortError' }));
2235
+ }
2236
+ }
2119
2237
  const controller = this.activeAbortControllers.get(sessionKey);
2120
2238
  const activeTurn = this.activeTurns.get(sessionKey);
2121
- const hadActiveState = !!controller || !!activeTurn || this.activeStreams.has(sessionKey);
2239
+ const hadActiveState = !!controller || !!activeTurn || this.activeStreams.has(sessionKey)
2240
+ || [...(operations ?? [])].some(operation => operation.lockAcquired);
2122
2241
  const interruptTurn = activeTurn
2123
2242
  ? this.interruptAppServerTurn(activeTurn.threadId, activeTurn.turnId)
2124
2243
  : Promise.resolve();
2125
2244
  const interruptSubagents = this.interruptSubagentTurns(sessionKey);
2126
- if (!activeTurn)
2245
+ if (!activeTurn && !operations?.size)
2127
2246
  this.rememberPendingInterrupt(sessionKey);
2128
2247
  if (controller)
2129
2248
  controller.abort('User interrupt');
@@ -2206,12 +2325,21 @@ export class CodexRunner {
2206
2325
  const pending = [...this.unconfirmedInterrupts.entries()].filter(([, record]) => record.sessionId === sessionId || (threadId && record.threadId === threadId));
2207
2326
  await Promise.all(pending.map(async ([key, record]) => {
2208
2327
  try {
2209
- // Local stream closure and unsubscribe do not prove the backend turn
2210
- // stopped. Check its exact identity before allowing another writer.
2211
- const response = await withInterruptDeadline(record.client.threadRead(record.threadId, true, 2_000), 'Codex interrupt verification', 2_000);
2212
- const turn = response.thread?.turns?.find(turn => turn.id === record.turnId);
2328
+ const response = await withInterruptDeadline(record.client.threadRead(record.threadId, false, 2_000), 'Codex interrupt verification', 2_000);
2329
+ const threadStatus = response.thread?.status?.type;
2330
+ if (threadStatus === 'idle' || threadStatus === 'notLoaded') {
2331
+ if (this.unconfirmedInterrupts.get(key) === record) {
2332
+ this.unconfirmedInterrupts.delete(key);
2333
+ record.unsubscribe?.();
2334
+ }
2335
+ return;
2336
+ }
2337
+ if (threadStatus !== undefined)
2338
+ return;
2339
+ const history = await withInterruptDeadline(record.client.threadRead(record.threadId, true, 2_000), 'Codex interrupt history verification', 2_000);
2340
+ const turn = history.thread?.turns?.find(turn => turn.id === record.turnId);
2213
2341
  if (turn && ['completed', 'failed', 'interrupted'].includes(String(turn.status))
2214
- && !response.thread?.turns?.some(turn => turn.status === 'inProgress')
2342
+ && !history.thread?.turns?.some(turn => turn.status === 'inProgress')
2215
2343
  && this.unconfirmedInterrupts.get(key) === record) {
2216
2344
  this.unconfirmedInterrupts.delete(key);
2217
2345
  record.unsubscribe?.();
@@ -3219,7 +3347,7 @@ export class CodexRunner {
3219
3347
  return await denyInfrastructure('ec_command_not_canonical', canonicalEcApproval?.parseFailure
3220
3348
  ? `EC command approval denied because canonical argv parsing failed (${canonicalEcApproval.parseFailure.issue})`
3221
3349
  + (canonicalEcApproval.parseFailure.dialect === 'powershell' && canonicalEcApproval.parseFailure.issue === 'invalid-quote'
3222
- ? '. PowerShell treats smart quotes as delimiters too; escape quotes for PowerShell or use --text-from-file for message text.'
3350
+ ? '. PowerShell treats U+2018/U+2019/U+201A/U+201B as quote delimiters too. An EC inline text argument must be ASCII single-quoted; replace any character inside it that looks like a quote with U+02BC or U+FF07, or write the text to the session TMPDIR and pass it with --text-from-file.'
3223
3351
  : '')
3224
3352
  : 'EC command approval denied because no canonical argv could be derived', canonicalEcApproval?.parseFailure?.issue === 'invalid-quote' ? 'INVALID_QUOTE' : undefined);
3225
3353
  }
@@ -3542,15 +3670,18 @@ export class CodexRunner {
3542
3670
  ...(trackedItem.appContext && typeof trackedItem.appContext === 'object' ? { appContext: trackedItem.appContext } : {}),
3543
3671
  };
3544
3672
  const policyResult = this.permissionContexts.get(sessionKey)?.policyHook?.(toolName, toolInput);
3673
+ const systemSessionCheck = this.permissionContexts.get(sessionKey)?.systemSessionCheck;
3545
3674
  const workspacePath = this.resolvePermissionWorkspacePath(params);
3546
3675
  const mode = normalizeExecutionPermissionMode(this.chatModes.get(sessionKey) ?? this.currentMode);
3547
3676
  // MCP servers are inherited unless capability config explicitly disables
3548
3677
  // them. Apps retain their separate request/bypass lifecycle policy.
3549
- let allow = !policyResult?.block
3550
- && !!workspacePath
3551
- && (isAppCall
3552
- ? mode === 'request' || mode === 'bypass'
3553
- : mode !== 'readonly');
3678
+ let allow = systemSessionCheck
3679
+ ? systemSessionCheck(toolName, toolInput).allow
3680
+ : !policyResult?.block
3681
+ && !!workspacePath
3682
+ && (isAppCall
3683
+ ? mode === 'request' || mode === 'bypass'
3684
+ : mode !== 'readonly');
3554
3685
  if (allow) {
3555
3686
  try {
3556
3687
  for (const question of questions)
@@ -3838,6 +3969,7 @@ export class CodexRunner {
3838
3969
  // readonly decision. The map entry alone is stale if the directory was
3839
3970
  // replaced between preflight and the approval callback.
3840
3971
  managedTempDir: permCtx?.managedTempDir ?? this.getManagedTempDir(sessionKey),
3972
+ grantedReadRoots: permCtx?.grantedReadRoots,
3841
3973
  allowReadonlySourceDiagnostics: permCtx?.allowReadonlySourceDiagnostics === true,
3842
3974
  } : undefined;
3843
3975
  if (toolName === 'Bash')
@@ -3934,6 +4066,13 @@ export class CodexRunner {
3934
4066
  }
3935
4067
  async resolvePermissionDecision(sessionKey, toolName, toolInput, summary, reason, workspacePath = process.cwd(), operationCwd = workspacePath, requestId, callId) {
3936
4068
  const permissionContext = this.permissionContexts.get(sessionKey);
4069
+ // 系统会话:审批路径同样只看模块判定(FileChange / PermissionGrant 一律被模块拒绝)
4070
+ if (permissionContext?.systemSessionCheck) {
4071
+ const decision = permissionContext.systemSessionCheck(toolName, toolInput);
4072
+ return decision.allow
4073
+ ? 'allow'
4074
+ : { decision: 'deny', decisionSource: 'policy', policyCode: decision.code, reason: decision.reason };
4075
+ }
3937
4076
  const permissionPrompt = permissionContext?.sendPrompt ?? this.sendPromptFn;
3938
4077
  const hasFilesystemPermissions = (toolName === 'Bash' || toolName === 'PermissionGrant')
3939
4078
  && this.hasFilesystemPermissionRequest(toolInput);
@@ -4336,15 +4475,37 @@ export class CodexRunner {
4336
4475
  let input;
4337
4476
  let result;
4338
4477
  let isError = false;
4478
+ let executionFields = {};
4339
4479
  switch (type) {
4340
- case 'commandExecution':
4480
+ case 'commandExecution': {
4341
4481
  name = 'Shell';
4342
4482
  input = { command: item.command, cwd: item.cwd };
4343
4483
  result = item.aggregatedOutput ?? item.output ?? '';
4344
4484
  isError = item.exitCode != null
4345
4485
  ? Number(item.exitCode) !== 0
4346
4486
  : item.status === 'failed' || item.status === 'declined';
4487
+ const exitCode = typeof item.exitCode === 'number' && Number.isInteger(item.exitCode)
4488
+ ? item.exitCode
4489
+ : undefined;
4490
+ const itemError = item.error;
4491
+ const errorMessage = typeof itemError === 'string'
4492
+ ? itemError
4493
+ : itemError && typeof itemError === 'object' && 'message' in itemError
4494
+ && typeof itemError.message === 'string' ? itemError.message : undefined;
4495
+ executionFields = {
4496
+ ...(exitCode !== undefined ? { exit_code: exitCode } : {}),
4497
+ ...(item.aggregatedOutput != null ? { output_kind: 'combined' } : {}),
4498
+ ...(typeof item.durationMs === 'number' ? { duration_ms: item.durationMs } : {}),
4499
+ ...(isError ? {
4500
+ error: getToolResultErrorText({
4501
+ name, result, exitCode,
4502
+ error: errorMessage,
4503
+ reason: item.status === 'declined' ? 'Approval declined' : undefined,
4504
+ }),
4505
+ } : {}),
4506
+ };
4347
4507
  break;
4508
+ }
4348
4509
  case 'mcpToolCall':
4349
4510
  name = `MCP:${item.server}/${item.tool}`;
4350
4511
  input = item.arguments;
@@ -4405,6 +4566,7 @@ export class CodexRunner {
4405
4566
  ...(id ? { tool_use_id: id } : {}),
4406
4567
  content: result,
4407
4568
  is_error: isError,
4569
+ ...executionFields,
4408
4570
  },
4409
4571
  ];
4410
4572
  }
@@ -4965,17 +5127,12 @@ export class CodexRunner {
4965
5127
  }
4966
5128
  : undefined;
4967
5129
  const itemErrorText = outputTextForManagedHook(typeof item.error === 'string' ? item.error : item.error?.message);
4968
- const hookErrorText = /(?:Command|Tool call) blocked by PreToolUse hook\s*:/i.test(itemErrorText)
4969
- ? itemErrorText
4970
- : undefined;
4971
- const managedHookDenial = (item.status === 'failed' || item.status === 'declined' || hookErrorText)
4972
- ? parseManagedPreToolUseDenial([
4973
- item.aggregatedOutput,
4974
- item.output,
4975
- item.result,
4976
- hookErrorText,
4977
- ].filter(value => value !== undefined && value !== null).map(outputTextForManagedHook).join('\n'))
5130
+ const hookOutput = item.type === 'commandExecution' && item.exitCode == null && !itemErrorText
5131
+ && (item.status === 'failed' || item.status === 'declined')
5132
+ ? [item.aggregatedOutput, item.output, item.result].map(outputTextForManagedHook).find(text => /^\s*(?:Script failed\s+)?(?:Script error:\s*)?(?:Command|Tool call) blocked by PreToolUse hook\s*:/i.test(text))
4978
5133
  : undefined;
5134
+ const managedHookDenial = parseManagedPreToolUseDenial(itemErrorText)
5135
+ ?? parseManagedPreToolUseDenial(hookOutput);
4979
5136
  const managedHookFields = !denial && managedHookDenial
4980
5137
  ? {
4981
5138
  isError: true,
@@ -5056,9 +5213,11 @@ export class CodexRunner {
5056
5213
  type: 'tool_result',
5057
5214
  name: 'Shell',
5058
5215
  result: item.aggregatedOutput ?? '',
5216
+ ...(item.aggregatedOutput != null ? { outputKind: 'combined' } : {}),
5059
5217
  ...(blockedFields ?? {
5060
5218
  isError,
5061
5219
  ...codexItemErrorFields(item),
5220
+ ...(itemErrorText ? { error: itemErrorText } : {}),
5062
5221
  executed: true,
5063
5222
  executionState: isError ? 'failed' : 'completed',
5064
5223
  ...(exitCode !== undefined ? { exitCode } : {}),
@@ -5294,15 +5453,29 @@ export class CodexRunner {
5294
5453
  };
5295
5454
  return Object.values(usage).some(value => value !== undefined) ? usage : undefined;
5296
5455
  }
5297
- mapCodexContextUsage(raw) {
5456
+ mapCodexContextUsage(raw, fallbackModel, runtimeWindow) {
5298
5457
  if (!raw || typeof raw !== 'object')
5299
5458
  return undefined;
5300
- const totalTokens = this.pickNumber(raw.totalTokens, raw.total_tokens, raw.total);
5301
- const maxTokens = this.pickNumber(raw.maxTokens, raw.max_tokens, raw.max);
5302
- const model = typeof raw.model === 'string' ? raw.model : undefined;
5303
- if (totalTokens === undefined || maxTokens === undefined || !model)
5459
+ const totalTokens = this.pickNumber(raw.totalTokens, raw.total_tokens, raw.total, raw.last?.totalTokens, raw.last?.total_tokens);
5460
+ const rawModel = typeof raw.model === 'string' && raw.model.trim() ? raw.model : undefined;
5461
+ const fallbackWindow = fallbackModel ? parseModelId(fallbackModel).contextWindow : undefined;
5462
+ const model = fallbackWindow !== undefined ? fallbackModel : rawModel ?? fallbackModel;
5463
+ if (totalTokens === undefined || totalTokens < 0 || !model)
5464
+ return undefined;
5465
+ const reportedMaxTokens = [runtimeWindow, raw.modelContextWindow, raw.model_context_window, raw.maxTokens, raw.max_tokens, raw.max]
5466
+ .find(value => typeof value === 'number' && Number.isFinite(value) && value > 0);
5467
+ const suffixWindow = parseModelId(model).contextWindow;
5468
+ const maxTokens = reportedMaxTokens ?? suffixWindow ?? findModelSpec(resolveRoot(), model)?.context_window;
5469
+ if (maxTokens === undefined || !Number.isFinite(maxTokens) || maxTokens <= 0)
5304
5470
  return undefined;
5305
- const percentage = this.pickNumber(raw.percentage) ?? Math.round((totalTokens / maxTokens) * 100);
5471
+ const nativeWindow = [runtimeWindow, raw.modelContextWindow, raw.model_context_window]
5472
+ .find(value => typeof value === 'number' && Number.isFinite(value) && value > 0);
5473
+ const legacyWindow = [raw.maxTokens, raw.max_tokens, raw.max]
5474
+ .find(value => typeof value === 'number' && Number.isFinite(value) && value > 0);
5475
+ const percentage = (nativeWindow === undefined && (legacyWindow !== undefined || suffixWindow !== undefined)
5476
+ ? this.pickNumber(raw.percentage)
5477
+ : undefined)
5478
+ ?? Math.round((totalTokens / maxTokens) * 100);
5306
5479
  const effort = typeof raw.effort === 'string' ? raw.effort : undefined;
5307
5480
  return { totalTokens, maxTokens, percentage, model, effort };
5308
5481
  }
@@ -5310,7 +5483,12 @@ export class CodexRunner {
5310
5483
  const status = turn.status || 'completed';
5311
5484
  const outputLimit = status === 'failed' && isOutputTokenLimitError(new Error(turn.error?.message ?? ''));
5312
5485
  const tokenUsage = this.mapCodexTokenUsage(state.tokenUsage?.last ?? turn.tokenUsage ?? turn.usage);
5313
- const contextUsage = this.mapCodexContextUsage(turn.contextUsage ?? state.tokenUsage?.contextUsage ?? state.tokenUsage?.context);
5486
+ const terminalTokenUsage = turn.tokenUsage ?? turn.usage;
5487
+ const contextUsage = this.mapCodexContextUsage({
5488
+ ...state.tokenUsage,
5489
+ ...(terminalTokenUsage && typeof terminalTokenUsage === 'object' ? terminalTokenUsage : {}),
5490
+ ...(turn.contextUsage ?? state.tokenUsage?.contextUsage ?? state.tokenUsage?.context),
5491
+ }, state.modelContextId ?? state.model, this.pickNumber(state.tokenUsage?.modelContextWindow, state.tokenUsage?.model_context_window, terminalTokenUsage?.modelContextWindow, terminalTokenUsage?.model_context_window));
5314
5492
  const terminalReason = status === 'completed'
5315
5493
  ? undefined
5316
5494
  : status === 'interrupted'