evolcore 0.0.5 → 0.0.6

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 (322) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +4 -12
  3. package/dist/agents/baseagent.js +39 -0
  4. package/dist/agents/claude-runner.js +10 -2
  5. package/dist/agents/codex-runner.js +37 -8
  6. package/dist/agents/ecagent-runner.js +1159 -0
  7. package/dist/agents/gemini-runner.js +4 -1
  8. package/dist/agents/runner-types.js +20 -4
  9. package/dist/aun/msg/history.js +32 -9
  10. package/dist/aun/outbox.js +4 -9
  11. package/dist/channels/aun.js +32 -6
  12. package/dist/channels/daemon.js +7 -4
  13. package/dist/channels/dingtalk.js +3 -5
  14. package/dist/channels/feishu.js +16 -108
  15. package/dist/channels/wechat.js +4 -4
  16. package/dist/channels/wecom.js +4 -6
  17. package/dist/cli/agent-command.js +1 -1
  18. package/dist/cli/agent.js +42 -11
  19. package/dist/cli/aun-commands.js +50 -3
  20. package/dist/cli/daemon-commands.js +81 -6
  21. package/dist/cli/data-command.js +132 -0
  22. package/dist/cli/index.js +11 -3
  23. package/dist/cli/init.js +76 -17
  24. package/dist/cli/net-check.js +9 -20
  25. package/dist/cli/restart-monitor.js +11 -2
  26. package/dist/cli/trigger-command.js +1 -1
  27. package/dist/cli/watch-msg.js +12 -25
  28. package/dist/config/builtin-roles.js +3 -1
  29. package/dist/config/config-field-policy.js +1 -1
  30. package/dist/config/gateway-config.js +21 -4
  31. package/dist/config/resolved-config-op.js +10 -0
  32. package/dist/core/command/command-handler.js +2 -2
  33. package/dist/core/command/menu-handler.js +11 -9
  34. package/dist/core/command/role-menu.js +11 -0
  35. package/dist/core/command/slash-handler.js +18 -14
  36. package/dist/core/data-migration.js +1434 -0
  37. package/dist/core/handoff/store.js +14 -4
  38. package/dist/core/inference/text-inference.js +42 -1
  39. package/dist/core/message/pending-hints.js +1 -1
  40. package/dist/core/message/response-engine.js +406 -167
  41. package/dist/core/message/retry-scheduler.js +71 -0
  42. package/dist/core/model/model-catalog.js +7 -2
  43. package/dist/core/model/model-fallback.js +81 -0
  44. package/dist/core/protected-paths.js +41 -8
  45. package/dist/core/session/adapters/ecagent-session-file-adapter.js +112 -0
  46. package/dist/core/session/session-fs-store.js +283 -59
  47. package/dist/core/session/session-manager.js +19 -21
  48. package/dist/core/session/session-title.js +1 -0
  49. package/dist/core/session/session-turn-coordinator.js +8 -1
  50. package/dist/core/system-channels.js +1 -0
  51. package/dist/index.js +10 -9
  52. package/dist/paths.js +35 -27
  53. package/dist/trigger/feedback.js +16 -0
  54. package/dist/trigger/legacy-session-history.js +19 -0
  55. package/dist/trigger/manager.js +12 -1
  56. package/dist/trigger/parser.js +4 -2
  57. package/dist/trigger/scheduler.js +185 -15
  58. package/dist/trigger/session-lock.js +62 -0
  59. package/dist/trigger/state.js +64 -0
  60. package/dist/trigger/validation.js +32 -2
  61. package/dist/utils/aid-bind.js +1 -1
  62. package/dist/utils/error-utils.js +9 -0
  63. package/dist/utils/welcome.js +2 -3
  64. package/ecagent/LICENSE +21 -0
  65. package/ecagent/NOTICE +11 -0
  66. package/ecagent/README.md +61 -0
  67. package/ecagent/dist/agent-loop.d.ts +24 -0
  68. package/ecagent/dist/agent-loop.d.ts.map +1 -0
  69. package/ecagent/dist/agent-loop.js +547 -0
  70. package/ecagent/dist/agent-loop.js.map +1 -0
  71. package/ecagent/dist/agent.d.ts +127 -0
  72. package/ecagent/dist/agent.d.ts.map +1 -0
  73. package/ecagent/dist/agent.js +386 -0
  74. package/ecagent/dist/agent.js.map +1 -0
  75. package/ecagent/dist/harness/agent-harness.d.ts +95 -0
  76. package/ecagent/dist/harness/agent-harness.d.ts.map +1 -0
  77. package/ecagent/dist/harness/agent-harness.js +926 -0
  78. package/ecagent/dist/harness/agent-harness.js.map +1 -0
  79. package/ecagent/dist/harness/compaction/branch-summarization.d.ts +51 -0
  80. package/ecagent/dist/harness/compaction/branch-summarization.d.ts.map +1 -0
  81. package/ecagent/dist/harness/compaction/branch-summarization.js +174 -0
  82. package/ecagent/dist/harness/compaction/branch-summarization.js.map +1 -0
  83. package/ecagent/dist/harness/compaction/compaction.d.ts +95 -0
  84. package/ecagent/dist/harness/compaction/compaction.d.ts.map +1 -0
  85. package/ecagent/dist/harness/compaction/compaction.js +528 -0
  86. package/ecagent/dist/harness/compaction/compaction.js.map +1 -0
  87. package/ecagent/dist/harness/compaction/utils.d.ts +25 -0
  88. package/ecagent/dist/harness/compaction/utils.d.ts.map +1 -0
  89. package/ecagent/dist/harness/compaction/utils.js +131 -0
  90. package/ecagent/dist/harness/compaction/utils.js.map +1 -0
  91. package/ecagent/dist/harness/env/nodejs.d.ts +51 -0
  92. package/ecagent/dist/harness/env/nodejs.d.ts.map +1 -0
  93. package/ecagent/dist/harness/env/nodejs.js +513 -0
  94. package/ecagent/dist/harness/env/nodejs.js.map +1 -0
  95. package/ecagent/dist/harness/messages.d.ts +51 -0
  96. package/ecagent/dist/harness/messages.d.ts.map +1 -0
  97. package/ecagent/dist/harness/messages.js +102 -0
  98. package/ecagent/dist/harness/messages.js.map +1 -0
  99. package/ecagent/dist/harness/prompt-templates.d.ts +48 -0
  100. package/ecagent/dist/harness/prompt-templates.d.ts.map +1 -0
  101. package/ecagent/dist/harness/prompt-templates.js +230 -0
  102. package/ecagent/dist/harness/prompt-templates.js.map +1 -0
  103. package/ecagent/dist/harness/session/jsonl-repo.d.ts +26 -0
  104. package/ecagent/dist/harness/session/jsonl-repo.d.ts.map +1 -0
  105. package/ecagent/dist/harness/session/jsonl-repo.js +100 -0
  106. package/ecagent/dist/harness/session/jsonl-repo.js.map +1 -0
  107. package/ecagent/dist/harness/session/jsonl-storage.d.ts +34 -0
  108. package/ecagent/dist/harness/session/jsonl-storage.d.ts.map +1 -0
  109. package/ecagent/dist/harness/session/jsonl-storage.js +234 -0
  110. package/ecagent/dist/harness/session/jsonl-storage.js.map +1 -0
  111. package/ecagent/dist/harness/session/memory-repo.d.ts +18 -0
  112. package/ecagent/dist/harness/session/memory-repo.d.ts.map +1 -0
  113. package/ecagent/dist/harness/session/memory-repo.js +44 -0
  114. package/ecagent/dist/harness/session/memory-repo.js.map +1 -0
  115. package/ecagent/dist/harness/session/memory-storage.d.ts +25 -0
  116. package/ecagent/dist/harness/session/memory-storage.d.ts.map +1 -0
  117. package/ecagent/dist/harness/session/memory-storage.js +111 -0
  118. package/ecagent/dist/harness/session/memory-storage.js.map +1 -0
  119. package/ecagent/dist/harness/session/repo-utils.d.ts +11 -0
  120. package/ecagent/dist/harness/session/repo-utils.d.ts.map +1 -0
  121. package/ecagent/dist/harness/session/repo-utils.js +39 -0
  122. package/ecagent/dist/harness/session/repo-utils.js.map +1 -0
  123. package/ecagent/dist/harness/session/session.d.ts +47 -0
  124. package/ecagent/dist/harness/session/session.d.ts.map +1 -0
  125. package/ecagent/dist/harness/session/session.js +245 -0
  126. package/ecagent/dist/harness/session/session.js.map +1 -0
  127. package/ecagent/dist/harness/session/uuid.d.ts +2 -0
  128. package/ecagent/dist/harness/session/uuid.d.ts.map +1 -0
  129. package/ecagent/dist/harness/session/uuid.js +50 -0
  130. package/ecagent/dist/harness/session/uuid.js.map +1 -0
  131. package/ecagent/dist/harness/skills.d.ts +44 -0
  132. package/ecagent/dist/harness/skills.d.ts.map +1 -0
  133. package/ecagent/dist/harness/skills.js +311 -0
  134. package/ecagent/dist/harness/skills.js.map +1 -0
  135. package/ecagent/dist/harness/system-prompt.d.ts +3 -0
  136. package/ecagent/dist/harness/system-prompt.d.ts.map +1 -0
  137. package/ecagent/dist/harness/system-prompt.js +30 -0
  138. package/ecagent/dist/harness/system-prompt.js.map +1 -0
  139. package/ecagent/dist/harness/types.d.ts +619 -0
  140. package/ecagent/dist/harness/types.d.ts.map +1 -0
  141. package/ecagent/dist/harness/types.js +81 -0
  142. package/ecagent/dist/harness/types.js.map +1 -0
  143. package/ecagent/dist/harness/utils/shell-output.d.ts +14 -0
  144. package/ecagent/dist/harness/utils/shell-output.d.ts.map +1 -0
  145. package/ecagent/dist/harness/utils/shell-output.js +126 -0
  146. package/ecagent/dist/harness/utils/shell-output.js.map +1 -0
  147. package/ecagent/dist/harness/utils/truncate.d.ts +70 -0
  148. package/ecagent/dist/harness/utils/truncate.d.ts.map +1 -0
  149. package/ecagent/dist/harness/utils/truncate.js +290 -0
  150. package/ecagent/dist/harness/utils/truncate.js.map +1 -0
  151. package/ecagent/dist/index.d.ts +22 -0
  152. package/ecagent/dist/index.d.ts.map +1 -0
  153. package/ecagent/dist/index.js +27 -0
  154. package/ecagent/dist/index.js.map +1 -0
  155. package/ecagent/dist/node.d.ts +3 -0
  156. package/ecagent/dist/node.d.ts.map +1 -0
  157. package/ecagent/dist/node.js +3 -0
  158. package/ecagent/dist/node.js.map +1 -0
  159. package/ecagent/dist/proxy.d.ts +69 -0
  160. package/ecagent/dist/proxy.d.ts.map +1 -0
  161. package/ecagent/dist/proxy.js +278 -0
  162. package/ecagent/dist/proxy.js.map +1 -0
  163. package/ecagent/dist/runtime/api/lazy.d.ts +15 -0
  164. package/ecagent/dist/runtime/api/lazy.d.ts.map +1 -0
  165. package/ecagent/dist/runtime/api/lazy.js +59 -0
  166. package/ecagent/dist/runtime/api/lazy.js.map +1 -0
  167. package/ecagent/dist/runtime/api/pi-messages.d.ts +97 -0
  168. package/ecagent/dist/runtime/api/pi-messages.d.ts.map +1 -0
  169. package/ecagent/dist/runtime/api/pi-messages.js +307 -0
  170. package/ecagent/dist/runtime/api/pi-messages.js.map +1 -0
  171. package/ecagent/dist/runtime/api/pi-messages.lazy.d.ts +3 -0
  172. package/ecagent/dist/runtime/api/pi-messages.lazy.d.ts.map +1 -0
  173. package/ecagent/dist/runtime/api/pi-messages.lazy.js +3 -0
  174. package/ecagent/dist/runtime/api/pi-messages.lazy.js.map +1 -0
  175. package/ecagent/dist/runtime/auth/context.d.ts +7 -0
  176. package/ecagent/dist/runtime/auth/context.d.ts.map +1 -0
  177. package/ecagent/dist/runtime/auth/context.js +42 -0
  178. package/ecagent/dist/runtime/auth/context.js.map +1 -0
  179. package/ecagent/dist/runtime/auth/credential-store.d.ts +16 -0
  180. package/ecagent/dist/runtime/auth/credential-store.d.ts.map +1 -0
  181. package/ecagent/dist/runtime/auth/credential-store.js +39 -0
  182. package/ecagent/dist/runtime/auth/credential-store.js.map +1 -0
  183. package/ecagent/dist/runtime/auth/helpers.d.ts +20 -0
  184. package/ecagent/dist/runtime/auth/helpers.d.ts.map +1 -0
  185. package/ecagent/dist/runtime/auth/helpers.js +46 -0
  186. package/ecagent/dist/runtime/auth/helpers.js.map +1 -0
  187. package/ecagent/dist/runtime/auth/resolve.d.ts +26 -0
  188. package/ecagent/dist/runtime/auth/resolve.d.ts.map +1 -0
  189. package/ecagent/dist/runtime/auth/resolve.js +100 -0
  190. package/ecagent/dist/runtime/auth/resolve.js.map +1 -0
  191. package/ecagent/dist/runtime/auth/types.d.ts +180 -0
  192. package/ecagent/dist/runtime/auth/types.d.ts.map +1 -0
  193. package/ecagent/dist/runtime/auth/types.js +2 -0
  194. package/ecagent/dist/runtime/auth/types.js.map +1 -0
  195. package/ecagent/dist/runtime/compat.d.ts +28 -0
  196. package/ecagent/dist/runtime/compat.d.ts.map +1 -0
  197. package/ecagent/dist/runtime/compat.js +86 -0
  198. package/ecagent/dist/runtime/compat.js.map +1 -0
  199. package/ecagent/dist/runtime/index.d.ts +20 -0
  200. package/ecagent/dist/runtime/index.d.ts.map +1 -0
  201. package/ecagent/dist/runtime/index.js +17 -0
  202. package/ecagent/dist/runtime/index.js.map +1 -0
  203. package/ecagent/dist/runtime/models.d.ts +142 -0
  204. package/ecagent/dist/runtime/models.d.ts.map +1 -0
  205. package/ecagent/dist/runtime/models.js +245 -0
  206. package/ecagent/dist/runtime/models.js.map +1 -0
  207. package/ecagent/dist/runtime/oauth.d.ts +2 -0
  208. package/ecagent/dist/runtime/oauth.d.ts.map +1 -0
  209. package/ecagent/dist/runtime/oauth.js +2 -0
  210. package/ecagent/dist/runtime/oauth.js.map +1 -0
  211. package/ecagent/dist/runtime/providers/faux.d.ts +97 -0
  212. package/ecagent/dist/runtime/providers/faux.d.ts.map +1 -0
  213. package/ecagent/dist/runtime/providers/faux.js +395 -0
  214. package/ecagent/dist/runtime/providers/faux.js.map +1 -0
  215. package/ecagent/dist/runtime/types.d.ts +607 -0
  216. package/ecagent/dist/runtime/types.d.ts.map +1 -0
  217. package/ecagent/dist/runtime/types.js +2 -0
  218. package/ecagent/dist/runtime/types.js.map +1 -0
  219. package/ecagent/dist/runtime/utils/diagnostics.d.ts +19 -0
  220. package/ecagent/dist/runtime/utils/diagnostics.d.ts.map +1 -0
  221. package/ecagent/dist/runtime/utils/diagnostics.js +25 -0
  222. package/ecagent/dist/runtime/utils/diagnostics.js.map +1 -0
  223. package/ecagent/dist/runtime/utils/event-stream.d.ts +21 -0
  224. package/ecagent/dist/runtime/utils/event-stream.d.ts.map +1 -0
  225. package/ecagent/dist/runtime/utils/event-stream.js +77 -0
  226. package/ecagent/dist/runtime/utils/event-stream.js.map +1 -0
  227. package/ecagent/dist/runtime/utils/headers.d.ts +4 -0
  228. package/ecagent/dist/runtime/utils/headers.d.ts.map +1 -0
  229. package/ecagent/dist/runtime/utils/headers.js +18 -0
  230. package/ecagent/dist/runtime/utils/headers.js.map +1 -0
  231. package/ecagent/dist/runtime/utils/json-parse.d.ts +16 -0
  232. package/ecagent/dist/runtime/utils/json-parse.d.ts.map +1 -0
  233. package/ecagent/dist/runtime/utils/json-parse.js +113 -0
  234. package/ecagent/dist/runtime/utils/json-parse.js.map +1 -0
  235. package/ecagent/dist/runtime/utils/oauth/types.d.ts +64 -0
  236. package/ecagent/dist/runtime/utils/oauth/types.d.ts.map +1 -0
  237. package/ecagent/dist/runtime/utils/oauth/types.js +2 -0
  238. package/ecagent/dist/runtime/utils/oauth/types.js.map +1 -0
  239. package/ecagent/dist/runtime/utils/overflow.d.ts +58 -0
  240. package/ecagent/dist/runtime/utils/overflow.d.ts.map +1 -0
  241. package/ecagent/dist/runtime/utils/overflow.js +158 -0
  242. package/ecagent/dist/runtime/utils/overflow.js.map +1 -0
  243. package/ecagent/dist/runtime/utils/provider-env.d.ts +7 -0
  244. package/ecagent/dist/runtime/utils/provider-env.d.ts.map +1 -0
  245. package/ecagent/dist/runtime/utils/provider-env.js +44 -0
  246. package/ecagent/dist/runtime/utils/provider-env.js.map +1 -0
  247. package/ecagent/dist/runtime/utils/retry.d.ts +12 -0
  248. package/ecagent/dist/runtime/utils/retry.d.ts.map +1 -0
  249. package/ecagent/dist/runtime/utils/retry.js +90 -0
  250. package/ecagent/dist/runtime/utils/retry.js.map +1 -0
  251. package/ecagent/dist/runtime/utils/typebox-helpers.d.ts +17 -0
  252. package/ecagent/dist/runtime/utils/typebox-helpers.d.ts.map +1 -0
  253. package/ecagent/dist/runtime/utils/typebox-helpers.js +21 -0
  254. package/ecagent/dist/runtime/utils/typebox-helpers.js.map +1 -0
  255. package/ecagent/dist/runtime/utils/validation.d.ts +18 -0
  256. package/ecagent/dist/runtime/utils/validation.d.ts.map +1 -0
  257. package/ecagent/dist/runtime/utils/validation.js +269 -0
  258. package/ecagent/dist/runtime/utils/validation.js.map +1 -0
  259. package/ecagent/dist/types.d.ts +401 -0
  260. package/ecagent/dist/types.d.ts.map +1 -0
  261. package/ecagent/dist/types.js +2 -0
  262. package/ecagent/dist/types.js.map +1 -0
  263. package/ecagent/package.json +93 -0
  264. package/ecagent/src/agent-loop.ts +792 -0
  265. package/ecagent/src/agent.ts +575 -0
  266. package/ecagent/src/harness/agent-harness.ts +1029 -0
  267. package/ecagent/src/harness/compaction/branch-summarization.ts +261 -0
  268. package/ecagent/src/harness/compaction/compaction.ts +753 -0
  269. package/ecagent/src/harness/compaction/utils.ts +144 -0
  270. package/ecagent/src/harness/env/nodejs.ts +569 -0
  271. package/ecagent/src/harness/messages.ts +164 -0
  272. package/ecagent/src/harness/prompt-templates.ts +267 -0
  273. package/ecagent/src/harness/session/jsonl-repo.ts +179 -0
  274. package/ecagent/src/harness/session/jsonl-storage.ts +314 -0
  275. package/ecagent/src/harness/session/memory-repo.ts +50 -0
  276. package/ecagent/src/harness/session/memory-storage.ts +133 -0
  277. package/ecagent/src/harness/session/repo-utils.ts +51 -0
  278. package/ecagent/src/harness/session/session.ts +338 -0
  279. package/ecagent/src/harness/session/uuid.ts +54 -0
  280. package/ecagent/src/harness/skills.ts +375 -0
  281. package/ecagent/src/harness/system-prompt.ts +34 -0
  282. package/ecagent/src/harness/types.ts +838 -0
  283. package/ecagent/src/harness/utils/shell-output.ts +135 -0
  284. package/ecagent/src/harness/utils/truncate.ts +344 -0
  285. package/ecagent/src/index.ts +46 -0
  286. package/ecagent/src/node.ts +2 -0
  287. package/ecagent/src/proxy.ts +367 -0
  288. package/ecagent/src/runtime/api/lazy.ts +70 -0
  289. package/ecagent/src/runtime/api/pi-messages.lazy.ts +4 -0
  290. package/ecagent/src/runtime/api/pi-messages.ts +436 -0
  291. package/ecagent/src/runtime/auth/context.ts +45 -0
  292. package/ecagent/src/runtime/auth/credential-store.ts +47 -0
  293. package/ecagent/src/runtime/auth/helpers.ts +46 -0
  294. package/ecagent/src/runtime/auth/resolve.ts +141 -0
  295. package/ecagent/src/runtime/auth/types.ts +182 -0
  296. package/ecagent/src/runtime/compat.ts +158 -0
  297. package/ecagent/src/runtime/index.ts +31 -0
  298. package/ecagent/src/runtime/models.ts +452 -0
  299. package/ecagent/src/runtime/oauth.ts +1 -0
  300. package/ecagent/src/runtime/providers/faux.ts +538 -0
  301. package/ecagent/src/runtime/types.ts +718 -0
  302. package/ecagent/src/runtime/utils/diagnostics.ts +45 -0
  303. package/ecagent/src/runtime/utils/event-stream.ts +88 -0
  304. package/ecagent/src/runtime/utils/headers.ts +18 -0
  305. package/ecagent/src/runtime/utils/json-parse.ts +124 -0
  306. package/ecagent/src/runtime/utils/oauth/types.ts +79 -0
  307. package/ecagent/src/runtime/utils/overflow.ts +165 -0
  308. package/ecagent/src/runtime/utils/provider-env.ts +52 -0
  309. package/ecagent/src/runtime/utils/retry.ts +101 -0
  310. package/ecagent/src/runtime/utils/typebox-helpers.ts +24 -0
  311. package/ecagent/src/runtime/utils/validation.ts +310 -0
  312. package/ecagent/src/types.ts +430 -0
  313. package/ecagent/tsconfig.build.json +32 -0
  314. package/kits/schemas/_meta.json +7 -4
  315. package/kits/schemas/agent-config.schema.10.json +370 -0
  316. package/kits/schemas/defaults.schema.4.json +93 -0
  317. package/kits/schemas/relation-config.schema.8.json +69 -0
  318. package/kits/templates/roles/admin.json +1 -1
  319. package/package.json +22 -6
  320. package/skills/eclink/SKILL.md +70 -0
  321. package/skills/eclink/agents/openai.yaml +4 -0
  322. package/assets/.env.template +0 -4
