evolcore 0.0.33 → 0.0.35-test.0

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 (152) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/bin/ec.js +3 -2
  3. package/bin/install-codex-managed-hooks.mjs +38 -4
  4. package/dist/agents/baseagent-availability.js +20 -36
  5. package/dist/agents/claude-runner.js +93 -14
  6. package/dist/agents/codex/diagnostics.js +79 -22
  7. package/dist/agents/codex-runner.js +245 -47
  8. package/dist/agents/ecagent-runner.js +90 -27
  9. package/dist/agents/runner-types.js +5 -6
  10. package/dist/aun/aid/agentmd.js +13 -2
  11. package/dist/aun/aid/client.js +1 -1
  12. package/dist/aun/aid/identity.js +1 -1
  13. package/dist/aun/msg/managed-operation.js +16 -0
  14. package/dist/aun/msg/p2p.js +18 -2
  15. package/dist/channels/aun.js +123 -7
  16. package/dist/channels/daemon.js +6 -3
  17. package/dist/cli/aun-commands.js +39 -14
  18. package/dist/cli/ctl-command.js +3 -1
  19. package/dist/cli/daemon-commands.js +130 -23
  20. package/dist/cli/fs-command.js +2 -1
  21. package/dist/cli/getmeta-command.js +211 -0
  22. package/dist/cli/index.js +26 -6
  23. package/dist/cli/integrity-command.js +196 -0
  24. package/dist/cli/managed-command-guard.js +3 -0
  25. package/dist/cli/memory-command.js +200 -156
  26. package/dist/cli/thread-command.js +17 -7
  27. package/dist/cli/trigger-command.js +6 -1
  28. package/dist/config/config-field-policy.js +4 -0
  29. package/dist/config/config-manager.js +19 -0
  30. package/dist/config/contact-book-store.js +5 -0
  31. package/dist/config/resolved-config-op.js +25 -8
  32. package/dist/config/role-service.js +13 -0
  33. package/dist/config/runtime-env.js +16 -0
  34. package/dist/config-store.js +30 -0
  35. package/dist/core/audit/log-integrity.js +3 -1
  36. package/dist/core/auth/agent-delegation.js +108 -35
  37. package/dist/core/auth/delegation-diagnostics.js +414 -0
  38. package/dist/core/auth/operation-authorizer.js +22 -3
  39. package/dist/core/auth/operation-catalog.js +16 -0
  40. package/dist/core/channel-runtime.js +1 -1
  41. package/dist/core/command/agent-control.js +18 -26
  42. package/dist/core/command/command-handler.js +145 -5
  43. package/dist/core/command/daemon-diagnostics.js +1 -0
  44. package/dist/core/command/menu-catalog.js +3 -0
  45. package/dist/core/command/menu-handler.js +24 -7
  46. package/dist/core/command/menu-protocol.js +1 -0
  47. package/dist/core/command/slash-handler.js +2 -0
  48. package/dist/core/command/topic-slash.js +4 -7
  49. package/dist/core/daemon-bootstrap.js +4 -7
  50. package/dist/core/event-catalog.js +14 -0
  51. package/dist/core/file-integrity/read.js +61 -0
  52. package/dist/core/file-integrity/store.js +262 -0
  53. package/dist/core/ipc-registration.js +47 -2
  54. package/dist/core/memory/batch-meta.js +133 -0
  55. package/dist/core/memory/consolidation-context.js +132 -28
  56. package/dist/core/memory/consolidation-ops-records.js +14 -16
  57. package/dist/core/memory/consolidation-trigger.js +15 -7
  58. package/dist/core/memory/consolidation.js +146 -26
  59. package/dist/core/memory/index-manager.js +19 -28
  60. package/dist/core/memory/memory-admin.js +19 -14
  61. package/dist/core/memory/memory-document.js +53 -9
  62. package/dist/core/memory/memory-writer.js +50 -19
  63. package/dist/core/message/aun-ipc-sender.js +19 -1
  64. package/dist/core/message/im-renderer.js +20 -4
  65. package/dist/core/message/message-bridge.js +5 -4
  66. package/dist/core/message/message-log.js +1 -0
  67. package/dist/core/message/peer-mode.js +17 -0
  68. package/dist/core/message/response-engine.js +598 -50
  69. package/dist/core/permission/ec-command-parser.js +169 -44
  70. package/dist/core/permission/protected-paths.js +5 -1
  71. package/dist/core/permission/readonly-shell-query.js +43 -5
  72. package/dist/core/permission/session-path-grants.js +334 -0
  73. package/dist/core/permission/tool-canonical.js +226 -0
  74. package/dist/core/permission/tool-policy.js +60 -62
  75. package/dist/core/relation/peer-identity.js +87 -12
  76. package/dist/core/role/runtime-policy.js +21 -10
  77. package/dist/core/session/session-fs-store.js +123 -8
  78. package/dist/core/session/session-manager.js +131 -6
  79. package/dist/core/session/session-title.js +26 -1
  80. package/dist/core/skill/index-generator.js +17 -3
  81. package/dist/core/system-session/grants.js +40 -0
  82. package/dist/core/system-session/memory.js +289 -0
  83. package/dist/core/system-session/registry.js +74 -0
  84. package/dist/core/system-session/types.js +14 -0
  85. package/dist/core/thread/command-parser.js +6 -1
  86. package/dist/core/thread/ipc-operation.js +33 -16
  87. package/dist/core/thread/thread-operation.js +62 -24
  88. package/dist/core/thread/thread-service.js +258 -51
  89. package/dist/eck/kit-renderer.js +7 -2
  90. package/dist/eck/manifest-engine.js +24 -4
  91. package/dist/index.js +11 -2
  92. package/dist/ipc.js +51 -40
  93. package/dist/paths.js +70 -0
  94. package/dist/stats/billing.js +20 -8
  95. package/dist/trigger/feedback.js +6 -1
  96. package/dist/trigger/parser.js +8 -1
  97. package/dist/trigger/system-definitions.js +3 -3
  98. package/dist/utils/cross-platform.js +17 -5
  99. package/dist/utils/error-dict.json +18 -4
  100. package/dist/utils/error-utils.js +121 -0
  101. package/dist/utils/instance-registry.js +13 -8
  102. package/dist/utils/log-writer.js +12 -0
  103. package/dist/utils/process-introspect.js +11 -0
  104. package/kits/docs/context-assembly.md +0 -1
  105. package/kits/docs/evolcore/agent-create.md +5 -4
  106. package/kits/docs/evolcore/agent.md +2 -2
  107. package/kits/docs/evolcore/config.md +2 -2
  108. package/kits/docs/evolcore/ctl.md +2 -2
  109. package/kits/docs/evolcore/ec-capabilities.md +1 -1
  110. package/kits/docs/evolcore/fs-architecture.md +9 -10
  111. package/kits/docs/evolcore/fs.md +2 -2
  112. package/kits/docs/evolcore/group-collaboration.md +3 -3
  113. package/kits/docs/evolcore/group.md +14 -12
  114. package/kits/docs/evolcore/memory-architecture.md +4 -5
  115. package/kits/docs/evolcore/memory-consolidation.md +19 -16
  116. package/kits/docs/evolcore/memory-tags.md +3 -1
  117. package/kits/docs/evolcore/memory.md +24 -12
  118. package/kits/docs/evolcore/model.md +16 -0
  119. package/kits/docs/evolcore/msg.md +7 -7
  120. package/kits/docs/evolcore/stats.md +1 -1
  121. package/kits/docs/evolcore/thread-chain.md +30 -14
  122. package/kits/docs/prompt-loading-architecture.md +0 -1
  123. package/kits/eck_manifest.auxiliary.json +9 -0
  124. package/kits/eck_manifest.json +20 -20
  125. package/kits/eck_manifest.memory.json +20 -12
  126. package/kits/rules/01-overview.md +11 -0
  127. package/kits/rules/03-identity.md +6 -3
  128. package/kits/rules/06-channel.md +1 -1
  129. package/kits/rules/07-memory-and-thread.md +13 -16
  130. package/kits/schemas/_meta.json +7 -1
  131. package/kits/schemas/agent-config.schema.13.json +1 -1
  132. package/kits/schemas/daemon.schema.7.json +140 -0
  133. package/kits/skills/ec-group/SKILL.md +8 -5
  134. package/kits/skills/ec-skill/scripts/cli.mjs +1 -1
  135. package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
  136. package/kits/skills/thread-ops/SKILL.md +18 -18
  137. package/kits/templates/memory-consolidation/forget.md +2 -2
  138. package/kits/templates/memory-consolidation/generalize.md +4 -4
  139. package/kits/templates/memory-consolidation/notes.md +12 -12
  140. package/kits/templates/memory-consolidation/profile.md +3 -3
  141. package/kits/templates/memory-consolidation/promote.md +3 -3
  142. package/kits/templates/memory-consolidation/workflow.md +42 -7
  143. package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
  144. package/kits/templates/system-fragments/bash-description.md +9 -0
  145. package/kits/templates/system-fragments/bootstrap.md +4 -3
  146. package/kits/templates/system-fragments/channel.md +2 -2
  147. package/kits/templates/system-fragments/memory.md +16 -9
  148. package/kits/templates/system-fragments/reply.md +6 -6
  149. package/kits/templates/system-fragments/session.md +1 -1
  150. package/package.json +4 -3
  151. package/dist/core/memory/memory-session-policy.js +0 -41
  152. package/dist/core/memory/memory-signature.js +0 -257
