evolcore 0.0.25 → 0.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/CHANGELOG.md +632 -565
  2. package/LICENSE +21 -21
  3. package/README.md +455 -455
  4. package/bin/codex-managed-acl.ps1 +203 -160
  5. package/bin/codex-managed-hook.mjs +208 -208
  6. package/bin/ec-safe-output.js +327 -327
  7. package/bin/ec.js +40 -29
  8. package/bin/install-codex-managed-hooks.mjs +582 -555
  9. package/dist/agents/baseagent-availability.js +68 -0
  10. package/dist/agents/baseagent.js +96 -46
  11. package/dist/agents/claude-runner.js +172 -33
  12. package/dist/agents/codex/app-server-client.js +76 -31
  13. package/dist/agents/codex/catalog.js +1 -1
  14. package/dist/agents/codex/diagnostics.js +34 -1
  15. package/dist/agents/codex-runner.js +434 -446
  16. package/dist/agents/ecagent-runner.js +174 -34
  17. package/dist/agents/gemini-runner.js +8 -7
  18. package/dist/agents/runner-types.js +75 -19
  19. package/dist/aun/aid/agentmd.js +151 -36
  20. package/dist/aun/aid/client.js +1 -1
  21. package/dist/aun/aid/control-aid.js +180 -1
  22. package/dist/aun/aid/identity.js +4 -4
  23. package/dist/aun/aid/index.js +1 -1
  24. package/dist/aun/aid/store.js +17 -13
  25. package/dist/aun/group/history.js +303 -0
  26. package/dist/aun/group/identity.js +43 -0
  27. package/dist/aun/group/index.js +5 -0
  28. package/dist/aun/{msg/group.js → group/operations.js} +162 -39
  29. package/dist/aun/{menu-client.js → menu/client.js} +85 -6
  30. package/dist/aun/menu/index.js +2 -0
  31. package/dist/aun/menu/transport.js +49 -0
  32. package/dist/aun/msg/index.js +2 -1
  33. package/dist/aun/msg/managed-operation.js +85 -19
  34. package/dist/aun/msg/mention-schema.js +51 -0
  35. package/dist/aun/msg/p2p.js +19 -9
  36. package/dist/aun/msg/upload.js +43 -80
  37. package/dist/aun/outbox.js +29 -7
  38. package/dist/aun/rpc/client.js +1 -0
  39. package/dist/aun/storage/client.js +39 -50
  40. package/dist/channels/aun.js +842 -318
  41. package/dist/channels/daemon.js +10 -6
  42. package/dist/cli/agent-command.js +212 -89
  43. package/dist/cli/agent-create.js +626 -11
  44. package/dist/cli/agent.js +125 -13
  45. package/dist/cli/aun-commands.js +487 -337
  46. package/dist/cli/bench.js +20 -20
  47. package/dist/cli/config.js +55 -33
  48. package/dist/cli/contact.js +72 -15
  49. package/dist/cli/ctl-command.js +24 -24
  50. package/dist/cli/daemon-commands.js +140 -63
  51. package/dist/cli/diag.js +31 -0
  52. package/dist/cli/fs-command.js +70 -70
  53. package/dist/cli/handoff-command.js +46 -14
  54. package/dist/cli/index.js +91 -79
  55. package/dist/cli/init.js +34 -1
  56. package/dist/cli/link-rules.js +21 -21
  57. package/dist/cli/managed-command-guard.js +5 -1
  58. package/dist/cli/menu-client.js +51 -32
  59. package/dist/cli/model.js +3 -3
  60. package/dist/cli/net-check.js +22 -22
  61. package/dist/cli/queue-command.js +31 -31
  62. package/dist/cli/response.js +32 -32
  63. package/dist/cli/restart-monitor.js +103 -42
  64. package/dist/cli/stats.js +51 -51
  65. package/dist/cli/task-command.js +59 -0
  66. package/dist/cli/task-context.js +97 -49
  67. package/dist/cli/trigger-command.js +74 -64
  68. package/dist/config/builtin-roles.js +11 -0
  69. package/dist/config/config-field-policy.js +11 -0
  70. package/dist/config/config-manager.js +32 -4
  71. package/dist/config/config-operation-service.js +160 -0
  72. package/dist/config/contact-book-store.js +1 -1
  73. package/dist/config/contact-book.js +1 -1
  74. package/dist/config/contact-operation-service.js +97 -8
  75. package/dist/config/peer-role-resolver.js +1 -1
  76. package/dist/config/resolved-config-op.js +30 -7
  77. package/dist/config-store.js +1 -1
  78. package/dist/core/agent-application-service.js +7 -1
  79. package/dist/core/audit/event-key.js +17 -2
  80. package/dist/core/audit/log-integrity.js +34 -8
  81. package/dist/core/auth/agent-delegation.js +48 -14
  82. package/dist/core/auth/auth-gateway.js +2 -0
  83. package/dist/core/auth/authorization-audit.js +53 -49
  84. package/dist/core/auth/operation-authorizer.js +90 -7
  85. package/dist/core/auth/operation-catalog.js +145 -4
  86. package/dist/core/auth/remote-access-store.js +155 -10
  87. package/dist/core/bootstrap-messages.js +66 -7
  88. package/dist/core/bootstrap-service.js +111 -20
  89. package/dist/core/command/agent-control.js +88 -7
  90. package/dist/core/command/agent-storage-menu.js +443 -0
  91. package/dist/core/command/cli-intent-parser.js +19 -0
  92. package/dist/core/command/command-handler.js +159 -65
  93. package/dist/core/command/data-root-resolver.js +273 -0
  94. package/dist/core/command/diag-service.js +118 -0
  95. package/dist/core/command/group-menu.js +7 -2
  96. package/dist/core/command/menu-catalog.js +46 -7
  97. package/dist/core/command/menu-handler.js +392 -100
  98. package/dist/core/command/menu-protocol.js +45 -6
  99. package/dist/core/command/menu-token-store.js +41 -8
  100. package/dist/core/command/role-menu.js +2 -2
  101. package/dist/core/command/slash-gate.js +34 -4
  102. package/dist/core/command/slash-handler.js +123 -43
  103. package/dist/core/event-catalog.js +4 -0
  104. package/dist/core/evolagent-registry.js +28 -2
  105. package/dist/core/group/invite-service.js +924 -0
  106. package/dist/core/handoff/runtime.js +205 -27
  107. package/dist/core/handoff/store.js +193 -4
  108. package/dist/core/interaction-router.js +22 -8
  109. package/dist/core/message/im-renderer.js +146 -34
  110. package/dist/core/message/message-bridge.js +163 -20
  111. package/dist/core/message/message-log.js +5 -3
  112. package/dist/core/message/message-queue.js +99 -41
  113. package/dist/core/message/message-utils.js +1 -0
  114. package/dist/core/message/response-engine.js +755 -144
  115. package/dist/core/model/catalog-service.js +79 -49
  116. package/dist/core/model/context-window-suffix.js +33 -0
  117. package/dist/core/model/model-catalog.js +1 -1
  118. package/dist/core/model/model-permission.js +12 -3
  119. package/dist/core/permission/approval-gateway.js +36 -3
  120. package/dist/core/permission/ec-command-parser.js +112 -41
  121. package/dist/core/permission/protected-paths.js +134 -16
  122. package/dist/core/permission/sandbox-runtime.js +108 -17
  123. package/dist/core/permission/tool-error-code.js +15 -0
  124. package/dist/core/permission/tool-policy.js +9 -8
  125. package/dist/core/relation/peer-identity.js +57 -6
  126. package/dist/core/relation/shared-data.js +106 -0
  127. package/dist/core/role/runtime-policy.js +64 -0
  128. package/dist/core/runtime-lock.js +10 -26
  129. package/dist/core/session/runner-task-history.js +170 -0
  130. package/dist/core/session/session-manager.js +34 -6
  131. package/dist/core/session/session-mapper.js +41 -0
  132. package/dist/core/session/session-turn-coordinator.js +37 -7
  133. package/dist/core/session/session-turns.js +48 -7
  134. package/dist/core/task/task-recovery-service.js +176 -0
  135. package/dist/core/task/task-resume-operation.js +32 -0
  136. package/dist/eck/group-rules-sync.js +5 -3
  137. package/dist/eck/kit-renderer.js +1 -0
  138. package/dist/index.js +588 -217
  139. package/dist/ipc.js +193 -15
  140. package/dist/paths.js +17 -4
  141. package/dist/product.js +2 -0
  142. package/dist/response-system/engines/v1/proactive-flow.js +59 -2
  143. package/dist/stats/billing.js +41 -15
  144. package/dist/stats/db.js +159 -159
  145. package/dist/stats/eck-vars.js +9 -9
  146. package/dist/stats/price-resolver.js +28 -10
  147. package/dist/stats/query.js +139 -139
  148. package/dist/stats/role-budget.js +6 -6
  149. package/dist/stats/writer.js +51 -51
  150. package/dist/trigger/event-source.js +19 -0
  151. package/dist/trigger/feedback.js +233 -40
  152. package/dist/trigger/parser.js +55 -5
  153. package/dist/trigger/patch.js +9 -0
  154. package/dist/trigger/scheduler.js +30 -4
  155. package/dist/trigger/validation.js +20 -3
  156. package/dist/utils/cross-platform.js +30 -1
  157. package/dist/utils/ecweb-utils.js +1 -0
  158. package/dist/utils/error-dict.json +160 -160
  159. package/dist/utils/error-utils.js +61 -2
  160. package/dist/utils/evolcore-version.js +129 -0
  161. package/dist/utils/logger.js +26 -14
  162. package/dist/utils/macos-autostart.js +22 -22
  163. package/dist/utils/model-prices.jsonl +23 -23
  164. package/dist/utils/npm-ops.js +176 -35
  165. package/dist/utils/tool-summary.js +59 -3
  166. package/dist/utils/welcome.js +196 -23
  167. package/ecagent/LICENSE +21 -21
  168. package/ecagent/NOTICE +11 -11
  169. package/ecagent/README.md +61 -61
  170. package/ecagent/dist/harness/agent-harness.js +3 -0
  171. package/ecagent/dist/harness/compaction/branch-summarization.js +30 -30
  172. package/ecagent/dist/harness/compaction/compaction.js +83 -83
  173. package/ecagent/dist/harness/messages.js +7 -7
  174. package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
  175. package/kits/docs/GUIDE.md +20 -20
  176. package/kits/docs/INDEX.md +60 -66
  177. package/kits/docs/aun/CHEATSHEET.md +19 -19
  178. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -15
  179. package/kits/docs/channels/aun.md +69 -56
  180. package/kits/docs/channels/feishu.md +56 -56
  181. package/kits/docs/context-assembly.md +379 -367
  182. package/kits/docs/eck_templates/GUIDE.template.md +22 -22
  183. package/kits/docs/eck_templates/INDEX.template.md +28 -28
  184. package/kits/docs/eck_templates/path-registry.template.md +33 -33
  185. package/kits/docs/eck_templates/runtime.template.md +19 -19
  186. package/kits/docs/evolcore/INDEX.md +76 -71
  187. package/kits/docs/evolcore/agent-create.md +202 -165
  188. package/kits/docs/evolcore/agent.md +99 -67
  189. package/kits/docs/evolcore/aid.md +68 -60
  190. package/kits/docs/evolcore/codex-windows-sandbox.md +146 -146
  191. package/kits/docs/evolcore/config.md +241 -195
  192. package/kits/docs/evolcore/contact.md +75 -68
  193. package/kits/docs/evolcore/ctl.md +46 -46
  194. package/kits/docs/evolcore/event.md +216 -216
  195. package/kits/docs/evolcore/fs-architecture.md +1215 -1215
  196. package/kits/docs/evolcore/fs.md +110 -110
  197. package/kits/docs/evolcore/group-collaboration.md +244 -244
  198. package/kits/docs/evolcore/group-fs.md +17 -17
  199. package/kits/docs/evolcore/group-rules.md +251 -251
  200. package/kits/docs/evolcore/group.md +267 -152
  201. package/kits/docs/evolcore/menu-client.md +58 -34
  202. package/kits/docs/evolcore/model.md +50 -50
  203. package/kits/docs/evolcore/msg.md +169 -165
  204. package/kits/docs/evolcore/response.md +75 -75
  205. package/kits/docs/evolcore/rpc.md +37 -37
  206. package/kits/docs/evolcore/self-summary.md +29 -29
  207. package/kits/docs/evolcore/stats.md +83 -83
  208. package/kits/docs/evolcore/storage.md +53 -50
  209. package/kits/docs/evolcore/task.md +14 -0
  210. package/kits/docs/evolcore/trigger.md +623 -575
  211. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -26
  212. package/kits/docs/identity/PATH_OPS.md +16 -16
  213. package/kits/docs/identity/ROLE_DETAIL.md +25 -25
  214. package/kits/docs/identity/identity-tools.md +26 -26
  215. package/kits/docs/path-registry.md +62 -62
  216. package/kits/docs/prompt-loading-architecture.md +269 -269
  217. package/kits/docs/venues/aun-group.md +45 -45
  218. package/kits/docs/venues/aun-private.md +10 -10
  219. package/kits/docs/venues/client-desktop.md +10 -10
  220. package/kits/docs/venues/client-mobile.md +10 -10
  221. package/kits/docs/venues/feishu-group.md +13 -13
  222. package/kits/docs/venues/feishu-private.md +9 -9
  223. package/kits/docs/venues/group.md +42 -25
  224. package/kits/docs/venues/private.md +10 -10
  225. package/kits/eck_manifest.auxiliary.json +43 -43
  226. package/kits/eck_manifest.json +217 -190
  227. package/kits/eck_message_manifest.json +74 -74
  228. package/kits/migrations/README-role-config-v4.md +32 -32
  229. package/kits/migrations/migrate-contact-book-v2.mjs +748 -748
  230. package/kits/migrations/migrate-role-config-v4.mjs +614 -614
  231. package/kits/migrations/migrate-role-config-v5.mjs +365 -365
  232. package/kits/migrations/rename-config-file.mjs +99 -99
  233. package/kits/rules/01-overview.md +166 -162
  234. package/kits/rules/02-navigation.md +96 -96
  235. package/kits/rules/03-identity.md +36 -36
  236. package/kits/rules/04-relation.md +76 -75
  237. package/kits/rules/05-venue.md +60 -60
  238. package/kits/rules/06-channel.md +65 -63
  239. package/kits/schemas/_meta.json +53 -53
  240. package/kits/schemas/agent-config.schema.1.json +177 -177
  241. package/kits/schemas/agent-config.schema.10.json +395 -395
  242. package/kits/schemas/agent-config.schema.11.json +421 -421
  243. package/kits/schemas/agent-config.schema.12.json +451 -427
  244. package/kits/schemas/agent-config.schema.2.json +239 -239
  245. package/kits/schemas/agent-config.schema.3.json +119 -119
  246. package/kits/schemas/agent-config.schema.4.json +208 -208
  247. package/kits/schemas/agent-config.schema.5.json +326 -326
  248. package/kits/schemas/agent-config.schema.6.json +322 -322
  249. package/kits/schemas/agent-config.schema.7.json +303 -303
  250. package/kits/schemas/agent-config.schema.8.json +304 -304
  251. package/kits/schemas/agent-config.schema.9.json +364 -364
  252. package/kits/schemas/contact-book.schema.1.json +36 -36
  253. package/kits/schemas/contact-book.schema.2.json +43 -43
  254. package/kits/schemas/contact-book.schema.3.json +67 -67
  255. package/kits/schemas/daemon.schema.1.json +91 -91
  256. package/kits/schemas/daemon.schema.2.json +101 -101
  257. package/kits/schemas/daemon.schema.3.json +123 -123
  258. package/kits/schemas/daemon.schema.4.json +136 -136
  259. package/kits/schemas/daemon.schema.5.json +135 -135
  260. package/kits/schemas/daemon.schema.6.json +131 -131
  261. package/kits/schemas/defaults.schema.1.json +81 -81
  262. package/kits/schemas/defaults.schema.2.json +85 -85
  263. package/kits/schemas/defaults.schema.3.json +73 -73
  264. package/kits/schemas/defaults.schema.4.json +107 -107
  265. package/kits/schemas/defaults.schema.5.json +119 -119
  266. package/kits/schemas/menu-exec-schema-commands.md +208 -208
  267. package/kits/schemas/migrations/README.md +30 -30
  268. package/kits/schemas/relation-config.schema.1.json +158 -158
  269. package/kits/schemas/relation-config.schema.2.json +73 -73
  270. package/kits/schemas/relation-config.schema.3.json +50 -50
  271. package/kits/schemas/relation-config.schema.4.json +47 -47
  272. package/kits/schemas/relation-config.schema.5.json +47 -47
  273. package/kits/schemas/relation-config.schema.6.json +46 -46
  274. package/kits/schemas/relation-config.schema.7.json +59 -59
  275. package/kits/schemas/relation-config.schema.8.json +103 -103
  276. package/kits/schemas/role-config.schema.1.json +215 -201
  277. package/kits/schemas/role-registry.schema.1.json +35 -35
  278. package/kits/schemas/single-session.schema.1.json +31 -31
  279. package/kits/schemas/single-session.schema.2.json +57 -57
  280. package/kits/schemas/single-session.schema.3.json +32 -32
  281. package/kits/templates/bootstrap-welcome.md +17 -17
  282. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -9
  283. package/kits/templates/message-fragments/handoff-request-to-target.md +19 -19
  284. package/kits/templates/message-fragments/handoff-response-to-origin.md +9 -9
  285. package/kits/templates/message-fragments/inject-default.md +2 -2
  286. package/kits/templates/message-fragments/item.md +2 -2
  287. package/kits/templates/roles/admin.json +92 -92
  288. package/kits/templates/roles/member.json +101 -96
  289. package/kits/templates/roles/owner.json +9 -9
  290. package/kits/templates/roles/visitor.json +90 -85
  291. package/kits/templates/system-fragments/baseagent.md +14 -14
  292. package/kits/templates/system-fragments/bootstrap.md +53 -34
  293. package/kits/templates/system-fragments/channel.md +28 -64
  294. package/kits/templates/system-fragments/commands.md +36 -28
  295. package/kits/templates/system-fragments/group-unread-history.md +7 -0
  296. package/kits/templates/system-fragments/identity.md +13 -13
  297. package/kits/templates/system-fragments/relation.md +19 -19
  298. package/kits/templates/system-fragments/reply.md +56 -0
  299. package/kits/templates/system-fragments/session.md +59 -59
  300. package/kits/templates/system-fragments/venue.md +31 -31
  301. package/package.json +104 -98
  302. package/dist/aun/group-identity.js +0 -9
  303. /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
  304. /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