@@ -2,7 +2,7 @@ import path from 'path';
2
2
  import fs from 'fs';
3
3
  import os from 'os';
4
4
  import crypto from 'crypto';
5
- import { BaseagentRunnerUnavailableError, hasCompact, hasClearSession, autoCompactWindowForModel, isClaudeContextUsageModel } from '../../agents/runner-types.js';
5
+ import { BaseagentRunnerUnavailableError, hasCompact, autoCompactWindowForContextWindow, autoCompactWindowForModel, isClaudeContextUsageModel, numericToken } from '../../agents/runner-types.js';
6
6
  import { buildTaskRuntimeEnv } from '../../cli/task-context.js';
7
7
  import { IMRenderer } from './im-renderer.js';
8
8
  import { createSendFileMarkerPattern } from './file-markers.js';
@@ -10,7 +10,7 @@ import { createTextInferenceProvider } from '../inference/text-inference.js';
10
10
  import { StreamIdleMonitor } from './stream-idle-monitor.js';
11
11
  import { logger } from '../../utils/logger.js';
12
12
  import { isHostChinese } from '../../utils/locale.js';
13
- import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, prefixErrorType, isRetryableError, isContextTooLongText } from '../../utils/error-utils.js';
13
+ import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, isAbortTerminalReason, prefixErrorType, isRetryableError, isContextTooLongText } from '../../utils/error-utils.js';
14
14
  import { isEvolcoreSendCommandForSession } from '../permission/ec-command-parser.js';