@@ -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';
@@ -86,6 +88,24 @@ class AsyncEventQueue {
86
88
  }
87
89
  }
88
90
  }
91
+ function parseCodexRetryProgress(message) {
92
+ const match = message.match(/\bReconnecting\.\.\.\s*(\d+)\s*\/\s*(\d+)\b/i);
93
+ if (!match)
94
+ return undefined;
95
+ const count = Number(match[1]);
96
+ const limit = Number(match[2]);
97
+ if (!Number.isSafeInteger(count) || !Number.isSafeInteger(limit) || count < 1 || limit < count)
98
+ return undefined;
99
+ return { count, limit };
100
+ }
101
+ function createCodexRetryMetadataError(message, state) {
102
+ const error = new Error(message);
103
+ if (state.modelRetryCount !== undefined)
104
+ error.codexRetryCount = state.modelRetryCount;
105
+ if (state.modelRetryLimit !== undefined)
106
+ error.codexRetryLimit = state.modelRetryLimit;
107
+ return error;
108
+ }
89
109
  function fallbackErrorCodeForToolDenial(decisionSource, policyCode) {
90
110
  if (decisionSource === 'policy' && policyCode === 'apply_patch_invalid_argument')
91
111
  return 'INVALID_ARGUMENT';
@@ -128,6 +148,24 @@ function codexItemErrorFields(item) {
128
148
  const errorDetailCode = normalizeToolErrorDetailCode(rawCode);
129
149
  return errorDetailCode ? { errorDetailCode } : {};
130
150
  }
151
+ function waitForCodexOperation(operation, signal) {
152
+ return new Promise((resolve, reject) => {
153
+ const onAbort = () => {
154
+ signal.removeEventListener('abort', onAbort);
155
+ reject(signal.reason);
156
+ };
157
+ signal.addEventListener('abort', onAbort, { once: true });
158
+ operation.then(value => {
159
+ signal.removeEventListener('abort', onAbort);
160
+ resolve(value);
161
+ }, error => {
162
+ signal.removeEventListener('abort', onAbort);
163
+ reject(error);
164
+ });
165
+ if (signal.aborted)
166
+ onAbort();
167
+ });
168
+ }
131
169
  function isCodexReasoningNotification(method) {
132
170
  return method === 'item/reasoning/textDelta'
133
171
  || method === 'item/reasoning/summaryTextDelta'
@@ -335,6 +373,11 @@ export async function getCodexAppServerAvailabilityAsync() {
335
373
  }
336
374
  let version;
337
375
  try {
376
+ // Each probe is a full Codex CLI launch (~0.3-0.45s on Windows). Run the
377
+ // app-server probe concurrently; its result is only consulted after the
378
+ // version gate passes, so the decision order is unchanged.
379
+ const appServerProbe = probeCli('codex', ['app-server', '--help'], options);
380
+ appServerProbe.catch(() => { });
338
381
  version = parseCodexCliVersion(await probeCli('codex', ['--version'], options)) ?? undefined;
339
382
  if (!version)
340
383
  return { available: false, reason: '无法读取 Codex CLI 版本。' };
@@ -342,7 +385,7 @@ export async function getCodexAppServerAvailabilityAsync() {
342
385
  return { available: false, version, requiresUpgrade: true,
343
386
  reason: `Codex CLI ${version} 低于最低要求 ${MIN_CODEX_CLI_VERSION}。请升级 Codex CLI:npm install -g @openai/codex@latest` };
344
387
  }
345
- await probeCli('codex', ['app-server', '--help'], options);
388
+ await appServerProbe;
346
389
  return { available: true, version };
347
390
  }
348
391
  catch (error) {
@@ -359,6 +402,7 @@ export class CodexRunner {
359
402
  retryInputSemantics = 'at_most_once';
360
403
  capabilities;
361
404
  model;
405
+ modelContextId;
362
406
  effort;
363
407
  activeAbortControllers = new Map();
364
408
  activeStreams = new Map();
@@ -378,6 +422,7 @@ export class CodexRunner {
378
422
  unconfirmedInterrupts = new Map();
379
423
  steeringTails = new Map();
380
424
  threadOperationLocks = new Map();
425
+ queryOperations = new Map();
381
426
  activeOperationReleases = new Map();
382
427
  /** Last app-server client that loaded each thread, including unsubscribed threads. */
383
428
  threadSubscriptions = new Map();
@@ -431,7 +476,8 @@ export class CodexRunner {
431
476
  fileRewind: 'git-head',
432
477
  fullaccess: true,
433
478
  };
434
- this.model = stripContextWindowSuffix(this.resolvedConfig.model);
479
+ this.modelContextId = this.resolvedConfig.model;
480
+ this.model = stripContextWindowSuffix(this.modelContextId);
435
481
  if (this.resolvedConfig.effort)
436
482
  this.effort = this.resolvedConfig.effort;
437
483
  this.onSessionIdUpdate = callbacks.onSessionIdUpdate;
@@ -1179,7 +1225,11 @@ export class CodexRunner {
1179
1225
  }
1180
1226
  }
1181
1227
  // ── ModelSwitcher ──
1182
- setModel(model) { this.model = stripContextWindowSuffix(model); this.resetAppServerClient(); }
1228
+ setModel(model) {
1229
+ this.modelContextId = model;
1230
+ this.model = stripContextWindowSuffix(model);
1231
+ this.resetAppServerClient();
1232
+ }
1183
1233
  getModel() { return this.model; }
1184
1234
  getModelCatalog() {
1185
1235
  return getRunnerCatalog('codex', this.resolvedConfig.agentAid, this.resolvedConfig);
@@ -1222,6 +1272,16 @@ export class CodexRunner {
1222
1272
  const digest = createHash('sha256').update(sessionId).digest('hex').slice(0, 24);
1223
1273
  return `__evolcore_${label}_${digest}_hclass_v1`;
1224
1274
  }
1275
+ /**
1276
+ * 系统会话的写入洞:只对模块声明的可写文件逐个开 write 规则,不开目录、不开 glob。
1277
+ * 声明为空(缺批次目录等)时返回空表,沙箱保持 :read-only。
1278
+ */
1279
+ resolveSystemSessionWriteRules(sessionId) {
1280
+ const writeFiles = this.permissionContexts.get(sessionId)?.systemSessionGrants?.writeFiles ?? [];
1281
+ return Object.fromEntries(writeFiles
1282
+ .filter(file => path.isAbsolute(file))
1283
+ .map(file => [path.resolve(file), 'write']));
1284
+ }
1225
1285
  resolveGitMetadataWriteRules(projectPath, mode) {
1226
1286
  if (mode === 'readonly')
1227
1287
  return {};
@@ -1298,6 +1358,7 @@ export class CodexRunner {
1298
1358
  expandLClassReadDeny: process.platform !== 'linux',
1299
1359
  }),
1300
1360
  ...this.resolveGitMetadataWriteRules(projectPath, mode),
1361
+ ...this.resolveSystemSessionWriteRules(sessionId),
1301
1362
  ...(bootstrapAgentMdPath ? {
1302
1363
  [path.resolve(bootstrapAgentMdPath)]: mode === 'readonly' ? 'read' : 'write',
1303
1364
  } : {}),
@@ -1330,12 +1391,18 @@ export class CodexRunner {
1330
1391
  }
1331
1392
  buildExecutionSandboxOptions(sessionId, mode, projectPath, bootstrapAgentMdPath, personalMdPath, permissionContext = this.permissionContexts.get(sessionId) ?? {}) {
1332
1393
  const decision = resolvePhaseOneExecutionSandbox(mode, permissionContext.role);
1394
+ // 系统会话明确声明不要沙箱:Codex 的 permission profile 以项目目录为中心,
1395
+ // 而系统会话的读写边界($EVOLCORE_HOME 下的批次目录与记忆目录)与 cwd 无关,
1396
+ // 硬套 profile 只会 fail closed。边界改由 systemSessionCheck 逐次调用判定。
1397
+ const sessionSandboxOff = this.permissionContexts.get(sessionId)?.systemSessionSandbox === 'off';
1398
+ const sandboxOff = decision.state === 'off' || sessionSandboxOff;
1333
1399
  return {
1334
1400
  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,
1401
+ permissionProfileConfig: sandboxOff
1402
+ ? undefined
1403
+ : this.buildPermissionProfileConfig(sessionId, decision.permissionMode, projectPath, bootstrapAgentMdPath, personalMdPath),
1404
+ sandbox: sandboxOff ? 'danger-full-access' : undefined,
1405
+ sessionSandboxOff,
1339
1406
  };
1340
1407
  }
1341
1408
  isTrustedFullAccessSession(sessionKey) {
@@ -1465,7 +1532,7 @@ export class CodexRunner {
1465
1532
  const normalizedInput = toolInput && typeof toolInput === 'object' && !Array.isArray(toolInput)
1466
1533
  ? toolInput
1467
1534
  : {};
1468
- const isShellTool = toolName === 'Bash' || toolName === 'Shell' || toolName === 'exec_command';
1535
+ const isShellTool = isShellToolName(toolName); // canonical normalisation via tool-canonical.ts
1469
1536
  const recordPreToolUseDenial = async (policyCode, reason) => {
1470
1537
  const summary = summarizeToolInputForAudit(toolName, normalizedInput).slice(0, 512);
1471
1538
  auditToolPreflightDenial({
@@ -1533,6 +1600,14 @@ export class CodexRunner {
1533
1600
  return { ok: true, applicable: true, decision: 'deny', reason };
1534
1601
  }
1535
1602
  }
1603
+ // 系统会话:模块判定即最终结论,不进入 preflight / 角色 / permissionMode 分支
1604
+ if (context.systemSessionCheck) {
1605
+ const decision = context.systemSessionCheck(isShellTool ? 'Bash' : toolName, normalizedInput);
1606
+ if (decision.allow)
1607
+ return { ok: true, applicable: true, decision: 'allow' };
1608
+ await recordPreToolUseDenial(decision.code, decision.reason);
1609
+ return { ok: true, applicable: true, decision: 'deny', reason: decision.reason };
1610
+ }
1536
1611
  if ((this.chatModes.get(sessionKey) ?? this.currentMode) === 'fullaccess') {
1537
1612
  if (this.isTrustedFullAccessSession(sessionKey))
1538
1613
  return { ok: true, applicable: true, decision: 'allow' };
@@ -1623,6 +1698,19 @@ export class CodexRunner {
1623
1698
  hasActiveStream(key) {
1624
1699
  return this.activeStreams.has(key) || this.activeAbortControllers.has(key) || this.activeTurns.has(key);
1625
1700
  }
1701
+ hasActiveOperation(sessionId, taskId) {
1702
+ const operations = this.queryOperations.get(sessionId);
1703
+ return !!operations && [...operations].some(operation => !taskId || operation.taskId === taskId);
1704
+ }
1705
+ cancelQueuedTurn(sessionId, identity) {
1706
+ const operation = [...(this.queryOperations.get(sessionId) ?? [])].find(candidate => !candidate.lockAcquired
1707
+ && candidate.taskId === identity.taskId
1708
+ && candidate.generation === identity.generation);
1709
+ if (!operation)
1710
+ return false;
1711
+ operation.controller.abort(Object.assign(new Error('Codex queued operation cancelled'), { name: 'AbortError' }));
1712
+ return true;
1713
+ }
1626
1714
  threadOperationKey(threadId, sessionId) {
1627
1715
  // Once a thread id is known it is the shared identity, including when
1628
1716
  // multiple EvolCore sessions happen to address the same Codex thread.
@@ -1659,6 +1747,7 @@ export class CodexRunner {
1659
1747
  merged.then(() => {
1660
1748
  if (primary.tail === merged)
1661
1749
  primary.tail = undefined;
1750
+ this.pruneThreadOperationLocks();
1662
1751
  });
1663
1752
  }
1664
1753
  for (const alias of other.aliases) {
@@ -1694,7 +1783,8 @@ export class CodexRunner {
1694
1783
  lock.aliases.delete(key);
1695
1784
  }
1696
1785
  }
1697
- async acquireThreadOperation(key, operation) {
1786
+ async acquireThreadOperation(key, operation, signal) {
1787
+ signal?.throwIfAborted();
1698
1788
  const lock = this.getThreadOperationLock(key);
1699
1789
  const previous = lock.tail;
1700
1790
  let releaseCurrent;
@@ -1702,12 +1792,8 @@ export class CodexRunner {
1702
1792
  releaseCurrent = resolve;
1703
1793
  });
1704
1794
  lock.tail = current;
1705
- if (previous) {
1706
- logger.info(`[CodexRunner] queued ${operation} behind an active operation: ${key}`);
1707
- await previous;
1708
- }
1709
1795
  let released = false;
1710
- return () => {
1796
+ const release = () => {
1711
1797
  if (released)
1712
1798
  return;
1713
1799
  released = true;
@@ -1716,6 +1802,21 @@ export class CodexRunner {
1716
1802
  releaseCurrent();
1717
1803
  this.pruneThreadOperationLocks();
1718
1804
  };
1805
+ try {
1806
+ if (previous) {
1807
+ logger.info(`[CodexRunner] queued ${operation} behind an active operation: ${key}`);
1808
+ await (signal ? waitForCodexOperation(previous, signal) : previous);
1809
+ }
1810
+ signal?.throwIfAborted();
1811
+ return release;
1812
+ }
1813
+ catch (error) {
1814
+ if (signal?.aborted && previous)
1815
+ void previous.then(release);
1816
+ else
1817
+ release();
1818
+ throw error;
1819
+ }
1719
1820
  }
1720
1821
  async *holdThreadOperation(stream, sessionId, release) {
1721
1822
  try {
@@ -1747,11 +1848,29 @@ export class CodexRunner {
1747
1848
  ? this.cachedThreadForTopic(sessionId, topicAgentSessionId)
1748
1849
  : initialAgentSessionId || this.activeSessions.get(sessionId);
1749
1850
  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');
1851
+ const operation = {
1852
+ controller: new AbortController(),
1853
+ taskId: modelOverride?.turn?.taskId ?? this.permissionContexts.get(sessionId)?.taskId,
1854
+ generation: modelOverride?.turn?.generation,
1855
+ lockAcquired: false,
1856
+ };
1857
+ const operations = this.queryOperations.get(sessionId) ?? new Set();
1858
+ operations.add(operation);
1859
+ this.queryOperations.set(sessionId, operations);
1860
+ let releaseLock;
1861
+ const release = () => {
1862
+ operations.delete(operation);
1863
+ if (this.queryOperations.get(sessionId) === operations && operations.size === 0) {
1864
+ this.queryOperations.delete(sessionId);
1865
+ }
1866
+ releaseLock?.();
1867
+ };
1752
1868
  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);
1869
+ await waitForCodexOperation(this.assertInterruptSettled(sessionId, modelOverride?.recovery?.agentSessionId || knownThreadId), operation.controller.signal);
1870
+ releaseLock = await this.acquireThreadOperation(operationKey, 'thread/resume+turn/start', operation.controller.signal);
1871
+ operation.lockAcquired = true;
1872
+ await waitForCodexOperation(this.assertInterruptSettled(sessionId, modelOverride?.recovery?.agentSessionId || knownThreadId), operation.controller.signal);
1873
+ const stream = await this.runQueryLocked(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv, operation.controller);
1755
1874
  this.activeOperationReleases.set(sessionId, release);
1756
1875
  return this.holdThreadOperation(stream, sessionId, release);
1757
1876
  }
@@ -1760,7 +1879,7 @@ export class CodexRunner {
1760
1879
  throw error;
1761
1880
  }
1762
1881
  }
1763
- async runQueryLocked(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv) {
1882
+ async runQueryLocked(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv, operationController) {
1764
1883
  // A daemon-managed task must never fall back to the app-server's
1765
1884
  // process-wide TMPDIR if its session namespace disappeared.
1766
1885
  getManagedTaskTempDir(runtimeEnv);
@@ -1789,7 +1908,8 @@ export class CodexRunner {
1789
1908
  }
1790
1909
  await this.assertInterruptSettled(sessionId, agentSessionId);
1791
1910
  let resumingThread = !!agentSessionId;
1792
- const callModel = stripContextWindowSuffix(modelOverride?.model || this.model);
1911
+ const modelContextId = modelOverride?.model || this.modelContextId;
1912
+ const callModel = stripContextWindowSuffix(modelContextId);
1793
1913
  const callEffort = modelOverride?.effortMode === 'model_default'
1794
1914
  ? undefined
1795
1915
  : modelOverride?.effort ?? this.effort;
@@ -1901,6 +2021,7 @@ export class CodexRunner {
1901
2021
  if (!resumingThread && this.ensureThreadCredential(threadId, threadSessionToken) !== threadSessionToken) {
1902
2022
  throw new Error('Codex thread session credential could not be bound');
1903
2023
  }
2024
+ this.permissionContexts.get(sessionId)?.bindSessionCredential?.(threadSessionToken, threadId);
1904
2025
  const turn = modelOverride?.turn;
1905
2026
  if (!resumingThread) {
1906
2027
  // Keep a newly-created backend private to this runner until turn/start
@@ -1930,7 +2051,7 @@ export class CodexRunner {
1930
2051
  }
1931
2052
  this.threadProjectPaths.set(threadId, path.resolve(projectPath));
1932
2053
  this.bindExternalToolBoundary(threadId, externalToolConfig);
1933
- const controller = new AbortController();
2054
+ const controller = operationController ?? new AbortController();
1934
2055
  this.activeAbortControllers.set(sessionId, controller);
1935
2056
  const answer = modelOverride?.asyncQuestionAnswer;
1936
2057
  if (answer && answer.providerThreadId !== threadId) {
@@ -1944,6 +2065,7 @@ export class CodexRunner {
1944
2065
  sessionId,
1945
2066
  projectPath,
1946
2067
  model: callModel,
2068
+ modelContextId,
1947
2069
  committed: resumingThread,
1948
2070
  persistenceFinalized: resumingThread,
1949
2071
  streamedAgentMessageIds: new Set(),
@@ -1957,6 +2079,11 @@ export class CodexRunner {
1957
2079
  inputId: turn?.inputId,
1958
2080
  inputAcceptedEmitted: false,
1959
2081
  };
2082
+ let startupInterrupt;
2083
+ const interruptStartedTurn = (turnId) => {
2084
+ startupInterrupt ??= this.interruptAppServerTurn(threadId, turnId);
2085
+ return startupInterrupt;
2086
+ };
1960
2087
  const unsubscribe = appServer.onNotification(notification => {
1961
2088
  // 仅从 turn/started 锁定权威 turnId — resume 时会有上一轮 turn 的残留通知
1962
2089
  // (如 thread/tokenUsage/updated)先于新 turn 到达,不能用它们 latch turnId
@@ -2000,8 +2127,8 @@ export class CodexRunner {
2000
2127
  if (startedTurnId && !state.turnId) {
2001
2128
  state.turnId = startedTurnId;
2002
2129
  this.activeTurns.set(sessionId, { threadId, turnId: startedTurnId });
2003
- if (this.consumePendingInterrupt(sessionId)) {
2004
- this.interruptAppServerTurn(threadId, startedTurnId).catch(() => { });
2130
+ if (controller.signal.aborted || this.consumePendingInterrupt(sessionId)) {
2131
+ void interruptStartedTurn(startedTurnId);
2005
2132
  this.activeTurns.delete(sessionId);
2006
2133
  }
2007
2134
  }
@@ -2018,8 +2145,9 @@ export class CodexRunner {
2018
2145
  this.endAppServerQueueWhenSubagentsDone(queue, state);
2019
2146
  }
2020
2147
  });
2021
- if (this.consumePendingInterrupt(sessionId)) {
2148
+ if (controller.signal.aborted || this.consumePendingInterrupt(sessionId)) {
2022
2149
  controller.abort('User interrupt');
2150
+ queue.end();
2023
2151
  this.activeAbortControllers.delete(sessionId);
2024
2152
  this.activeStreams.delete(sessionId);
2025
2153
  if (!resumingThread)
@@ -2086,8 +2214,10 @@ export class CodexRunner {
2086
2214
  // the short settle window below.
2087
2215
  await state.confirmProvisional?.(0);
2088
2216
  }
2089
- if (turnId && this.consumePendingInterrupt(sessionId)) {
2090
- await this.interruptAppServerTurn(threadId, turnId);
2217
+ if (turnId && (controller.signal.aborted || this.consumePendingInterrupt(sessionId))) {
2218
+ if (!state.turnCompleted && !['completed', 'failed', 'interrupted'].includes(turnResponse.turn?.status)) {
2219
+ await interruptStartedTurn(turnId);
2220
+ }
2091
2221
  controller.abort('User interrupt');
2092
2222
  this.activeAbortControllers.delete(sessionId);
2093
2223
  this.activeStreams.delete(sessionId);
@@ -2116,14 +2246,21 @@ export class CodexRunner {
2116
2246
  // ── Interrupt ──
2117
2247
  async interrupt(sessionKey) {
2118
2248
  this.permissionContexts.get(sessionKey)?.pauseController?.cancel();
2249
+ const operations = this.queryOperations.get(sessionKey);
2250
+ for (const operation of operations ?? []) {
2251
+ if (operation.lockAcquired) {
2252
+ operation.controller.abort(Object.assign(new Error('Codex operation interrupted'), { name: 'AbortError' }));
2253
+ }
2254
+ }
2119
2255
  const controller = this.activeAbortControllers.get(sessionKey);
2120
2256
  const activeTurn = this.activeTurns.get(sessionKey);
2121
- const hadActiveState = !!controller || !!activeTurn || this.activeStreams.has(sessionKey);
2257
+ const hadActiveState = !!controller || !!activeTurn || this.activeStreams.has(sessionKey)
2258
+ || [...(operations ?? [])].some(operation => operation.lockAcquired);
2122
2259
  const interruptTurn = activeTurn
2123
2260
  ? this.interruptAppServerTurn(activeTurn.threadId, activeTurn.turnId)
2124
2261
  : Promise.resolve();
2125
2262
  const interruptSubagents = this.interruptSubagentTurns(sessionKey);
2126
- if (!activeTurn)
2263
+ if (!activeTurn && !operations?.size)
2127
2264
  this.rememberPendingInterrupt(sessionKey);
2128
2265
  if (controller)
2129
2266
  controller.abort('User interrupt');
@@ -3228,7 +3365,7 @@ export class CodexRunner {
3228
3365
  return await denyInfrastructure('ec_command_not_canonical', canonicalEcApproval?.parseFailure
3229
3366
  ? `EC command approval denied because canonical argv parsing failed (${canonicalEcApproval.parseFailure.issue})`
3230
3367
  + (canonicalEcApproval.parseFailure.dialect === 'powershell' && canonicalEcApproval.parseFailure.issue === 'invalid-quote'
3231
- ? '. PowerShell treats smart quotes as delimiters too; escape quotes for PowerShell or use --text-from-file for message text.'
3368
+ ? '. 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.'
3232
3369
  : '')
3233
3370
  : 'EC command approval denied because no canonical argv could be derived', canonicalEcApproval?.parseFailure?.issue === 'invalid-quote' ? 'INVALID_QUOTE' : undefined);
3234
3371
  }
@@ -3551,15 +3688,18 @@ export class CodexRunner {
3551
3688
  ...(trackedItem.appContext && typeof trackedItem.appContext === 'object' ? { appContext: trackedItem.appContext } : {}),
3552
3689
  };
3553
3690
  const policyResult = this.permissionContexts.get(sessionKey)?.policyHook?.(toolName, toolInput);
3691
+ const systemSessionCheck = this.permissionContexts.get(sessionKey)?.systemSessionCheck;
3554
3692
  const workspacePath = this.resolvePermissionWorkspacePath(params);
3555
3693
  const mode = normalizeExecutionPermissionMode(this.chatModes.get(sessionKey) ?? this.currentMode);
3556
3694
  // MCP servers are inherited unless capability config explicitly disables
3557
3695
  // them. Apps retain their separate request/bypass lifecycle policy.
3558
- let allow = !policyResult?.block
3559
- && !!workspacePath
3560
- && (isAppCall
3561
- ? mode === 'request' || mode === 'bypass'
3562
- : mode !== 'readonly');
3696
+ let allow = systemSessionCheck
3697
+ ? systemSessionCheck(toolName, toolInput).allow
3698
+ : !policyResult?.block
3699
+ && !!workspacePath
3700
+ && (isAppCall
3701
+ ? mode === 'request' || mode === 'bypass'
3702
+ : mode !== 'readonly');
3563
3703
  if (allow) {
3564
3704
  try {
3565
3705
  for (const question of questions)
@@ -3847,6 +3987,7 @@ export class CodexRunner {
3847
3987
  // readonly decision. The map entry alone is stale if the directory was
3848
3988
  // replaced between preflight and the approval callback.
3849
3989
  managedTempDir: permCtx?.managedTempDir ?? this.getManagedTempDir(sessionKey),
3990
+ grantedReadRoots: permCtx?.grantedReadRoots,
3850
3991
  allowReadonlySourceDiagnostics: permCtx?.allowReadonlySourceDiagnostics === true,
3851
3992
  } : undefined;
3852
3993
  if (toolName === 'Bash')
@@ -3943,6 +4084,13 @@ export class CodexRunner {
3943
4084
  }
3944
4085
  async resolvePermissionDecision(sessionKey, toolName, toolInput, summary, reason, workspacePath = process.cwd(), operationCwd = workspacePath, requestId, callId) {
3945
4086
  const permissionContext = this.permissionContexts.get(sessionKey);
4087
+ // 系统会话:审批路径同样只看模块判定(FileChange / PermissionGrant 一律被模块拒绝)
4088
+ if (permissionContext?.systemSessionCheck) {
4089
+ const decision = permissionContext.systemSessionCheck(toolName, toolInput);
4090
+ return decision.allow
4091
+ ? 'allow'
4092
+ : { decision: 'deny', decisionSource: 'policy', policyCode: decision.code, reason: decision.reason };
4093
+ }
3946
4094
  const permissionPrompt = permissionContext?.sendPrompt ?? this.sendPromptFn;
3947
4095
  const hasFilesystemPermissions = (toolName === 'Bash' || toolName === 'PermissionGrant')
3948
4096
  && this.hasFilesystemPermissionRequest(toolInput);
@@ -4345,15 +4493,37 @@ export class CodexRunner {
4345
4493
  let input;
4346
4494
  let result;
4347
4495
  let isError = false;
4496
+ let executionFields = {};
4348
4497
  switch (type) {
4349
- case 'commandExecution':
4498
+ case 'commandExecution': {
4350
4499
  name = 'Shell';
4351
4500
  input = { command: item.command, cwd: item.cwd };
4352
4501
  result = item.aggregatedOutput ?? item.output ?? '';
4353
4502
  isError = item.exitCode != null
4354
4503
  ? Number(item.exitCode) !== 0
4355
4504
  : item.status === 'failed' || item.status === 'declined';
4505
+ const exitCode = typeof item.exitCode === 'number' && Number.isInteger(item.exitCode)
4506
+ ? item.exitCode
4507
+ : undefined;
4508
+ const itemError = item.error;
4509
+ const errorMessage = typeof itemError === 'string'
4510
+ ? itemError
4511
+ : itemError && typeof itemError === 'object' && 'message' in itemError
4512
+ && typeof itemError.message === 'string' ? itemError.message : undefined;
4513
+ executionFields = {
4514
+ ...(exitCode !== undefined ? { exit_code: exitCode } : {}),
4515
+ ...(item.aggregatedOutput != null ? { output_kind: 'combined' } : {}),
4516
+ ...(typeof item.durationMs === 'number' ? { duration_ms: item.durationMs } : {}),
4517
+ ...(isError ? {
4518
+ error: getToolResultErrorText({
4519
+ name, result, exitCode,
4520
+ error: errorMessage,
4521
+ reason: item.status === 'declined' ? 'Approval declined' : undefined,
4522
+ }),
4523
+ } : {}),
4524
+ };
4356
4525
  break;
4526
+ }
4357
4527
  case 'mcpToolCall':
4358
4528
  name = `MCP:${item.server}/${item.tool}`;
4359
4529
  input = item.arguments;
@@ -4414,6 +4584,7 @@ export class CodexRunner {
4414
4584
  ...(id ? { tool_use_id: id } : {}),
4415
4585
  content: result,
4416
4586
  is_error: isError,
4587
+ ...executionFields,
4417
4588
  },
4418
4589
  ];
4419
4590
  }
@@ -4778,7 +4949,7 @@ export class CodexRunner {
4778
4949
  lastReportedUsage: state.tokenUsage?.last,
4779
4950
  })}`);
4780
4951
  if (isRetryableError(new Error(turn.error.message))) {
4781
- throw new Error(turn.error.message);
4952
+ throw createCodexRetryMetadataError(turn.error.message, state);
4782
4953
  }
4783
4954
  yield { type: 'error', error: turn.error.message, errorType: toAgentErrorType(turn.error.message) };
4784
4955
  }
@@ -4800,6 +4971,11 @@ export class CodexRunner {
4800
4971
  // Codex owns retries within this turn. Throwing here closes our subscription
4801
4972
  // before its recovery can finish; runQuery must not submit the input again.
4802
4973
  if (params.willRetry === true) {
4974
+ const retryProgress = parseCodexRetryProgress(message);
4975
+ if (retryProgress) {
4976
+ state.modelRetryCount = Math.max(state.modelRetryCount ?? 0, retryProgress.count);
4977
+ state.modelRetryLimit = retryProgress.limit;
4978
+ }
4803
4979
  yield {
4804
4980
  type: 'notice', severity: 'warn', subtype: 'codex-model-retry',
4805
4981
  message: isOutputTokenLimitError(new Error(message))
@@ -4808,8 +4984,9 @@ export class CodexRunner {
4808
4984
  };
4809
4985
  break;
4810
4986
  }
4811
- if (isRetryableError(new Error(message)))
4812
- throw new Error(message);
4987
+ if (isRetryableError(new Error(message))) {
4988
+ throw createCodexRetryMetadataError(message, state);
4989
+ }
4813
4990
  yield { type: 'error', error: message, errorType: toAgentErrorType(message) };
4814
4991
  break;
4815
4992
  }
@@ -5060,9 +5237,11 @@ export class CodexRunner {
5060
5237
  type: 'tool_result',
5061
5238
  name: 'Shell',
5062
5239
  result: item.aggregatedOutput ?? '',
5240
+ ...(item.aggregatedOutput != null ? { outputKind: 'combined' } : {}),
5063
5241
  ...(blockedFields ?? {
5064
5242
  isError,
5065
5243
  ...codexItemErrorFields(item),
5244
+ ...(itemErrorText ? { error: itemErrorText } : {}),
5066
5245
  executed: true,
5067
5246
  executionState: isError ? 'failed' : 'completed',
5068
5247
  ...(exitCode !== undefined ? { exitCode } : {}),
@@ -5298,15 +5477,29 @@ export class CodexRunner {
5298
5477
  };
5299
5478
  return Object.values(usage).some(value => value !== undefined) ? usage : undefined;
5300
5479
  }
5301
- mapCodexContextUsage(raw) {
5480
+ mapCodexContextUsage(raw, fallbackModel, runtimeWindow) {
5302
5481
  if (!raw || typeof raw !== 'object')
5303
5482
  return undefined;
5304
- const totalTokens = this.pickNumber(raw.totalTokens, raw.total_tokens, raw.total);
5305
- const maxTokens = this.pickNumber(raw.maxTokens, raw.max_tokens, raw.max);
5306
- const model = typeof raw.model === 'string' ? raw.model : undefined;
5307
- if (totalTokens === undefined || maxTokens === undefined || !model)
5483
+ const totalTokens = this.pickNumber(raw.totalTokens, raw.total_tokens, raw.total, raw.last?.totalTokens, raw.last?.total_tokens);
5484
+ const rawModel = typeof raw.model === 'string' && raw.model.trim() ? raw.model : undefined;
5485
+ const fallbackWindow = fallbackModel ? parseModelId(fallbackModel).contextWindow : undefined;
5486
+ const model = fallbackWindow !== undefined ? fallbackModel : rawModel ?? fallbackModel;
5487
+ if (totalTokens === undefined || totalTokens < 0 || !model)
5488
+ return undefined;
5489
+ const reportedMaxTokens = [runtimeWindow, raw.modelContextWindow, raw.model_context_window, raw.maxTokens, raw.max_tokens, raw.max]
5490
+ .find(value => typeof value === 'number' && Number.isFinite(value) && value > 0);
5491
+ const suffixWindow = parseModelId(model).contextWindow;
5492
+ const maxTokens = reportedMaxTokens ?? suffixWindow ?? findModelSpec(resolveRoot(), model)?.context_window;
5493
+ if (maxTokens === undefined || !Number.isFinite(maxTokens) || maxTokens <= 0)
5308
5494
  return undefined;
5309
- const percentage = this.pickNumber(raw.percentage) ?? Math.round((totalTokens / maxTokens) * 100);
5495
+ const nativeWindow = [runtimeWindow, raw.modelContextWindow, raw.model_context_window]
5496
+ .find(value => typeof value === 'number' && Number.isFinite(value) && value > 0);
5497
+ const legacyWindow = [raw.maxTokens, raw.max_tokens, raw.max]
5498
+ .find(value => typeof value === 'number' && Number.isFinite(value) && value > 0);
5499
+ const percentage = (nativeWindow === undefined && (legacyWindow !== undefined || suffixWindow !== undefined)
5500
+ ? this.pickNumber(raw.percentage)
5501
+ : undefined)
5502
+ ?? Math.round((totalTokens / maxTokens) * 100);
5310
5503
  const effort = typeof raw.effort === 'string' ? raw.effort : undefined;
5311
5504
  return { totalTokens, maxTokens, percentage, model, effort };
5312
5505
  }
@@ -5314,7 +5507,12 @@ export class CodexRunner {
5314
5507
  const status = turn.status || 'completed';
5315
5508
  const outputLimit = status === 'failed' && isOutputTokenLimitError(new Error(turn.error?.message ?? ''));
5316
5509
  const tokenUsage = this.mapCodexTokenUsage(state.tokenUsage?.last ?? turn.tokenUsage ?? turn.usage);
5317
- const contextUsage = this.mapCodexContextUsage(turn.contextUsage ?? state.tokenUsage?.contextUsage ?? state.tokenUsage?.context);
5510
+ const terminalTokenUsage = turn.tokenUsage ?? turn.usage;
5511
+ const contextUsage = this.mapCodexContextUsage({
5512
+ ...state.tokenUsage,
5513
+ ...(terminalTokenUsage && typeof terminalTokenUsage === 'object' ? terminalTokenUsage : {}),
5514
+ ...(turn.contextUsage ?? state.tokenUsage?.contextUsage ?? state.tokenUsage?.context),
5515
+ }, state.modelContextId ?? state.model, this.pickNumber(state.tokenUsage?.modelContextWindow, state.tokenUsage?.model_context_window, terminalTokenUsage?.modelContextWindow, terminalTokenUsage?.model_context_window));
5318
5516
  const terminalReason = status === 'completed'
5319
5517
  ? undefined
5320
5518
  : status === 'interrupted'