@@ -1,4 +1,6 @@
1
- import { getRunnerCatalog, catalogEfforts, assertCatalogSelectable } from '../core/model/model-catalog.js';
1
+ import { getRunnerCatalog, catalogEfforts, assertCatalogSelectable, TRANSPORT_EFFORTS, resolveCatalogModel, } from '../core/model/model-catalog.js';
2
+ // Ecagent/OpenAI API 不认识 Claude Code SDK 的窗口后缀,必须传入基础模型 ID。
3
+ import { stripContextWindowSuffix } from '../core/model/context-window-suffix.js';
2
4
  import fs from 'fs';
3
5
  import path from 'path';
4
6
  import { AgentHarness, JsonlSessionRepo, NodeExecutionEnv, } from '../../ecagent/dist/node.js';
@@ -10,29 +12,23 @@ import { requestDangerousCommandPermission } from '../core/permission/approval-g
10
12
  import { normalizeExecutionPermissionMode, normalizePermissionMode } from '../core/permission/mode.js';
11
13
  import { checkDangerousCommand, checkReadonly, evaluateToolPreflight } from '../core/permission/tool-policy.js';
12
14
  import { logger } from '../utils/logger.js';
15
+ import { ErrorType } from '../utils/error-utils.js';
13
16
  import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
14
17
  import { auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
15
18
  import { resolveEcagentConfig } from './baseagent.js';
16
19
  import { buildModelRequestHeaders } from './request-identity.js';
17
20
  import { appendRequestPath } from './request-identity.js';
18
21
  import { getManagedTaskTempDir } from '../cli/task-context.js';
19
- import { hasTrustedFullAccessContext } from './runner-types.js';
22
+ import { unsettledInterruptError, withInterruptDeadline, hasTrustedFullAccessContext } from './runner-types.js';
20
23
  const PROVIDER_ID = 'evolcore-gateway';
21
- const DEFAULT_SYSTEM_PROMPT = `You are ecagent, the coding agent built into EvolCore.
22
- Work directly in the current project. Inspect relevant files before changing them, keep edits scoped, and verify the result.
24
+ const DEFAULT_SYSTEM_PROMPT = `You are ecagent, the coding agent built into EvolCore.
25
+ Work directly in the current project. Inspect relevant files before changing them, keep edits scoped, and verify the result.
23
26
  Use the provided tools for filesystem and shell work. Never claim a tool action succeeded unless its result confirms success.`;
24
27
  const MAX_READ_BYTES = 1024 * 1024;
25
28
  const MAX_TOOL_OUTPUT = 200_000;
26
29
  const MAX_SCAN_FILES = 5_000;
27
30
  const MAX_SEARCH_RESULTS = 500;
28
31
  const DEFAULT_BASH_TIMEOUT_MS = 120_000;
29
- /**
30
- * 规范化模型 ID:去掉 Claude Code SDK 专有的上下文窗口后缀 [1m]/[200k]
31
- * Ecagent/OpenAI API 不认识这些后缀,必须传入标准模型 ID
32
- */
33
- function stripContextWindowSuffix(modelId) {
34
- return modelId.replace(/\[(?:1m|200k)\]$/i, '');
35
- }
36
32
  class AsyncEventQueue {
37
33
  values = [];
38
34
  waiters = [];
@@ -684,6 +680,8 @@ function extractToolResult(result) {
684
680
  }
685
681
  function errorType(message) {
686
682
  const normalized = message.toLowerCase();
683
+ if (normalized.includes('invalid jsonl session file') || normalized.includes('session file'))
684
+ return ErrorType.SESSION_UNAVAILABLE;
687
685
  if (normalized.includes('[auth]') || normalized.includes('401') || normalized.includes('403'))
688
686
  return 'auth';
689
687
  if (normalized.includes('context') && (normalized.includes('long') || normalized.includes('length')))
@@ -695,6 +693,7 @@ function errorType(message) {
695
693
  export class EcagentRunner {
696
694
  name = 'ecagent';
697
695
  capabilities = {
696
+ taskRecovery: true,
698
697
  fullaccess: true,
699
698
  clear: true, compact: true, fork: true, askUserQuestion: false, planApproval: false, fileRewind: 'unsupported',
700
699
  };
@@ -705,6 +704,9 @@ export class EcagentRunner {
705
704
  effort;
706
705
  permissionMode = DEFAULT_PERMISSION_MODE;
707
706
  runtimes = new Map();
707
+ /** Serialize harness lifecycles that share one durable ecagent session file. */
708
+ sessionRunTails = new Map();
709
+ sessionRuns = new Map();
708
710
  activeStreams = new Map();
709
711
  /** Queue identity prevents an old run's finally from consuming a new run's interrupt. */
710
712
  pendingInterrupts = new Map();
@@ -760,7 +762,15 @@ export class EcagentRunner {
760
762
  return catalog.models.map(m => m.id);
761
763
  }
762
764
  async getAvailableEfforts(model) {
763
- return catalogEfforts(await this.getModelCatalog(), model, 'ecagent');
765
+ const catalog = await this.getModelCatalog();
766
+ const efforts = catalogEfforts(catalog, model, 'ecagent');
767
+ if (efforts.length > 0)
768
+ return efforts;
769
+ // The embedded transport advertises reasoning support for every model it
770
+ // constructs. Older gateways may omit reasoningLevels from /models; keep
771
+ // menu.options aligned with the actual runner capability in that case.
772
+ const entry = catalog.models.find(item => item.id === resolveCatalogModel(catalog, model));
773
+ return entry && entry.reasoningLevels === undefined ? [...TRANSPORT_EFFORTS.ecagent] : efforts;
764
774
  }
765
775
  registerStream(key, stream) { this.activeStreams.set(key, stream); }
766
776
  cleanupStream(key) {
@@ -804,6 +814,7 @@ export class EcagentRunner {
804
814
  // Keep managed tasks from silently inheriting the daemon-wide TMPDIR when
805
815
  // their session-owned runtime directory is unavailable.
806
816
  getManagedTaskTempDir(runtimeEnv);
817
+ this.assertInterruptSettled(sessionId);
807
818
  const topicBinding = modelOverride?.backend?.kind === 'topic';
808
819
  if (topicBinding && (!modelOverride?.turn || modelOverride.turn.sessionId !== sessionId || !this.onSessionIdUpdate)) {
809
820
  const error = new Error('topic backend run requires an active TurnLease and binding callback');
@@ -811,20 +822,43 @@ export class EcagentRunner {
811
822
  throw error;
812
823
  }
813
824
  const queue = new AsyncEventQueue();
825
+ const lifecycle = { queue, cancelled: false };
826
+ const runs = this.sessionRuns.get(sessionId) ?? new Set();
827
+ runs.add(lifecycle);
828
+ this.sessionRuns.set(sessionId, runs);
814
829
  this.activeStreams.set(sessionId, queue);
815
- void this.executeRun(queue, {
830
+ const request = {
816
831
  sessionId, prompt, projectPath: path.resolve(projectPath), initialAgentSessionId, images,
817
832
  systemPromptAppend, modelOverride, runtimeEnv,
833
+ };
834
+ const previous = this.sessionRunTails.get(sessionId) ?? Promise.resolve();
835
+ const run = previous.catch(() => undefined).then(() => {
836
+ if (!lifecycle.cancelled)
837
+ return this.executeRun(queue, request, lifecycle);
818
838
  });
839
+ lifecycle.done = run;
840
+ this.sessionRunTails.set(sessionId, run);
841
+ const release = () => {
842
+ runs.delete(lifecycle);
843
+ if (this.sessionRuns.get(sessionId) === runs && runs.size === 0)
844
+ this.sessionRuns.delete(sessionId);
845
+ if (this.sessionRunTails.get(sessionId) === run)
846
+ this.sessionRunTails.delete(sessionId);
847
+ };
848
+ void run.then(release, release);
819
849
  return queue;
820
850
  }
821
- async executeRun(queue, request) {
822
- const stats = { startedAt: Date.now(), text: '', numTurns: 0, usage: {}, modelCalls: [] };
851
+ async executeRun(queue, request, lifecycle) {
852
+ const stats = {
853
+ startedAt: Date.now(), text: '', textBlocks: new Map(), numTurns: 0, usage: {}, modelCalls: [],
854
+ };
823
855
  let completeSent = false;
824
856
  try {
825
857
  await fs.promises.mkdir(request.projectPath, { recursive: true });
826
858
  const previousRuntime = this.runtimes.get(request.sessionId);
827
859
  const runtime = await this.getOrCreateRuntime(request.sessionId, request.projectPath, request.initialAgentSessionId, request.modelOverride);
860
+ if (lifecycle.cancelled)
861
+ throw new Error('ecagent run interrupted');
828
862
  if (previousRuntime !== runtime) {
829
863
  queue.push({ type: 'session_id', sessionId: runtime.agentSessionId });
830
864
  }
@@ -875,13 +909,16 @@ export class EcagentRunner {
875
909
  throw new Error('ecagent run interrupted');
876
910
  }
877
911
  harness.on('tool_call', async (event, signal) => {
912
+ if (lifecycle.cancelled || signal?.aborted)
913
+ return { block: true, reason: '工具调用已取消' };
878
914
  const pauseController = this.permissionContexts.get(request.sessionId)?.pauseController;
879
915
  if (pauseController) {
880
916
  const pauseResult = await pauseController.waitAtToolBoundary(signal);
881
917
  if (pauseResult === 'cancelled')
882
918
  return { block: true, reason: '工具调用已取消' };
883
919
  }
884
- return this.authorizeTool(request.sessionId, request.projectPath, permissionMode, event.toolName, event.input);
920
+ const decision = await this.authorizeTool(request.sessionId, request.projectPath, permissionMode, event.toolName, event.input);
921
+ return lifecycle.cancelled || signal?.aborted ? { block: true, reason: '工具调用已取消' } : decision;
885
922
  });
886
923
  const unsubscribe = harness.subscribe(event => this.mapHarnessEvent(queue, event, stats, model, effort));
887
924
  queue.push({ type: 'state_changed', state: 'running' });
@@ -895,20 +932,36 @@ export class EcagentRunner {
895
932
  }
896
933
  finally {
897
934
  unsubscribe();
898
- runtime.harness = undefined;
935
+ this.finishTextBlocks(queue, stats);
936
+ if (runtime.harness === harness)
937
+ runtime.harness = undefined;
899
938
  }
900
- if (result.stopReason === 'error' || result.stopReason === 'aborted') {
901
- const message = result.errorMessage || (result.stopReason === 'aborted' ? 'ecagent run interrupted' : 'ecagent run failed');
902
- queue.push({ type: 'error', error: message, errorType: errorType(message) });
939
+ const outputLimit = result.stopReason === 'length';
940
+ if (result.stopReason === 'error' || result.stopReason === 'aborted' || outputLimit) {
941
+ const message = result.errorMessage || (result.stopReason === 'aborted'
942
+ ? 'ecagent run interrupted'
943
+ : outputLimit
944
+ ? 'ecagent response reached the output limit (max_output_tokens)'
945
+ : 'ecagent run failed');
946
+ queue.push({ type: 'error', error: message, errorType: outputLimit ? ErrorType.MAX_OUTPUT_TOKENS : errorType(message) });
903
947
  }
904
948
  const title = await runtime.session.getSessionName();
905
949
  queue.push({ type: 'state_changed', state: 'idle' });
906
950
  queue.push({
907
951
  type: 'complete',
908
952
  result: stats.text,
909
- isError: result.stopReason === 'error' || result.stopReason === 'aborted',
910
- errors: result.errorMessage ? [result.errorMessage] : undefined,
911
- terminalReason: result.stopReason === 'aborted' ? 'interrupted' : result.stopReason,
953
+ ...(outputLimit ? { subtype: 'max_output_tokens' } : {}),
954
+ isError: result.stopReason === 'error' || result.stopReason === 'aborted' || outputLimit,
955
+ errors: result.errorMessage
956
+ ? [result.errorMessage]
957
+ : outputLimit
958
+ ? ['max_output_tokens']
959
+ : undefined,
960
+ terminalReason: result.stopReason === 'aborted'
961
+ ? 'interrupted'
962
+ : outputLimit
963
+ ? 'max_output_tokens'
964
+ : result.stopReason,
912
965
  durationMs: Date.now() - stats.startedAt,
913
966
  ttftMs: stats.firstTextAt ? stats.firstTextAt - stats.startedAt : undefined,
914
967
  sessionTitle: title,
@@ -921,6 +974,7 @@ export class EcagentRunner {
921
974
  completeSent = true;
922
975
  }
923
976
  catch (error) {
977
+ this.finishTextBlocks(queue, stats);
924
978
  const message = error instanceof Error ? error.message : String(error);
925
979
  const interrupted = message === 'ecagent run interrupted';
926
980
  logger.error(`[EcagentRunner] run failed for session ${request.sessionId}:`, error);
@@ -953,7 +1007,11 @@ export class EcagentRunner {
953
1007
  const targetId = topicBinding
954
1008
  ? (requestedTopicAgentSessionId || undefined)
955
1009
  : requestedId || current?.agentSessionId;
956
- if (current
1010
+ const recoverySessionId = modelOverride?.recovery?.agentSessionId;
1011
+ if (modelOverride?.recovery && (!recoverySessionId || targetId !== recoverySessionId)) {
1012
+ throw new Error('Task recovery requires the original ecagent session binding');
1013
+ }
1014
+ if (current && !recoverySessionId
957
1015
  && current.projectPath === projectPath
958
1016
  && (!targetId || current.agentSessionId === targetId)
959
1017
  && (!topicBinding || (!!targetId && current.agentSessionId === targetId)))
@@ -964,6 +1022,9 @@ export class EcagentRunner {
964
1022
  if (targetId) {
965
1023
  const metadata = (await repo.list({ cwd: projectPath })).find(item => item.id === targetId);
966
1024
  if (!metadata) {
1025
+ if (recoverySessionId) {
1026
+ throw new Error(`Task recovery backend is unavailable: ${recoverySessionId}`);
1027
+ }
967
1028
  if (topicBinding) {
968
1029
  const error = new Error(`bound topic backend is unavailable: ${targetId}`);
969
1030
  error.code = 'TOPIC_BACKEND_UNAVAILABLE';
@@ -984,6 +1045,9 @@ export class EcagentRunner {
984
1045
  : await repo.create({ cwd: projectPath, id: sessionId });
985
1046
  }
986
1047
  const metadata = await session.getMetadata();
1048
+ if (recoverySessionId && metadata.id !== recoverySessionId) {
1049
+ throw new Error('Task recovery cannot replace the original ecagent session');
1050
+ }
987
1051
  const runtime = { agentSessionId: metadata.id, projectPath, repo, session };
988
1052
  if ((topicBinding && metadata.id !== requestedTopicAgentSessionId) || metadata.id !== targetId) {
989
1053
  const turn = modelOverride?.turn;
@@ -1002,6 +1066,29 @@ export class EcagentRunner {
1002
1066
  this.runtimes.set(sessionId, runtime);
1003
1067
  return runtime;
1004
1068
  }
1069
+ endTextBlock(queue, stats, contentIndex, content) {
1070
+ if (stats.textBlocks.get(contentIndex)?.ended)
1071
+ return;
1072
+ // Commit synchronously before enqueueing. message_end/finally may close the
1073
+ // same block again, but cannot append or project its text a second time.
1074
+ stats.textBlocks.set(contentIndex, { text: content, ended: true });
1075
+ stats.text += content;
1076
+ // Match Codex: only the completed body enters the common runner protocol.
1077
+ queue.push({ type: 'text', text: content, turn: stats.numTurns + 1 });
1078
+ }
1079
+ /** Recover a missing text_end from the final message, or the received deltas. */
1080
+ finishTextBlocks(queue, stats, message) {
1081
+ if (message) {
1082
+ message.content.forEach((block, index) => {
1083
+ if (block.type === 'text')
1084
+ this.endTextBlock(queue, stats, index, block.text);
1085
+ });
1086
+ }
1087
+ for (const [index, block] of stats.textBlocks) {
1088
+ if (!block.ended)
1089
+ this.endTextBlock(queue, stats, index, block.text);
1090
+ }
1091
+ }
1005
1092
  mapHarnessEvent(queue, event, stats, model, effort) {
1006
1093
  if (event.type === 'message_update'
1007
1094
  && (event.assistantMessageEvent.type === 'thinking_start'
@@ -1010,14 +1097,38 @@ export class EcagentRunner {
1010
1097
  queue.push({ type: 'heartbeat', source: 'reasoning', turn: stats.numTurns + 1 });
1011
1098
  return;
1012
1099
  }
1013
- if (event.type === 'message_update' && event.assistantMessageEvent.type === 'text_delta') {
1014
- if (!stats.firstTextAt)
1015
- stats.firstTextAt = Date.now();
1016
- stats.text += event.assistantMessageEvent.delta;
1017
- queue.push({ type: 'text', text: event.assistantMessageEvent.delta, turn: stats.numTurns + 1 });
1100
+ if (event.type === 'message_update') {
1101
+ const update = event.assistantMessageEvent;
1102
+ if (update.type === 'text_start') {
1103
+ if (!stats.textBlocks.has(update.contentIndex)) {
1104
+ stats.textBlocks.set(update.contentIndex, { text: '', ended: false });
1105
+ queue.push({ type: 'text_start', contentIndex: update.contentIndex, turn: stats.numTurns + 1 });
1106
+ }
1107
+ return;
1108
+ }
1109
+ if (update.type === 'text_delta') {
1110
+ const block = stats.textBlocks.get(update.contentIndex) ?? { text: '', ended: false };
1111
+ if (block.ended)
1112
+ return;
1113
+ if (!stats.firstTextAt)
1114
+ stats.firstTextAt = Date.now();
1115
+ block.text += update.delta;
1116
+ stats.textBlocks.set(update.contentIndex, block);
1117
+ // Keep the stream alive without exposing partial text to the renderer.
1118
+ queue.push({ type: 'heartbeat', source: 'text', turn: stats.numTurns + 1 });
1119
+ return;
1120
+ }
1121
+ if (update.type === 'text_end') {
1122
+ this.endTextBlock(queue, stats, update.contentIndex, update.content);
1123
+ return;
1124
+ }
1125
+ }
1126
+ if (event.type === 'message_end' && event.message.role === 'assistant') {
1127
+ this.finishTextBlocks(queue, stats, event.message);
1018
1128
  return;
1019
1129
  }
1020
1130
  if (event.type === 'tool_execution_start') {
1131
+ this.finishTextBlocks(queue, stats);
1021
1132
  queue.push({ type: 'tool_use', name: event.toolName, input: event.args, callId: event.toolCallId, turn: stats.numTurns + 1 });
1022
1133
  return;
1023
1134
  }
@@ -1030,6 +1141,8 @@ export class EcagentRunner {
1030
1141
  return;
1031
1142
  }
1032
1143
  if (event.type === 'turn_end' && event.message.role === 'assistant') {
1144
+ this.finishTextBlocks(queue, stats, event.message);
1145
+ stats.textBlocks.clear();
1033
1146
  stats.numTurns++;
1034
1147
  const usage = event.message.usage;
1035
1148
  stats.usage.input_tokens = Number(stats.usage.input_tokens ?? 0) + usage.input;
@@ -1155,13 +1268,36 @@ export class EcagentRunner {
1155
1268
  this.permissionContexts.get(sessionKey)?.pauseController?.cancel();
1156
1269
  this.permissionGateway?.cancelAll(sessionKey, 'interrupted');
1157
1270
  const harness = this.runtimes.get(sessionKey)?.harness;
1158
- if (harness) {
1159
- await harness.abort();
1271
+ const runs = [...(this.sessionRuns.get(sessionKey) ?? [])];
1272
+ for (const run of runs) {
1273
+ if (run.cancelled)
1274
+ continue;
1275
+ run.cancelled = true;
1276
+ run.queue.push({ type: 'complete', isError: true, terminalReason: 'interrupted' });
1277
+ run.queue.end();
1278
+ }
1279
+ if (runs.length === 0 && !harness) {
1280
+ const stream = this.activeStreams.get(sessionKey);
1281
+ if (stream)
1282
+ this.pendingInterrupts.set(sessionKey, stream);
1160
1283
  return;
1161
1284
  }
1162
- const stream = this.activeStreams.get(sessionKey);
1163
- if (stream)
1164
- this.pendingInterrupts.set(sessionKey, stream);
1285
+ // Ending the event stream releases the message queue, not the durable
1286
+ // session writer. Keep the run tail until all old code has actually exited.
1287
+ try {
1288
+ await withInterruptDeadline(Promise.all([
1289
+ harness?.abort(),
1290
+ ...runs.map(run => run.done),
1291
+ ]), 'ecagent');
1292
+ }
1293
+ catch (error) {
1294
+ logger.warn(`[EcagentRunner] Interrupted execution still requires quiescence: session=${sessionKey} error=${String(error)}`);
1295
+ }
1296
+ }
1297
+ assertInterruptSettled(sessionId) {
1298
+ if ([...(this.sessionRuns.get(sessionId) ?? [])].some(run => run.cancelled)) {
1299
+ throw unsettledInterruptError('ecagent');
1300
+ }
1165
1301
  }
1166
1302
  injectUserMessage(sessionId, text) {
1167
1303
  const harness = this.runtimes.get(sessionId)?.harness;
@@ -1174,6 +1310,7 @@ export class EcagentRunner {
1174
1310
  const runtime = this.runtimes.get(sessionId);
1175
1311
  const stream = this.activeStreams.get(sessionId);
1176
1312
  await this.interrupt(sessionId).catch(() => { });
1313
+ this.assertInterruptSettled(sessionId);
1177
1314
  if (runtime && this.runtimes.get(sessionId) === runtime)
1178
1315
  this.runtimes.delete(sessionId);
1179
1316
  if (stream && this.activeStreams.get(sessionId) === stream) {
@@ -1188,6 +1325,7 @@ export class EcagentRunner {
1188
1325
  try {
1189
1326
  const currentRuntime = this.runtimes.get(sessionId);
1190
1327
  await this.interrupt(sessionId);
1328
+ this.assertInterruptSettled(sessionId);
1191
1329
  const env = new NodeExecutionEnv({ cwd: projectPath });
1192
1330
  const repo = new JsonlSessionRepo({ fs: env, sessionsRoot: this.sessionsRoot() });
1193
1331
  const metadata = (await repo.list({ cwd: path.resolve(projectPath) })).find(item => item.id === agentSessionId);
@@ -1333,6 +1471,8 @@ export class EcagentRunner {
1333
1471
  async dispose() {
1334
1472
  await Promise.allSettled([...this.runtimes.keys()].map(sessionId => this.closeSession(sessionId)));
1335
1473
  this.runtimes.clear();
1474
+ this.sessionRuns.clear();
1475
+ this.sessionRunTails.clear();
1336
1476
  this.activeStreams.clear();
1337
1477
  this.pendingInterrupts.clear();
1338
1478
  }
@@ -1,4 +1,6 @@
1
1
  import { getCatalog, assertCatalogSelectable } from '../core/model/model-catalog.js';
2
+ // Gemini API 不认识 Claude Code SDK 的窗口后缀,必须传入基础模型 ID。
3
+ import { stripContextWindowSuffix } from '../core/model/context-window-suffix.js';
2
4
  /**
3
5
  * Gemini Agent Runner
4
6
  *
@@ -10,6 +12,7 @@ import { getCatalog, assertCatalogSelectable } from '../core/model/model-catalog
10
12
  * GeminiRunner → spawn `gemini -p ...` → stdout JSONL stream
11
13
  */
12
14
  import { spawn } from 'child_process';
15
+ import { MAIN_CLI_BIN } from '../product.js';
13
16
  import { createInterface } from 'readline';
14
17
  import fs from 'fs';
15
18
  import path from 'path';
@@ -33,13 +36,6 @@ const ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?(?:\x1b\\|\x07)/g;
33
36
  function stripAnsi(text) {
34
37
  return text.replace(ANSI_RE, '');
35
38
  }
36
- /**
37
- * 规范化模型 ID:去掉 Claude Code SDK 专有的上下文窗口后缀 [1m]/[200k]
38
- * Gemini API 不认识这些后缀,必须传入标准模型 ID
39
- */
40
- function stripContextWindowSuffix(modelId) {
41
- return modelId.replace(/\[(?:1m|200k)\]$/i, '');
42
- }
43
39
  // ── MIME → 扩展名映射 ──
44
40
  const MIME_EXT = {
45
41
  'image/png': '.png',
@@ -148,6 +144,10 @@ const GEMINI_EC_COMMAND_PATTERNS = [
148
144
  String.raw `\x00"command":"[ ]*ec[ ]+(?:[^"\x00;&|$()<>\x60\r\n\\]|\\(?:\\|"))*"\x00`,
149
145
  ];
150
146
  const GEMINI_BOOTSTRAP_PERSONA_COMMAND_PATTERN = String.raw `^\{\x00"command":"[ ]*ec[ ]+agent[ ]+persona(?:[ ]+show|[ ]+--(?:text|file)[ ]+(?:[^"\x00;&|$()<>\x60\r\n\\]|\\(?:\\|"))+)[ ]*"\x00\}$`;
147
+ // A single-quoted shell argument can safely carry multiline Markdown, backticks,
148
+ // dollar signs and semicolons. Outside that argument only --format json is legal.
149
+ const GEMINI_WELCOME_ESCAPED_QUOTE = escapeGeminiArgsRegex(JSON.stringify("'\\''").slice(1, -1));
150
+ const GEMINI_BOOTSTRAP_WELCOME_COMMAND_PATTERN = String.raw `^\{\x00"command":"[ ]*${escapeGeminiArgsRegex(MAIN_CLI_BIN)}[ ]+agent[ ]+welcome[ ]+(?:show|--text[ ]+'(?:[^'"\x00\\]|\\(?:\\|"|n|r|t)|${GEMINI_WELCOME_ESCAPED_QUOTE})*')(?:[ ]+--format[ ]+json)?[ ]*"\x00\}$`;
151
151
  export function resolveGeminiPermissionProfile(value, bootstrap = false) {
152
152
  if (value === 'fullaccess')
153
153
  return { mode: 'fullaccess', approvalMode: 'yolo' };
@@ -217,6 +217,7 @@ export function buildGeminiAdminPolicy(profile, bootstrapAgentMdPath, personalMd
217
217
  appendGeminiPolicyRule(lines, 'run_shell_command', 'allow', 990, geminiExactShellCommandPattern(`ec aid agentmd put ${bootstrap.aid}`));
218
218
  appendGeminiPolicyRule(lines, 'run_shell_command', 'allow', 990, geminiExactShellCommandPattern(`ec agent ready ${bootstrap.aid}`));
219
219
  appendGeminiPolicyRule(lines, 'run_shell_command', 'allow', 990, GEMINI_BOOTSTRAP_PERSONA_COMMAND_PATTERN);
220
+ appendGeminiPolicyRule(lines, 'run_shell_command', 'allow', 990, GEMINI_BOOTSTRAP_WELCOME_COMMAND_PATTERN);
220
221
  if (personalMdPath) {
221
222
  const personalPathPattern = geminiManagedIdentityPathPattern(personalMdPath);
222
223
  appendGeminiPolicyRule(lines, 'read_file', 'allow', 989, personalPathPattern);
@@ -1,3 +1,4 @@
1
+ import { parseModelId, stripContextWindowSuffix } from '../core/model/context-window-suffix.js';
1
2
  export const BASEAGENT_RUNNER_UNAVAILABLE = 'BASEAGENT_RUNNER_UNAVAILABLE';
2
3
  export class BaseagentRunnerUnavailableError extends Error {
3
4
  evolagentName;
@@ -12,6 +13,30 @@ export class BaseagentRunnerUnavailableError extends Error {
12
13
  this.name = 'BaseagentRunnerUnavailableError';
13
14
  }
14
15
  }
16
+ /** Codex's native asynchronous reply envelope used by its rich clients. */
17
+ export function formatAsyncQuestionAnswer(answer) {
18
+ return '<send_user_message_question_reply>' + JSON.stringify({
19
+ questionItemId: answer.questionItemId,
20
+ question: answer.question,
21
+ answer: answer.answer,
22
+ }) + '</send_user_message_question_reply>';
23
+ }
24
+ export function parseAsyncUserQuestions(value) {
25
+ if (!Array.isArray(value) || value.length === 0 || value.length > 20) {
26
+ throw new Error('Invalid asynchronous question list');
27
+ }
28
+ return value.map(question => {
29
+ if (!question || typeof question !== 'object' || typeof question.title !== 'string'
30
+ || !question.title.trim() || question.title.length > 16_384) {
31
+ throw new Error('Invalid asynchronous question title');
32
+ }
33
+ if (question.options != null && (!Array.isArray(question.options) || !question.options.length
34
+ || question.options.length > 20 || question.options.some((option) => typeof option !== 'string' || !option.trim() || option.length > 4_096))) {
35
+ throw new Error('Invalid asynchronous question options');
36
+ }
37
+ return { title: question.title, ...(question.options ? { options: [...question.options] } : {}) };
38
+ });
39
+ }
15
40
  export function hasTrustedFullAccessContext(context) {
16
41
  const authorization = context?.executionPermission;
17
42
  if (!authorization
@@ -19,8 +44,10 @@ export function hasTrustedFullAccessContext(context) {
19
44
  || authorization.processRole !== 'fullaccess-run'
20
45
  || authorization.dataScope !== 'daemon')
21
46
  return false;
22
- if (authorization.source === 'fullaccess-command') {
23
- return typeof authorization.authorizedBy === 'string' && authorization.authorizedBy.length > 0;
47
+ if (authorization.source === 'fullaccess-command' || authorization.source === 'agent-policy') {
48
+ return typeof authorization.authorizedBy === 'string' && authorization.authorizedBy.length > 0
49
+ && (authorization.source !== 'agent-policy'
50
+ || (typeof authorization.agentAid === 'string' && authorization.agentAid.length > 0));
24
51
  }
25
52
  return authorization.source === 'trigger'
26
53
  && typeof authorization.authorizedBy === 'string' && authorization.authorizedBy.length > 0
@@ -31,6 +58,28 @@ export function hasTrustedFullAccessContext(context) {
31
58
  // being mistaken for a runnable privileged invocation.
32
59
  && typeof authorization.attemptId === 'string' && authorization.attemptId.length > 0;
33
60
  }
61
+ /** Bounds waiting only; the caller must retain ownership until execution stops. */
62
+ export async function withInterruptDeadline(operation, label, timeoutMs = 5_000) {
63
+ let timer;
64
+ try {
65
+ return await Promise.race([
66
+ operation,
67
+ new Promise((_, reject) => {
68
+ timer = setTimeout(() => reject(new Error(`${label}: interrupt deadline exceeded`)), timeoutMs);
69
+ timer.unref?.();
70
+ }),
71
+ ]);
72
+ }
73
+ finally {
74
+ if (timer)
75
+ clearTimeout(timer);
76
+ }
77
+ }
78
+ export function unsettledInterruptError(runner) {
79
+ return Object.assign(new Error(`${runner} 的上一次执行尚未确认停止,本次任务未启动;旧执行结束后可在原会话继续。`), {
80
+ code: 'RUNNER_INTERRUPT_UNCONFIRMED',
81
+ });
82
+ }
34
83
  // ── 类型守卫 ──
35
84
  export function hasModelSwitcher(agent) {
36
85
  return typeof agent.setModel === 'function' && typeof agent.listModels === 'function';
@@ -69,20 +118,22 @@ const CLAUDE_CONTEXT_MODEL_ALIASES = ['opus', 'sonnet', 'haiku'];
69
118
  export function isClaudeContextUsageModel(model) {
70
119
  if (!model)
71
120
  return false;
72
- const baseModel = model.replace(/\[1m\]$/i, '');
121
+ const baseModel = stripContextWindowSuffix(model);
73
122
  return /^claude-/i.test(baseModel) || CLAUDE_CONTEXT_MODEL_ALIASES.includes(baseModel);
74
123
  }
75
124
  export function isOneMillionContextModel(model) {
76
125
  if (!model)
77
126
  return false;
127
+ // 显式后缀是网关给出的权威窗口,优先于本地前缀表:`claude-opus-5[200k]`
128
+ // 即使基础 ID 在 1M 名单里,也必须按 200k 处理。
129
+ const { base, contextWindow } = parseModelId(model);
130
+ if (contextWindow !== undefined)
131
+ return contextWindow >= 1_000_000;
78
132
  if (ONE_M_CONTEXT_MODEL_ALIASES.includes(model))
79
133
  return true;
80
- if (/\[1m\]$/i.test(model))
81
- return true;
82
- if (/deepseek-v4/i.test(model))
134
+ if (/deepseek-v4/i.test(base))
83
135
  return true;
84
- const baseModel = model.replace(/\[1m\]$/i, '');
85
- return ONE_M_CONTEXT_MODEL_PREFIXES.some(prefix => baseModel === prefix || baseModel.startsWith(`${prefix}-`));
136
+ return ONE_M_CONTEXT_MODEL_PREFIXES.some(prefix => base === prefix || base.startsWith(`${prefix}-`));
86
137
  }
87
138
  /**
88
139
  * EvolCore's conservative local context-window catalog: 1M models = 1,000,000;
@@ -94,17 +145,22 @@ export function realContextWindowForModel(model) {
94
145
  return isOneMillionContextModel(model) ? 1000000 : 200000;
95
146
  }
96
147
  /**
97
- * The SDK's compaction threshold must leave room for the next model turn and
98
- * its tool results. Keep a 10k reserve for 200k-class windows and retain the
99
- * historical 350k cap for large-context models.
148
+ * EvolCore auto-compact threshold: fixed at 650k tokens regardless of model
149
+ * context window. Large-window models (1M) compress at 650k; small-window
150
+ * models (200k/128k) never reach this threshold and rely on the SDK's own
151
+ * built-in compaction instead.
100
152
  */
101
- export function autoCompactWindowForContextWindow(contextWindow) {
102
- if (!contextWindow || contextWindow <= 0)
103
- return undefined;
104
- const reserve = Math.min(10_000, Math.floor(contextWindow * 0.05));
105
- return Math.min(350_000, Math.max(1, contextWindow - reserve));
153
+ export function autoCompactWindowForModel(_model) {
154
+ return 650_000;
106
155
  }
107
- /** Auto-compact threshold derived from the configured model context window. */
108
- export function autoCompactWindowForModel(model) {
109
- return autoCompactWindowForContextWindow(realContextWindowForModel(model)) ?? 350_000;
156
+ /**
157
+ * Threshold passed to the Claude Code SDK's autoCompactWindow option.
158
+ * Kept window-aware so the SDK's own built-in compaction continues to fire
159
+ * at the right point for small-context models (200k/128k), where EvolCore's
160
+ * fixed 650k threshold would never trigger.
161
+ */
162
+ export function sdkAutoCompactWindowForModel(model) {
163
+ const contextWindow = realContextWindowForModel(model);
164
+ const reserve = Math.min(10_000, Math.floor(contextWindow * 0.05));
165
+ return Math.min(650_000, Math.max(1, contextWindow - reserve));
110
166
  }