15
15
  import { summarizeToolInput } from '../../utils/tool-summary.js';
16
16
  import { getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
@@ -28,7 +28,9 @@ import { AGENT_DELEGATION_TOKEN_ENV } from '../auth/agent-delegation.js';
28
28
  import { hasTrustedPrincipal } from '../auth/authenticated-actor.js';
29
29
  // Re-export 工具函数(向后兼容,让其他模块可以从 response-engine 导入)
30
30
  export { buildEnvelope, sendInteractionPayload } from './message-utils.js';
31
- import { constrainResolvedModelForRole } from '../model/model-permission.js';
31
+ import { constrainResolvedModelForRole, isModelAllowedForRoleBaseagent } from '../model/model-permission.js';
32
+ import { eligibleFallbackModels } from '../model/model-fallback.js';
33
+ import { RetryScheduler } from './retry-scheduler.js';
32
34
  import { constrainRuntimePermissionMode, resolveRuntimeStringField } from '../role/runtime-policy.js';
33
35
  import { resolveEffective, resolveEffectiveFieldWithSource } from '../../config/config-manager.js';
34
36
  import { dispatchToMentionMode } from '../../config/mention-mode.js';
@@ -130,9 +132,11 @@ function createRetryAttemptTimeout(timeoutMs) {
130
132
  },
131
133
  };
132
134
  }
135
+ const MAX_CONTEXT_RECOVERY_ATTEMPTS = 2;
133
136
  const RETRY_EXHAUSTED_MARK = Symbol('evolcore.retryExhausted');
134
137
  const RETRY_MADE_PROGRESS_MARK = Symbol('evolcore.retryMadeProgress');
135
138
  const RETRY_HEALTH_RECORDED_MARK = Symbol('evolcore.retryHealthRecorded');
139
+ const MODEL_FALLBACK_EXHAUSTED_MARK = Symbol('evolcore.modelFallbackExhausted');
136
140
  function markRetryExhausted(error, retries) {
137
141
  if (error && typeof error === 'object') {
138
142
  error[RETRY_EXHAUSTED_MARK] = retries;
@@ -163,16 +167,84 @@ function markRetryHealthRecorded(error) {
163
167
  function wasRetryHealthRecorded(error) {
164
168
  return !!(error && typeof error === 'object' && error[RETRY_HEALTH_RECORDED_MARK] === true);
165
169
  }
170
+ function markModelFallbackExhausted(error, message) {
171
+ error[MODEL_FALLBACK_EXHAUSTED_MARK] = message;
172
+ return error;
173
+ }
174
+ function getModelFallbackExhaustedMessage(error) {
175
+ const message = error && typeof error === 'object'
176
+ ? error[MODEL_FALLBACK_EXHAUSTED_MARK]
177
+ : undefined;
178
+ return typeof message === 'string' ? message : undefined;
179
+ }
166
180
  function isNonErrorInterruptReason(reason) {
167
181
  return reason === 'new_message'
168
182
  || reason === 'stop'
169
183
  || reason === 'recalled'
170
184
  || reason === 'daemon_restart';
171
185
  }
186
+ function isExpectedTerminalInterrupt(reason) {
187
+ return isAbortTerminalReason(reason);
188
+ }
172
189
  function formatRetryableErrorFinalMessage(error, retries) {
173
190
  const reason = getErrorMessage(error, undefined, false) || 'API 暂时不可用';
174
191
  return `❌ API 暂时不可用,已自动重试 ${retries} 次仍失败,任务已停止。\n原因:${reason}`;
175
192
  }
193
+ /**
194
+ * A protocol failure may be replayed only when the stream provides no evidence
195
+ * that model work or a side effect occurred. This deliberately excludes normal
196
+ * silent completions, which can be input-accepted and still return an empty
197
+ * result after consuming tokens.
198
+ */
199
+ function isSafeProtocolReplayCandidate(result, reason) {
200
+ if (reason !== 'input_not_consumed' && reason !== 'empty_success')
201
+ return false;
202
+ if (result.isError
203
+ || result.hasReceivedText
204
+ || result.assistantUuid
205
+ || (result.textCount ?? 0) > 0
206
+ || (result.toolUseCount ?? 0) > 0
207
+ || (result.openToolUseIds?.length ?? 0) > 0
208
+ || result.lastReplyText.trim()
209
+ || result.fullText.trim()
210
+ || hasRecordedTokenUsage(result)) {
211
+ return false;
212
+ }
213
+ return true;
214
+ }
215
+ function usageHasTokens(usage) {
216
+ if (!usage)
217
+ return false;
218
+ if (numericToken(usage.input_tokens) > 0
219
+ || numericToken(usage.output_tokens) > 0
220
+ || numericToken(usage.cache_read_input_tokens) > 0
221
+ || numericToken(usage.cache_creation_input_tokens) > 0) {
222
+ return true;
223
+ }
224
+ return Array.isArray(usage.iterations) && usage.iterations.some(usageHasTokens);
225
+ }
226
+ function hasRecordedTokenUsage(result) {
227
+ return usageHasTokens(result.tokenUsage)
228
+ || usageHasTokens(result.lastModelCall?.tokenUsage)
229
+ || result.modelCalls?.some(call => usageHasTokens(call.tokenUsage)) === true;
230
+ }
231
+ function formatProtocolReplayExhaustedMessage(reason, replayAttempts) {
232
+ if (replayAttempts === 0 || (reason !== 'input_not_consumed' && reason !== 'empty_success')) {
233
+ return `Agent 协议未闭合(${reason}),任务未提交`;
234
+ }
235
+ const detail = reason === 'input_not_consumed'
236
+ ? '模型服务未确认接收本次输入'
237
+ : '模型服务未返回可提交的结果';
238
+ return `${detail}(${reason}),已立即重试 ${replayAttempts} 次仍失败,任务未提交。\n请稍后重试;若持续出现,请切换模型或向上游服务商提供错误代码和发生时间。`;
239
+ }
240
+ function withFallbackModel(override, candidate) {
241
+ const { model: _model, effort: _effort, effortMode: _effortMode, ...preserved } = override || {};
242
+ return {
243
+ ...preserved,
244
+ model: candidate.model,
245
+ ...(candidate.effort ? { effort: candidate.effort } : { effortMode: 'model_default' }),
246
+ };
247
+ }
176
248
  function getStreamErrorText(result) {
177
249
  return [
178
250
  result.errors?.join('\n'),
@@ -217,6 +289,17 @@ function streamHitContextLimit(result) {
217
289
  isContextTooLongText(result.errors?.join(' ') || '') ||
218
290
  isContextTooLongText(result.fullText);
219
291
  }
292
+ /** Preserve task-level audit facts when a context recovery starts a new SDK stream. */
293
+ function mergeContextRecoveryResult(previous, next) {
294
+ return {
295
+ ...next,
296
+ inputAccepted: previous.inputAccepted || next.inputAccepted,
297
+ textCount: (previous.textCount ?? 0) + (next.textCount ?? 0),
298
+ toolUseCount: (previous.toolUseCount ?? 0) + (next.toolUseCount ?? 0),
299
+ numTurns: (previous.numTurns ?? 0) + (next.numTurns ?? 0),
300
+ contextRecoveryAttempts: (previous.contextRecoveryAttempts ?? 0) + 1,
301
+ };
302
+ }
220
303
  /**
221
304
  * `isContextTooLongText` 是给错误分类用的宽松匹配,不能拿来删除模型正文:正常回答
222
305
  * 可能会说明或引用 `Prompt is too long`。这里只识别网关会单独输出的短错误行。
@@ -292,9 +375,6 @@ function getContextCompactFailedHint(agent) {
292
375
  function canCompactAgent(agent) {
293
376
  return hasCompact(agent) && agent.capabilities?.compact !== false;
294
377
  }
295
- function canClearAgent(agent) {
296
- return hasClearSession(agent) && agent.capabilities?.clear !== false;
297
- }
298
378
  export async function resolveGroupRulesForTurn(opts, sync = syncGroupRulesContext) {
299
379
  if (!opts.isAunGroup)
300
380
  return {};
@@ -305,9 +385,7 @@ export async function resolveGroupRulesForTurn(opts, sync = syncGroupRulesContex
305
385
  return sync({ selfAid: opts.selfAid, groupId: opts.groupId, channel: opts.channel });
306
386
  }
307
387
  function autoCompactTokensFromMaxTokens(maxTokens) {
308
- if (!maxTokens || maxTokens <= 0)
309
- return undefined;
310
- return maxTokens >= 1000000 ? maxTokens - 100000 : maxTokens;
388
+ return autoCompactWindowForContextWindow(maxTokens);
311
389
  }
312
390
  /**
313
391
  * 构造 OutboundEnvelope —— 出站三件套的信封部分。
@@ -341,8 +419,10 @@ export class ResponseEngine {
341
419
  primaryRunnerKey;
342
420
  interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
343
421
  timeoutErrors = new Map();
344
- /** sessionId → 模型降级状态(带退避探测,进程重启清零) */
422
+ /** sessionId → 已成功的模型降级状态(进程重启清零) */
345
423
  modelFallbackMap = new Map();
424
+ /** sessionId → interruptible task-level API retry delays. */
425
+ retryDelayWaiters = new Map();
346
426
  interactionRouter;
347
427
  messageQueue;
348
428
  handoffRuntime;
@@ -479,6 +559,7 @@ export class ResponseEngine {
479
559
  const reason = (event.reason || 'new_message');
480
560
  this.turnCoordinator.invalidate(event.sessionId, reason);
481
561
  this.interruptedSessions.set(event.sessionId, reason);
562
+ this.cancelRetryDelays(event.sessionId);
482
563
  this.agentDelegationRegistry?.revokeSession(event.sessionId);
483
564
  }
484
565
  });
@@ -512,6 +593,9 @@ export class ResponseEngine {
512
593
  this.messageQueue = queue;
513
594
  }
514
595
  async interruptSession(sessionId, reason) {
596
+ // A retry delay has no active runner stream to abort. Wake it before any
597
+ // session-store I/O so /stop and a newer message take effect immediately.
598
+ this.cancelRetryDelays(sessionId);
515
599
  const session = await this.sessionManager.getSessionById(sessionId);
516
600
  if (!session)
517
601
  return;
@@ -831,30 +915,22 @@ export class ResponseEngine {
831
915
  return await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress, proactive, undefined, // 重试分支不调插件钩子
832
916
  undefined, turnLease);
833
917
  }
834
- await this.emitOperationalNotice(renderer, '⚠️ 压缩失败,尝试清空会话历史后重试...', 'warn', 'compact-failed-clear');
835
- await renderer.flush();
836
- if (!canClearAgent(agent)) {
837
- throw new Error('CONTEXT_COMPACT_FAILED');
838
- }
839
- const previousAgentSessionId = session.agentSessionId;
840
- let cleared = false;
841
- try {
842
- cleared = await agent.clearSession(session.id, previousAgentSessionId, absoluteProjectPath);
843
- }
844
- catch (error) {
845
- logger.warn(`[ResponseEngine] clearSession failed after compact failure: ${error}`);
846
- }
847
- if (!cleared) {
848
- throw new Error('CONTEXT_COMPACT_FAILED');
918
+ // Dropping the whole session hides the root cause and can replay side
919
+ // effects in unattended trigger tasks. Surface an actionable failure
920
+ // instead; callers may explicitly start a fresh task if that is desired.
921
+ throw new Error('CONTEXT_COMPACT_FAILED');
922
+ }
923
+ async recoverFromContextLimit(initialResult, opts) {
924
+ let result = initialResult;
925
+ let attempts = initialResult.contextRecoveryAttempts ?? 0;
926
+ while (attempts < MAX_CONTEXT_RECOVERY_ATTEMPTS) {
927
+ const recovered = await this.retryAfterContextRecovery('上下文已自动压缩,请继续之前未完成的任务。', opts);
928
+ result = mergeContextRecoveryResult(result, recovered);
929
+ attempts = result.contextRecoveryAttempts ?? attempts + 1;
930
+ if (!result.isError || !streamHitContextLimit(result))
931
+ return result;
849
932
  }
850
- session.agentSessionId = undefined;
851
- await this.sessionManager.updateSession(session.id, { agentSessionId: null });
852
- await this.emitOperationalNotice(renderer, '✅ 会话已清空,继续处理...', 'info', 'clear-retry');
853
- await renderer.flush();
854
- const retryStream = await agent.runQuery(session.id, '会话历史已清空,请继续之前未完成的任务。', absoluteProjectPath, undefined, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
855
- agent.registerStream(streamKey, retryStream);
856
- return await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress, proactive, undefined, // 重试分支不调插件钩子
857
- undefined, turnLease);
933
+ return result;
858
934
  }
859
935
  /**
860
936
  * 根据 channel 标识查找渠道信息
@@ -1357,6 +1433,36 @@ export class ResponseEngine {
1357
1433
  ? configuredSeconds * 1000
1358
1434
  : undefined;
1359
1435
  }
1436
+ waitForRetryDelay(sessionId, delayMs) {
1437
+ if (this.interruptedSessions.has(sessionId))
1438
+ return Promise.resolve(false);
1439
+ return new Promise(resolve => {
1440
+ let settled = false;
1441
+ const waiters = this.retryDelayWaiters.get(sessionId) ?? new Set();
1442
+ this.retryDelayWaiters.set(sessionId, waiters);
1443
+ const finish = (elapsed) => {
1444
+ if (settled)
1445
+ return;
1446
+ settled = true;
1447
+ clearTimeout(timer);
1448
+ waiters.delete(cancel);
1449
+ if (waiters.size === 0)
1450
+ this.retryDelayWaiters.delete(sessionId);
1451
+ resolve(elapsed);
1452
+ };
1453
+ const cancel = () => finish(false);
1454
+ const timer = setTimeout(() => finish(true), delayMs);
1455
+ timer.unref?.();
1456
+ waiters.add(cancel);
1457
+ });
1458
+ }
1459
+ cancelRetryDelays(sessionId) {
1460
+ const waiters = this.retryDelayWaiters.get(sessionId);
1461
+ if (!waiters)
1462
+ return;
1463
+ for (const cancel of [...waiters])
1464
+ cancel();
1465
+ }
1360
1466
  /** 自动安全模式已禁用:仅保留错误计数,不再自动切换状态 */
1361
1467
  async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, getLastIdleSec, outputState = { middleOutputMode: 'all' }, timeoutControl) {
1362
1468
  const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
@@ -1703,6 +1809,11 @@ export class ResponseEngine {
1703
1809
  });
1704
1810
  };
1705
1811
  }
1812
+ // Kept outside the task body so a thrown terminal error can preserve the
1813
+ // work already performed in its trigger audit metadata.
1814
+ let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
1815
+ let startTime = Date.now();
1816
+ let protocolReplayAttempts = 0;
1706
1817
  try {
1707
1818
  const isBackground = this.isBackgroundSession(session, message.channel, message.channelId);
1708
1819
  // 记录收到消息
@@ -1879,7 +1990,7 @@ export class ResponseEngine {
1879
1990
  // 均以普通非最终文本显示运行提示;none 不显示。其它 activity 在 all 下仍保持结构化。
1880
1991
  // 它仍发生在真正的 prompt 执行之前。
1881
1992
  await this.runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, renderer);
1882
- const startTime = Date.now();
1993
+ startTime = Date.now();
1883
1994
  if (isProactive) {
1884
1995
  logger.info(`[ResponseEngine] proactive mode: outputs via thought.put task=${taskId}`);
1885
1996
  }
@@ -2151,12 +2262,15 @@ export class ResponseEngine {
2151
2262
  : body;
2152
2263
  // 先用裸文本兜底;vars 构造完成后用消息渲染层重算(见下方 effectivePrompt 重赋值)。
2153
2264
  let effectivePrompt = wrapPrompt(message.content);
2154
- let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
2155
2265
  let renderResult;
2156
2266
  let effectiveSystemPrompt;
2157
2267
  let modelOverride;
2158
- let usedFallback = false;
2268
+ let usedFallback;
2159
2269
  let skipEvolcoreModel = false;
2270
+ let probingPrimaryModel = false;
2271
+ let primaryModel;
2272
+ let fallbackCandidates = [];
2273
+ let previousFallbackState;
2160
2274
  let agentModel;
2161
2275
  let v2HandoffIds = [];
2162
2276
  let v2HandoffDirection;
@@ -2211,64 +2325,50 @@ export class ResponseEngine {
2211
2325
  let effectiveEffortSource;
2212
2326
  const triggerModelOverride = message.triggerMeta?.modelOverride;
2213
2327
  const triggerEffortOverride = message.triggerMeta?.effortOverride;
2214
- let appliedTriggerModelOverride = false;
2215
- let effectiveTriggerEffortOverride;
2216
- // 取降级状态,按退避策略决定是否跳过 evolcore 作用域模型
2217
- const fbState = this.modelFallbackMap.get(session.id) ?? {
2218
- failCount: 0, fallbackActive: false,
2219
- messagesSinceFallback: 0, nextProbeAt: 2, hintShown: false,
2220
- };
2221
- // 退避期内递增消息计数,判断是否到探测点
2222
- if (fbState.fallbackActive) {
2223
- fbState.messagesSinceFallback++;
2224
- skipEvolcoreModel = fbState.messagesSinceFallback < fbState.nextProbeAt;
2225
- this.modelFallbackMap.set(session.id, fbState);
2226
- }
2227
- // 非跳过时:尝试解析 evolcore 作用域模型
2328
+ const hasExplicitTriggerModel = typeof triggerModelOverride === 'string'
2329
+ && triggerModelOverride.trim().length > 0;
2228
2330
  let evolcoreModelOverride;
2229
- if (!skipEvolcoreModel) {
2230
- try {
2231
- const selector = { self: selfAid || undefined, peerKey };
2232
- const modelResolution = resolveEffectiveFieldWithSource(`baseagents.${normalizedBaseagent.canonical}.model`, selector, { cache: true });
2233
- const effortResolution = resolveEffectiveFieldWithSource(`baseagents.${normalizedBaseagent.canonical}.${normalizedBaseagent.canonical === 'codex' ? 'reasoning' : 'effort'}`, selector, { cache: true });
2234
- const modelDecision = constrainResolvedModelForRole({
2235
- role: peerRole,
2236
- baseagent: normalizedBaseagent.canonical,
2237
- model: modelResolution.value,
2238
- resolveModelId: typeof agent.resolveModelId === 'function'
2239
- ? agent.resolveModelId.bind(agent)
2240
- : undefined,
2241
- selfAid: selfAid || undefined,
2242
- });
2243
- const model = modelDecision.model;
2244
- const effortField = `baseagents.${normalizedBaseagent.canonical}.${normalizedBaseagent.canonical === 'codex' ? 'reasoning' : 'effort'}`;
2245
- const effortDecision = resolveRuntimeStringField({
2246
- selfAid: selfAid || undefined,
2247
- role: peerRole,
2248
- field: effortField,
2249
- configuredValue: effortResolution.value,
2250
- });
2251
- const effectiveEffortValue = effortDecision.effectiveValue;
2252
- const effort = effectiveEffortValue === 'auto' ? undefined : effectiveEffortValue;
2253
- effectiveModelSource = modelDecision.constrained
2254
- ? 'role'
2255
- : modelResolution.source ? `${modelResolution.source.target}:${modelResolution.source.file}` : undefined;
2256
- effectiveEffortSource = effortDecision.decidedBy === 'role'
2257
- ? 'role'
2258
- : effortResolution.source
2259
- ? `${effortResolution.source.target}:${effortResolution.source.file}`
2260
- : undefined;
2261
- if (model || effort) {
2262
- evolcoreModelOverride = { model, effort };
2263
- effectiveModel = model;
2264
- }
2265
- effectiveEffort = effectiveEffortValue;
2266
- }
2267
- catch (e) {
2268
- logger.warn(`[ResponseEngine] effective model config resolution failed: ${e instanceof Error ? e.message : String(e)}`);
2331
+ try {
2332
+ const selector = { self: selfAid || undefined, peerKey };
2333
+ const modelResolution = resolveEffectiveFieldWithSource(`baseagents.${normalizedBaseagent.canonical}.model`, selector, { cache: true });
2334
+ const effortResolution = resolveEffectiveFieldWithSource(`baseagents.${normalizedBaseagent.canonical}.${normalizedBaseagent.canonical === 'codex' ? 'reasoning' : 'effort'}`, selector, { cache: true });
2335
+ const modelDecision = constrainResolvedModelForRole({
2336
+ role: peerRole,
2337
+ baseagent: normalizedBaseagent.canonical,
2338
+ model: modelResolution.value,
2339
+ resolveModelId: typeof agent.resolveModelId === 'function'
2340
+ ? agent.resolveModelId.bind(agent)
2341
+ : undefined,
2342
+ selfAid: selfAid || undefined,
2343
+ });
2344
+ const model = modelDecision.model;
2345
+ const effortField = `baseagents.${normalizedBaseagent.canonical}.${normalizedBaseagent.canonical === 'codex' ? 'reasoning' : 'effort'}`;
2346
+ const effortDecision = resolveRuntimeStringField({
2347
+ selfAid: selfAid || undefined,
2348
+ role: peerRole,
2349
+ field: effortField,
2350
+ configuredValue: effortResolution.value,
2351
+ });
2352
+ const effectiveEffortValue = effortDecision.effectiveValue;
2353
+ const effort = effectiveEffortValue === 'auto' ? undefined : effectiveEffortValue;
2354
+ effectiveModelSource = modelDecision.constrained
2355
+ ? 'role'
2356
+ : modelResolution.source ? `${modelResolution.source.target}:${modelResolution.source.file}` : undefined;
2357
+ effectiveEffortSource = effortDecision.decidedBy === 'role'
2358
+ ? 'role'
2359
+ : effortResolution.source
2360
+ ? `${effortResolution.source.target}:${effortResolution.source.file}`
2361
+ : undefined;
2362
+ if (model || effort) {
2363
+ evolcoreModelOverride = { model, effort };
2364
+ effectiveModel = model;
2269
2365
  }
2270
- modelOverride = evolcoreModelOverride;
2366
+ effectiveEffort = effectiveEffortValue;
2271
2367
  }
2368
+ catch (e) {
2369
+ logger.warn(`[ResponseEngine] effective model config resolution failed: ${e instanceof Error ? e.message : String(e)}`);
2370
+ }
2371
+ modelOverride = evolcoreModelOverride;
2272
2372
  if (triggerModelOverride || triggerEffortOverride) {
2273
2373
  const constrainedOverride = { ...(modelOverride || {}) };
2274
2374
  if (triggerModelOverride) {
@@ -2287,7 +2387,6 @@ export class ResponseEngine {
2287
2387
  delete constrainedOverride.model;
2288
2388
  effectiveModel = triggerModelDecision.model;
2289
2389
  effectiveModelSource = triggerModelDecision.constrained ? 'role' : 'trigger';
2290
- appliedTriggerModelOverride = !triggerModelDecision.constrained;
2291
2390
  }
2292
2391
  if (triggerEffortOverride) {
2293
2392
  const effortField = `baseagents.${normalizedBaseagent.canonical}.${normalizedBaseagent.canonical === 'codex' ? 'reasoning' : 'effort'}`;
@@ -2298,9 +2397,9 @@ export class ResponseEngine {
2298
2397
  configuredValue: triggerEffortOverride,
2299
2398
  });
2300
2399
  const triggerEffort = triggerEffortDecision.effectiveValue;
2301
- effectiveTriggerEffortOverride = triggerEffort === 'auto' ? undefined : triggerEffort;
2302
- if (effectiveTriggerEffortOverride)
2303
- constrainedOverride.effort = effectiveTriggerEffortOverride;
2400
+ const effectiveTriggerEffort = triggerEffort === 'auto' ? undefined : triggerEffort;
2401
+ if (effectiveTriggerEffort)
2402
+ constrainedOverride.effort = effectiveTriggerEffort;
2304
2403
  else
2305
2404
  delete constrainedOverride.effort;
2306
2405
  effectiveEffort = triggerEffort;
@@ -2308,6 +2407,46 @@ export class ResponseEngine {
2308
2407
  }
2309
2408
  modelOverride = constrainedOverride;
2310
2409
  }
2410
+ agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
2411
+ primaryModel = modelOverride?.model || agentModel;
2412
+ if (!hasExplicitTriggerModel && primaryModel) {
2413
+ const fallbackResolution = eligibleFallbackModels({
2414
+ baseagent: normalizedBaseagent.canonical,
2415
+ currentModel: primaryModel,
2416
+ resolveModelId: typeof agent.resolveModelId === 'function'
2417
+ ? agent.resolveModelId.bind(agent)
2418
+ : undefined,
2419
+ isAllowed: model => isModelAllowedForRoleBaseagent(peerRole, normalizedBaseagent.canonical, model, typeof agent.resolveModelId === 'function'
2420
+ ? agent.resolveModelId.bind(agent)
2421
+ : undefined, selfAid || undefined),
2422
+ });
2423
+ fallbackCandidates = fallbackResolution.candidates;
2424
+ if (fallbackResolution.rejected.length > 0) {
2425
+ logger.warn(`[ResponseEngine] Ignored ${fallbackResolution.rejected.length} fallback model candidate(s): session=${session.id}`);
2426
+ }
2427
+ const existingFallback = this.modelFallbackMap.get(session.id);
2428
+ if (existingFallback
2429
+ && (existingFallback.baseagent !== normalizedBaseagent.canonical
2430
+ || existingFallback.primaryModel !== primaryModel
2431
+ || !fallbackCandidates.some(candidate => candidate.model === existingFallback.activeFallback.model))) {
2432
+ this.modelFallbackMap.delete(session.id);
2433
+ }
2434
+ else if (existingFallback) {
2435
+ previousFallbackState = existingFallback;
2436
+ existingFallback.messagesSinceFallback++;
2437
+ probingPrimaryModel = existingFallback.messagesSinceFallback >= existingFallback.nextProbeAt;
2438
+ if (!probingPrimaryModel) {
2439
+ skipEvolcoreModel = true;
2440
+ usedFallback = existingFallback.activeFallback;
2441
+ modelOverride = withFallbackModel(modelOverride, usedFallback);
2442
+ effectiveModel = usedFallback.model;
2443
+ effectiveModelSource = 'fallback';
2444
+ effectiveEffort = usedFallback.effort;
2445
+ effectiveEffortSource = usedFallback.effort ? 'fallback' : 'model_default';
2446
+ }
2447
+ this.modelFallbackMap.set(session.id, existingFallback);
2448
+ }
2449
+ }
2311
2450
  // permissionMode 随角色策略或 trigger override 传入;单 runner
2312
2451
  // 嵌入/测试路径没有 self/peer 作用域时,避免制造无配置来源的 override。
2313
2452
  const shouldPassPermissionMode = !!message.triggerMeta?.permissionModeOverride || !!selfAid;
@@ -2320,7 +2459,6 @@ export class ResponseEngine {
2320
2459
  sessionTitle: deriveSessionTitle(session.name, message.content, session.threadId),
2321
2460
  };
2322
2461
  }
2323
- agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
2324
2462
  const causationPath = inputCausation.trigger?.path ?? [];
2325
2463
  const originNode = causationPath[0];
2326
2464
  logger.info(`[ResponseEngine] execution context session=${session.id}`
@@ -2418,8 +2556,8 @@ export class ResponseEngine {
2418
2556
  baseAgentName: normalizedBaseagent.displayName,
2419
2557
  baseAgentModel: agentModel || undefined,
2420
2558
  effectiveModel: effectiveModel || agentModel || undefined,
2421
- modelFallbackActive: (fbState.fallbackActive || skipEvolcoreModel) ? true : undefined,
2422
- modelFallbackModel: (fbState.fallbackActive || skipEvolcoreModel) ? (agentModel || undefined) : undefined,
2559
+ modelFallbackActive: usedFallback ? true : undefined,
2560
+ modelFallbackModel: usedFallback?.model,
2423
2561
  agentSessionId: session.agentSessionId || undefined,
2424
2562
  // 渲染模式:各类型当前激活的 modeName(从内存 config 读,渲染层据此选 manifest section)。
2425
2563
  renderModes: this.agentRegistry?.resolveByChannel(channelKey)?.config?.render ?? undefined,
@@ -2603,12 +2741,11 @@ export class ResponseEngine {
2603
2741
  });
2604
2742
  runtimeEnv[AGENT_DELEGATION_TOKEN_ENV] = delegationToken;
2605
2743
  }
2606
- // 可重试错误(403/429/5xx/模型繁忙)按明确退避序列重试。
2607
- const RETRY_DELAYS_MS = [5_000, 10_000, 30_000];
2608
- const MAX_RETRIES = RETRY_DELAYS_MS.length;
2744
+ const retryScheduler = new RetryScheduler(fallbackCandidates, modelOverride?.model || agentModel, model => (typeof agent.resolveModelId === 'function'
2745
+ ? (agent.resolveModelId(model) ?? model)
2746
+ : model));
2609
2747
  const retryAttemptTimeoutMs = this.retryAttemptTimeoutMs();
2610
2748
  let runAttempt = 1;
2611
- let consecutiveRetryFailures = 0;
2612
2749
  const recordRetryHealthError = async (retryError) => {
2613
2750
  if (!policy.accumulateErrors(chatType, identityRole))
2614
2751
  return;
@@ -2624,10 +2761,10 @@ export class ResponseEngine {
2624
2761
  };
2625
2762
  while (true) {
2626
2763
  let streamRegistered = false;
2627
- const shouldTimeoutRetryAttempt = consecutiveRetryFailures > 0 && retryAttemptTimeoutMs !== undefined;
2764
+ const shouldTimeoutRetryAttempt = retryScheduler.shouldTimeoutAttempt() && retryAttemptTimeoutMs !== undefined;
2628
2765
  let attemptTimeout;
2629
2766
  try {
2630
- logger.info(`[ResponseEngine] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${runAttempt} consecutiveFailures=${consecutiveRetryFailures} agentSessionId=${session.agentSessionId ?? 'none'}`);
2767
+ logger.info(`[ResponseEngine] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${runAttempt} apiRetries=${retryScheduler.apiRetryAttemptsMade} agentSessionId=${session.agentSessionId ?? 'none'}`);
2631
2768
  const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, renderResult?.images.length ? renderResult.images : message.images, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
2632
2769
  agent.registerStream(streamKey, stream);
2633
2770
  streamRegistered = true;
@@ -2642,15 +2779,32 @@ export class ResponseEngine {
2642
2779
  streamResult = attemptTimeout
2643
2780
  ? await Promise.race([processAttempt, attemptTimeout.promise])
2644
2781
  : await processAttempt;
2782
+ if (!streamResult.isError) {
2783
+ const protocolDecision = this.turnCoordinator.evaluateCommit(turnLease, this.buildTurnCommitEvidence(session, streamResult));
2784
+ const protocolReason = protocolDecision.ok ? undefined : protocolDecision.reason;
2785
+ const safeProtocolReplay = isSafeProtocolReplayCandidate(streamResult, protocolReason);
2786
+ const protocolRetry = retryScheduler.afterProtocolFailure(safeProtocolReplay ? protocolReason : undefined, safeProtocolReplay);
2787
+ if (protocolRetry.kind === 'protocol_replay') {
2788
+ protocolReplayAttempts = protocolRetry.attempt;
2789
+ agent.cleanupStream(streamKey);
2790
+ logger.warn(`[ResponseEngine] Replaying zero-work protocol failure: session=${session.id} `
2791
+ + `task=${taskId} reason=${protocolReason} attempt=${protocolRetry.attempt}/${protocolRetry.maxAttempts}`);
2792
+ await this.emitOperationalNotice(renderer, `模型未确认完成,正在立即重试 ${protocolRetry.attempt}/${protocolRetry.maxAttempts}`, 'warn', 'protocol-retry');
2793
+ await renderer.flush();
2794
+ runAttempt++;
2795
+ continue;
2796
+ }
2797
+ }
2645
2798
  if (streamResult.isError && !streamHitContextLimit(streamResult)) {
2646
2799
  const streamErrorText = getStreamErrorText(streamResult);
2647
- if (streamErrorText && isRetryableError(new Error(streamErrorText))) {
2800
+ const streamError = streamErrorText ? new Error(streamErrorText) : undefined;
2801
+ if (streamError && (isRetryableError(streamError) || classifyError(streamError) === ErrorType.MODEL_UNAVAILABLE)) {
2648
2802
  renderer.discardPending();
2649
- throw new Error(streamErrorText);
2803
+ throw streamError;
2650
2804
  }
2651
2805
  }
2652
2806
  // 探测成功(退避期内到达探测点且用的是 evolcore 模型)→ 清零降级状态
2653
- if (fbState.fallbackActive && !skipEvolcoreModel && !usedFallback) {
2807
+ if (probingPrimaryModel && !usedFallback) {
2654
2808
  this.modelFallbackMap.delete(session.id);
2655
2809
  logger.info(`[ResponseEngine] Model probe succeeded, cleared fallback state for session=${session.id}`);
2656
2810
  }
@@ -2673,47 +2827,67 @@ export class ResponseEngine {
2673
2827
  permissionError._errorAlreadySent = true;
2674
2828
  throw permissionError;
2675
2829
  }
2676
- // 模型不可用:累计计数,本次切换到 baseAgentModel 立即重试,不让用户看到失败
2677
- if (classifyError(retryError) === ErrorType.MODEL_UNAVAILABLE && !appliedTriggerModelOverride && evolcoreModelOverride?.model) {
2678
- fbState.failCount++;
2679
- if (fbState.failCount >= 2) {
2680
- fbState.fallbackActive = true;
2681
- fbState.messagesSinceFallback = 0;
2682
- fbState.nextProbeAt = Math.min(Math.pow(2, fbState.failCount - 1), 8);
2830
+ if (classifyError(retryError) === ErrorType.MODEL_UNAVAILABLE && !hasExplicitTriggerModel) {
2831
+ if (probingPrimaryModel && previousFallbackState) {
2832
+ const resumedFallback = previousFallbackState.activeFallback;
2833
+ retryScheduler.markModelAttempted(resumedFallback.model);
2834
+ modelOverride = withFallbackModel(modelOverride, resumedFallback);
2835
+ usedFallback = resumedFallback;
2836
+ skipEvolcoreModel = true;
2837
+ probingPrimaryModel = false;
2838
+ previousFallbackState.messagesSinceFallback = 0;
2839
+ previousFallbackState.nextProbeAt = Math.min(previousFallbackState.nextProbeAt * 2, 8);
2840
+ this.modelFallbackMap.set(session.id, previousFallbackState);
2841
+ await this.emitOperationalNotice(renderer, `原模型 ${primaryModel ?? '默认模型'} 仍不可用,继续使用备用模型 ${resumedFallback.model}`, 'warn', 'model-probe-fallback');
2842
+ await renderer.flush();
2843
+ runAttempt++;
2844
+ continue;
2683
2845
  }
2684
- this.modelFallbackMap.set(session.id, fbState);
2685
- logger.warn(`[ResponseEngine] Model unavailable: ${evolcoreModelOverride.model}, failCount=${fbState.failCount}, fallbackActive=${fbState.fallbackActive}`);
2686
- // 切换到 baseAgentModel 重试(清除 model/effort,让 runQuery 使用 this.model;
2687
- // 保留 permissionMode —— 它与模型无关,不能因模型降级而丢失)
2688
- modelOverride = {
2689
- ...(effectiveTriggerEffortOverride ? { effort: effectiveTriggerEffortOverride } : {}),
2690
- permissionMode: effectivePermissionMode,
2691
- ...(modelOverride?.sessionTitle ? { sessionTitle: modelOverride.sessionTitle } : {}),
2692
- ...(modelOverride?.turn ? { turn: modelOverride.turn } : {}),
2693
- };
2694
- usedFallback = true;
2695
- runAttempt++;
2696
- continue;
2846
+ const modelFallback = retryScheduler.afterModelUnavailable();
2847
+ if (modelFallback.kind === 'model_fallback') {
2848
+ modelOverride = withFallbackModel(modelOverride, modelFallback.candidate);
2849
+ usedFallback = modelFallback.candidate;
2850
+ skipEvolcoreModel = true;
2851
+ if (primaryModel) {
2852
+ this.modelFallbackMap.set(session.id, {
2853
+ baseagent: normalizedBaseagent.canonical,
2854
+ primaryModel,
2855
+ activeFallback: modelFallback.candidate,
2856
+ messagesSinceFallback: 0,
2857
+ nextProbeAt: 2,
2858
+ hintShown: false,
2859
+ });
2860
+ }
2861
+ logger.warn(`[ResponseEngine] Model unavailable: switching ${primaryModel ?? '<runner-default>'} to ${modelFallback.candidate.model} (${modelFallback.attempt}/${modelFallback.candidateCount})`);
2862
+ await this.emitOperationalNotice(renderer, `模型 ${primaryModel ?? '默认模型'} 不可用,正在切换备用模型 ${modelFallback.candidate.model}(${modelFallback.attempt}/${modelFallback.candidateCount})`, 'warn', 'model-fallback');
2863
+ await renderer.flush();
2864
+ runAttempt++;
2865
+ continue;
2866
+ }
2867
+ this.modelFallbackMap.delete(session.id);
2868
+ const candidateHint = fallbackCandidates.length === 0
2869
+ ? '未配置可用备用模型'
2870
+ : '备用模型均不可用';
2871
+ throw markModelFallbackExhausted(new Error(`model_not_found: ${primaryModel ?? 'runner_default'}`), `当前模型 ${primaryModel ?? '默认模型'} 不可用,${candidateHint}。请稍后重试;管理员可在 agents/defaults.json 的 baseagents.${normalizedBaseagent.canonical}.fallbackModels 配置同类备用模型。`);
2697
2872
  }
2698
2873
  if (retryAttemptTimedOut || isRetryableError(retryError)) {
2699
- if (didRetryMakeProgress(retryError)) {
2700
- consecutiveRetryFailures = 0;
2701
- }
2702
2874
  await recordRetryHealthError(retryError);
2703
- if (consecutiveRetryFailures < MAX_RETRIES) {
2875
+ const apiRetry = retryScheduler.afterTransientFailure(didRetryMakeProgress(retryError));
2876
+ if (apiRetry.kind === 'api_retry') {
2704
2877
  // 检查中断状态:如果任务已被中断(/stop 或新消息),立即退出重试循环
2705
2878
  const interruptReason = this.interruptedSessions.get(session.id);
2706
2879
  if (interruptReason) {
2707
2880
  logger.info(`[ResponseEngine] Task interrupted during retry wait (reason=${interruptReason}), aborting retry loop session=${session.id}`);
2708
2881
  throw new Error('TASK_INTERRUPTED');
2709
2882
  }
2710
- const delay = RETRY_DELAYS_MS[consecutiveRetryFailures];
2711
- const nextRetryNumber = consecutiveRetryFailures + 1;
2712
- consecutiveRetryFailures++;
2713
- logger.warn(`[ResponseEngine] Retryable error (attempt ${runAttempt}, consecutive ${consecutiveRetryFailures}/${MAX_RETRIES}), retrying in ${delay}ms:`, retryError);
2714
- await this.emitOperationalNotice(renderer, `API 不可用,${delay / 1000}秒后重试 ${nextRetryNumber}/${MAX_RETRIES}`, 'warn', 'retry');
2883
+ logger.warn(`[ResponseEngine] Retryable error (attempt ${runAttempt}, api retry ${apiRetry.attempt}/${apiRetry.maxAttempts}), retrying in ${apiRetry.delayMs}ms:`, retryError);
2884
+ await this.emitOperationalNotice(renderer, `API 不可用,${apiRetry.delayMs / 1000}秒后重试 ${apiRetry.attempt}/${apiRetry.maxAttempts}`, 'warn', 'retry');
2715
2885
  await renderer.flush();
2716
- await new Promise(resolve => setTimeout(resolve, delay));
2886
+ const retryDelayElapsed = await this.waitForRetryDelay(session.id, apiRetry.delayMs);
2887
+ if (!retryDelayElapsed) {
2888
+ logger.info(`[ResponseEngine] Task interrupted during retry delay session=${session.id}`);
2889
+ throw new Error('TASK_INTERRUPTED');
2890
+ }
2717
2891
  // 延迟后再次检查中断状态(延迟期间可能收到中断信号)
2718
2892
  const postDelayInterrupt = this.interruptedSessions.get(session.id);
2719
2893
  if (postDelayInterrupt) {
@@ -2723,7 +2897,7 @@ export class ResponseEngine {
2723
2897
  runAttempt++;
2724
2898
  continue;
2725
2899
  }
2726
- markRetryExhausted(retryError, MAX_RETRIES);
2900
+ markRetryExhausted(retryError, retryScheduler.apiRetryAttemptsMade);
2727
2901
  }
2728
2902
  throw retryError; // 不可重试或已耗尽重试次数
2729
2903
  }
@@ -2734,7 +2908,7 @@ export class ResponseEngine {
2734
2908
  }
2735
2909
  catch (error) {
2736
2910
  if (classifyError(error) === ErrorType.CONTEXT_TOO_LONG && session.agentSessionId && canCompactAgent(agent)) {
2737
- streamResult = await this.retryAfterContextRecovery('上下文已自动压缩,请继续之前未完成的任务。', {
2911
+ streamResult = await this.recoverFromContextLimit(streamResult, {
2738
2912
  streamKey,
2739
2913
  renderer,
2740
2914
  agent,
@@ -2775,7 +2949,7 @@ export class ResponseEngine {
2775
2949
  const isPromptTooLong = streamResult.isError && !!session.agentSessionId && !!compactAgent
2776
2950
  && streamHitContextLimit(streamResult);
2777
2951
  if (isPromptTooLong) {
2778
- streamResult = await this.retryAfterContextRecovery('上下文已自动压缩,请继续之前未完成的任务。', {
2952
+ streamResult = await this.recoverFromContextLimit(streamResult, {
2779
2953
  streamKey,
2780
2954
  renderer,
2781
2955
  agent,
@@ -2833,7 +3007,7 @@ export class ResponseEngine {
2833
3007
  subtype: 'protocol_incomplete',
2834
3008
  terminalReason: 'protocol_incomplete',
2835
3009
  protocolIncompleteReason: reason,
2836
- errors: [`Agent 协议未闭合(${reason}),任务未提交`],
3010
+ errors: [formatProtocolReplayExhaustedMessage(reason, protocolReplayAttempts)],
2837
3011
  lastReplyText: '',
2838
3012
  fullText: '',
2839
3013
  hasReceivedText: false,
@@ -2994,13 +3168,12 @@ export class ResponseEngine {
2994
3168
  this.agentDelegationRegistry?.revokeTask(session.id, taskId);
2995
3169
  logger.info(`[ResponseEngine] session ${session.id} processing cleared task=${taskId}`);
2996
3170
  // 降级模型回复末尾追加标记(代码层硬注入,不依赖模型输出)
2997
- const usingFallback = usedFallback || (skipEvolcoreModel && agentModel != null);
2998
- if (usingFallback && agentModel) {
3171
+ if (usedFallback) {
2999
3172
  const curFbState = this.modelFallbackMap.get(session.id);
3000
3173
  const showHint = curFbState && curFbState.nextProbeAt >= 8 && !curFbState.hintShown;
3001
3174
  const suffix = showHint
3002
- ? `\n\n---\n⚠️ [降级模型: ${agentModel} | 可告诉我"帮我检查可用模型"来诊断]`
3003
- : `\n\n---\n⚠️ [降级模型: ${agentModel}]`;
3175
+ ? `\n\n---\n⚠️ [降级模型: ${usedFallback.model} | 可告诉我"帮我检查可用模型"来诊断]`
3176
+ : `\n\n---\n⚠️ [降级模型: ${usedFallback.model}]`;
3004
3177
  renderer.addText(suffix);
3005
3178
  if (showHint && curFbState) {
3006
3179
  curFbState.hintShown = true;
@@ -3016,7 +3189,8 @@ export class ResponseEngine {
3016
3189
  // Flush 剩余内容(文件标记已在 flush 时自动移除)
3017
3190
  await renderer.flush(true);
3018
3191
  }
3019
- if (!streamResult.isError) {
3192
+ const terminalInterrupt = isExpectedTerminalInterrupt(streamResult.terminalReason);
3193
+ if (!streamResult.isError && !terminalInterrupt) {
3020
3194
  const commitDecision = await this.turnCoordinator.commit(session, turnLease, this.buildTurnCommitEvidence(session, streamResult));
3021
3195
  if (!commitDecision.ok) {
3022
3196
  logger.info(`[ResponseEngine] Turn commit rejected after flush: session=${session.id} task=${taskId} reason=${commitDecision.reason}`);
@@ -3033,13 +3207,14 @@ export class ResponseEngine {
3033
3207
  }
3034
3208
  }
3035
3209
  else {
3036
- await this.turnCoordinator.finish(session, turnLease, 'failed', streamResult.protocolIncompleteReason || streamResult.terminalReason || streamResult.subtype);
3210
+ await this.turnCoordinator.finish(session, turnLease, terminalInterrupt ? 'cancelled' : 'failed', streamResult.protocolIncompleteReason || streamResult.terminalReason || streamResult.subtype);
3037
3211
  }
3038
3212
  // 注意:不在此处清除 interruptedSessions,由下一条消息的 prompt 包装逻辑消费
3039
3213
  const interruptReason = this.interruptedSessions.get(session.id);
3040
- if (streamResult.isError) {
3214
+ if (streamResult.isError || terminalInterrupt) {
3041
3215
  // Agent 流正常结束但任务结果失败(权限被拒、max turns、工具链失败等)
3042
- const errorSummary = streamResult.errors?.join('; ') || '任务执行失败';
3216
+ const errorSummary = streamResult.errors?.join('; ')
3217
+ || (terminalInterrupt ? '用户交互未确认,任务已暂停' : '任务执行失败');
3043
3218
  const userErrorSummary = streamResult.subtype === 'protocol_incomplete'
3044
3219
  ? errorSummary
3045
3220
  : streamHitContextLimit(streamResult)
@@ -3050,10 +3225,30 @@ export class ResponseEngine {
3050
3225
  // 用户主动打断(新消息/​/stop/​撤回)会让 SDK 流在工具调用中途被掐断,
3051
3226
  // 末尾 result message 形状异常并被标记为 error(含 SDK 内部 ede_diagnostic 串)。
3052
3227
  // 这不是真正的失败,不应把诊断串暴露给用户,也不计入错误统计。
3053
- const isExpectedInterrupt = isNonErrorInterruptReason(interruptReason);
3228
+ const isExpectedInterrupt = isNonErrorInterruptReason(interruptReason)
3229
+ || isExpectedTerminalInterrupt(streamResult.terminalReason);
3054
3230
  if (!isExpectedInterrupt && !permissionTerminalFailure) {
3055
- await adapter.send(envelope, { kind: 'result.error', text: userErrorSummary, reason: rawSubtype }).catch(() => { });
3056
- await adapter.send(envelope, { kind: 'status.error', metadata: { errorType: rawSubtype } }).catch(() => { });
3231
+ const terminalMetadata = {
3232
+ errorType: rawSubtype,
3233
+ message: userErrorSummary,
3234
+ durationMs: Date.now() - startTime,
3235
+ numTurns: streamResult.numTurns,
3236
+ toolCallCount: streamResult.toolUseCount ?? 0,
3237
+ tokenUsage: streamResult.tokenUsage,
3238
+ contextUsage: streamResult.contextUsage,
3239
+ contextRecoveryAttempts: streamResult.contextRecoveryAttempts ?? 0,
3240
+ };
3241
+ // DaemonChannel settles a trigger when it receives a terminal
3242
+ // payload. Send the metadata-bearing status first so failed runs do
3243
+ // not lose their tool counts and recovery diagnostics.
3244
+ if (message.source === 'trigger') {
3245
+ await adapter.send(envelope, { kind: 'status.error', metadata: terminalMetadata }).catch(() => { });
3246
+ await adapter.send(envelope, { kind: 'result.error', text: userErrorSummary, reason: rawSubtype }).catch(() => { });
3247
+ }
3248
+ else {
3249
+ await adapter.send(envelope, { kind: 'result.error', text: userErrorSummary, reason: rawSubtype }).catch(() => { });
3250
+ await adapter.send(envelope, { kind: 'status.error', metadata: terminalMetadata }).catch(() => { });
3251
+ }
3057
3252
  this.touchAgentActivity(channelKey);
3058
3253
  }
3059
3254
  if (isExpectedInterrupt) {
@@ -3061,7 +3256,15 @@ export class ResponseEngine {
3061
3256
  // 这里不再补发 task:error(否则同一次打断被记两遍且错误归类为 error)。
3062
3257
  // 仅记 info 日志收尾。注意:task:interrupted 已填充 interruptedSessions,
3063
3258
  // stats 侧已据此收尾任务生命周期,无需在此重复发事件。
3064
- logger.info(`[${message.channel}] Stream result error suppressed (user interrupt: ${interruptReason}): ${errorSummary}`);
3259
+ const interactionCancelled = isExpectedTerminalInterrupt(streamResult.terminalReason)
3260
+ && !isNonErrorInterruptReason(interruptReason);
3261
+ if (interactionCancelled) {
3262
+ await adapter.send(envelope, {
3263
+ kind: 'status.interrupted',
3264
+ metadata: { reason: 'interaction_cancelled' },
3265
+ }).catch(() => { });
3266
+ }
3267
+ logger.info(`[${message.channel}] Stream result error suppressed (${interactionCancelled ? 'interaction cancellation' : `user interrupt: ${interruptReason}`}): ${errorSummary}`);
3065
3268
  logger.message({
3066
3269
  msgId: messageId,
3067
3270
  sessionId: session.id,
@@ -3070,7 +3273,7 @@ export class ResponseEngine {
3070
3273
  error: errorSummary,
3071
3274
  terminalReason: streamResult.terminalReason
3072
3275
  });
3073
- await this.publishTriggerExecutionInterrupted(message, adapter, envelope, interruptReason ?? 'interrupted', taskCausation);
3276
+ await this.publishTriggerExecutionInterrupted(message, adapter, envelope, interactionCancelled ? 'interaction_cancelled' : (interruptReason ?? 'interrupted'), taskCausation);
3074
3277
  }
3075
3278
  else {
3076
3279
  logger.error(`[ResponseEngine] Task failed: session=${session.id} task=${taskId} ` +
@@ -3439,6 +3642,7 @@ export class ResponseEngine {
3439
3642
  // 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送错误提示
3440
3643
  // processEventStream 已通过 renderer 发过错误时也跳过
3441
3644
  const retryExhaustedCount = getRetryExhaustedCount(error);
3645
+ const modelFallbackExhaustedMessage = getModelFallbackExhaustedMessage(error);
3442
3646
  if (isUserInterrupt) {
3443
3647
  logger.info(`[ResponseEngine] User interrupt by new_message, skip sending error message`);
3444
3648
  }
@@ -3450,11 +3654,13 @@ export class ResponseEngine {
3450
3654
  const idleSec = getLastIdleSec?.() || 0;
3451
3655
  const userMessage = retryExhaustedCount
3452
3656
  ? formatRetryableErrorFinalMessage(error, retryExhaustedCount)
3453
- : isTotalExecutionTimeout
3454
- ? `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`
3455
- : isTimeout
3456
- ? (idleSec > 0 ? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断` : '⚠️ 任务超时,已自动中断')
3457
- : getErrorMessage(error, undefined);
3657
+ : modelFallbackExhaustedMessage
3658
+ ? modelFallbackExhaustedMessage
3659
+ : isTotalExecutionTimeout
3660
+ ? `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`
3661
+ : isTimeout
3662
+ ? (idleSec > 0 ? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断` : '⚠️ 任务超时,已自动中断')
3663
+ : getErrorMessage(error, undefined);
3458
3664
  // 获取 session 用于话题回复(如果 resolveSession 已执行)
3459
3665
  let sendOpts;
3460
3666
  try {
@@ -3471,10 +3677,32 @@ export class ResponseEngine {
3471
3677
  const errorPayload = {
3472
3678
  kind: 'result.error',
3473
3679
  text: userMessage,
3474
- reason: retryExhaustedCount ? 'retry_exhausted' : isTotalExecutionTimeout ? 'total_execution_timeout' : isTimeout ? 'timeout' : errType,
3680
+ reason: retryExhaustedCount ? 'retry_exhausted' : modelFallbackExhaustedMessage ? 'model_unavailable' : isTotalExecutionTimeout ? 'total_execution_timeout' : isTimeout ? 'timeout' : errType,
3475
3681
  };
3476
- await adapter.send({ ...envelope, replyContext: sendOpts }, errorPayload);
3477
- if (!isUserInterrupt && daemonTrigger) {
3682
+ if (!isUserInterrupt && daemonTrigger && procStatus === 'error') {
3683
+ // DaemonChannel settles a trigger on the first terminal payload. Put
3684
+ // the complete failure metadata first so the audit retains tool
3685
+ // counts and context-recovery diagnostics even for thrown errors
3686
+ // (for example when compact itself fails).
3687
+ await adapter.send({ ...envelope, replyContext: sendOpts }, {
3688
+ kind: 'status.error',
3689
+ metadata: {
3690
+ errorType: errType,
3691
+ message: userMessage,
3692
+ durationMs: Date.now() - startTime,
3693
+ numTurns: streamResult.numTurns,
3694
+ toolCallCount: streamResult.toolUseCount ?? 0,
3695
+ tokenUsage: streamResult.tokenUsage,
3696
+ contextUsage: streamResult.contextUsage,
3697
+ contextRecoveryAttempts: streamResult.contextRecoveryAttempts ?? 0,
3698
+ },
3699
+ }).catch(() => { });
3700
+ await adapter.send({ ...envelope, replyContext: sendOpts }, errorPayload);
3701
+ }
3702
+ else {
3703
+ await adapter.send({ ...envelope, replyContext: sendOpts }, errorPayload);
3704
+ }
3705
+ if (!isUserInterrupt && daemonTrigger && procStatus !== 'error') {
3478
3706
  await adapter.send({ ...envelope, replyContext: sendOpts }, statusPayload).catch(() => { });
3479
3707
  }
3480
3708
  // Proactive 可观测:catch 块的基础设施错误也透传为 thought,保证按 task_id 聚合完整
@@ -3574,9 +3802,14 @@ export class ResponseEngine {
3574
3802
  return undefined;
3575
3803
  const recordedAutoCompactTokens = row.auto_compact_tokens ?? undefined;
3576
3804
  const inferredAutoCompactTokens = autoCompactTokensFromMaxTokens(row.max_tokens ?? undefined);
3577
- const autoCompactTokens = recordedAutoCompactTokens && recordedAutoCompactTokens > 0
3805
+ const candidateAutoCompactTokens = recordedAutoCompactTokens && recordedAutoCompactTokens > 0
3578
3806
  ? recordedAutoCompactTokens
3579
3807
  : inferredAutoCompactTokens ?? autoCompactWindowForModel(model);
3808
+ // Historic rows may contain the old fixed 350k threshold. Never let a
3809
+ // persisted value exceed the context window that accompanied it.
3810
+ const autoCompactTokens = inferredAutoCompactTokens
3811
+ ? Math.min(candidateAutoCompactTokens, inferredAutoCompactTokens)
3812
+ : candidateAutoCompactTokens;
3580
3813
  return { totalTokens, autoCompactTokens };
3581
3814
  }
3582
3815
  finally {
@@ -3693,6 +3926,7 @@ export class ResponseEngine {
3693
3926
  resultText: result.lastReplyText || result.fullText,
3694
3927
  inputTokens: result.tokenUsage?.input_tokens ?? 0,
3695
3928
  outputTokens: result.tokenUsage?.output_tokens ?? 0,
3929
+ hasTokenUsage: hasRecordedTokenUsage(result),
3696
3930
  };
3697
3931
  }
3698
3932
  /**
@@ -4106,7 +4340,8 @@ export class ResponseEngine {
4106
4340
  // 上下文过长的错误不在此处输出 notice,留给外层 isPromptTooLong 触发 auto-compact
4107
4341
  const isContextError = isContextTooLongText(event.error || '');
4108
4342
  const isRetryableRuntimeError = isRetryableError(new Error(event.error || ''));
4109
- if (!isContextError && !isRetryableRuntimeError && !hasErrorResult && !shouldSuppress()) {
4343
+ const isRecoverableModelError = classifyError(new Error(event.error || '')) === ErrorType.MODEL_UNAVAILABLE;
4344
+ if (!isContextError && !isRetryableRuntimeError && !isRecoverableModelError && !hasErrorResult && !shouldSuppress()) {
4110
4345
  hasErrorResult = true;
4111
4346
  renderer.addNotice(getRuntimeErrorMessage(event.error || '任务执行失败', false), 'warn', 'runtime-error', true);
4112
4347
  }
@@ -4115,7 +4350,7 @@ export class ResponseEngine {
4115
4350
  // SDK 可能产生多个 complete 事件(如 subagent 或 auto-compact 二次查询),
4116
4351
  // 仅记录状态,最终 flush(true) 在流结束后统一执行
4117
4352
  if (event.type === 'complete') {
4118
- const isAbort = event.terminalReason === 'aborted_streaming' || event.terminalReason === 'aborted_tools';
4353
+ const isAbort = isExpectedTerminalInterrupt(event.terminalReason);
4119
4354
  logger.info(`[ResponseEngine] ${isAbort ? 'task interrupted' : 'complete event'}: queryFinal=${event.queryFinal !== false} isError=${event.isError} terminalReason=${event.terminalReason ?? 'none'} subtype=${event.subtype ?? 'none'} hasReceivedText=${hasReceivedText}`);
4120
4355
  // 自动回填会话名称
4121
4356
  if (event.sessionTitle && shouldAutoFillSessionTitle(session.name, session.threadId)) {
@@ -4132,7 +4367,8 @@ export class ResponseEngine {
4132
4367
  // 但用户主动中断(新消息打断 或 /stop 命令)时不显示错误提示
4133
4368
  // 上下文过长的错误留给外层 isPromptTooLong 触发 auto-compact,不在此处输出
4134
4369
  const interruptReason = this.interruptedSessions.get(session.id);
4135
- const isUserInterrupt = isNonErrorInterruptReason(interruptReason);
4370
+ const isUserInterrupt = isNonErrorInterruptReason(interruptReason)
4371
+ || isExpectedTerminalInterrupt(event.terminalReason);
4136
4372
  const isContextTooLong = event.terminalReason === 'prompt_too_long'
4137
4373
  || isContextTooLongText(event.errors?.join(' ') || '')
4138
4374
  || isContextTooLongText(lastReplyText);
@@ -4140,10 +4376,13 @@ export class ResponseEngine {
4140
4376
  const isRetryableCompleteError = event.isError && !isContextTooLong && completeErrorText
4141
4377
  ? isRetryableError(new Error(completeErrorText))
4142
4378
  : false;
4379
+ const isRecoverableModelError = event.isError && !isContextTooLong && completeErrorText
4380
+ ? classifyError(new Error(completeErrorText)) === ErrorType.MODEL_UNAVAILABLE
4381
+ : false;
4143
4382
  if (event.isError && completeErrorText && isPendingTextSameAsStreamError(renderer.getRemainingText(), completeErrorText)) {
4144
4383
  renderer.discardPendingText();
4145
4384
  }
4146
- if (event.isError && !hasErrorResult && !shouldSuppress() && !isUserInterrupt && !isContextTooLong && !isRetryableCompleteError) {
4385
+ if (event.isError && !hasErrorResult && !shouldSuppress() && !isUserInterrupt && !isContextTooLong && !isRetryableCompleteError && !isRecoverableModelError) {
4147
4386
  // 使用 terminalReason 提供更友好的错误提示(不带 emoji,由 formatter 统一加)
4148
4387
  const userFriendlyMessage = event.terminalReason === 'prompt_too_long'
4149
4388
  ? getContextTooLongHint(agent)