evolcore 0.0.1 → 0.0.3

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 (379) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/LICENSE +21 -0
  3. package/MIGRATION-0.5.0.md +378 -0
  4. package/README.md +318 -14
  5. package/ROLE_ACCESS_CONTROL.md +174 -0
  6. package/assets/.env.template +4 -0
  7. package/bin/ec-safe-output.js +161 -0
  8. package/bin/ec.js +29 -0
  9. package/dist/agents/baseagent.js +163 -0
  10. package/dist/agents/claude-runner.js +2565 -0
  11. package/dist/agents/codex-app-server-client.js +448 -0
  12. package/dist/agents/codex-runner.js +2682 -0
  13. package/dist/agents/gemini-runner.js +666 -0
  14. package/dist/agents/runner-types.js +75 -0
  15. package/dist/aun/aid/agentmd.js +216 -0
  16. package/dist/aun/aid/client.js +132 -0
  17. package/dist/aun/aid/control-aid.js +91 -0
  18. package/dist/aun/aid/identity.js +518 -0
  19. package/dist/aun/aid/index.js +4 -0
  20. package/dist/aun/aid/store.js +74 -0
  21. package/dist/aun/aid/types.js +1 -0
  22. package/dist/aun/aid/validation.js +21 -0
  23. package/dist/aun/group-identity.js +10 -0
  24. package/dist/aun/msg/group-index.js +6 -0
  25. package/dist/aun/msg/group.js +1231 -0
  26. package/dist/aun/msg/history.js +123 -0
  27. package/dist/aun/msg/index.js +5 -0
  28. package/dist/aun/msg/p2p.js +393 -0
  29. package/dist/aun/msg/payload-type.js +27 -0
  30. package/dist/aun/msg/upload.js +137 -0
  31. package/dist/aun/outbox.js +168 -0
  32. package/dist/aun/rpc/caller.js +42 -0
  33. package/dist/aun/rpc/connection.js +25 -0
  34. package/dist/aun/rpc/index.js +2 -0
  35. package/dist/aun/service-proxy.js +225 -0
  36. package/dist/aun/storage/download.js +29 -0
  37. package/dist/aun/storage/index.js +3 -0
  38. package/dist/aun/storage/manage.js +10 -0
  39. package/dist/aun/storage/upload.js +68 -0
  40. package/dist/channels/aun.js +4164 -0
  41. package/dist/channels/contact-bind-code.js +134 -0
  42. package/dist/channels/daemon.js +422 -0
  43. package/dist/channels/dingtalk.js +1479 -0
  44. package/dist/channels/feishu.js +1865 -0
  45. package/dist/channels/qqbot.js +409 -0
  46. package/dist/channels/wechat.js +817 -0
  47. package/dist/channels/wecom-card.js +101 -0
  48. package/dist/channels/wecom-onboarding.js +82 -0
  49. package/dist/channels/wecom-state.js +191 -0
  50. package/dist/channels/wecom.js +1157 -0
  51. package/dist/cli/agent-command.js +642 -0
  52. package/dist/cli/agent.js +1059 -0
  53. package/dist/cli/aun-commands.js +2003 -0
  54. package/dist/cli/bench.js +1228 -0
  55. package/dist/cli/cli-argv.js +66 -0
  56. package/dist/cli/code-stats.js +329 -0
  57. package/dist/cli/command-log.js +82 -0
  58. package/dist/cli/config-selector.js +69 -0
  59. package/dist/cli/config.js +261 -0
  60. package/dist/cli/contact.js +71 -0
  61. package/dist/cli/ctl-command.js +62 -0
  62. package/dist/cli/daemon-commands.js +2750 -0
  63. package/dist/cli/fs-command.js +1447 -0
  64. package/dist/cli/handoff-command.js +302 -0
  65. package/dist/cli/help.js +35 -0
  66. package/dist/cli/index.js +374 -0
  67. package/dist/cli/init-channel.js +1372 -0
  68. package/dist/cli/init.js +590 -0
  69. package/dist/cli/link-rules.js +240 -0
  70. package/dist/cli/model.js +591 -0
  71. package/dist/cli/net-check.js +723 -0
  72. package/dist/cli/queue-command.js +150 -0
  73. package/dist/cli/raw-key-input.js +25 -0
  74. package/dist/cli/response.js +344 -0
  75. package/dist/cli/restart-monitor.js +480 -0
  76. package/dist/cli/stats.js +609 -0
  77. package/dist/cli/task-context.js +80 -0
  78. package/dist/cli/trigger-command.js +545 -0
  79. package/dist/cli/version.js +93 -0
  80. package/dist/cli/watch-logs.js +33 -0
  81. package/dist/cli/watch-msg.js +673 -0
  82. package/dist/config/boot-log.js +266 -0
  83. package/dist/config/builtin-role-templates.js +42 -0
  84. package/dist/config/builtin-roles.js +91 -0
  85. package/dist/config/config-batch-get.js +11 -0
  86. package/dist/config/config-field-policy.js +261 -0
  87. package/dist/config/config-manager.js +1120 -0
  88. package/dist/config/config-operation-service.js +384 -0
  89. package/dist/config/contact-alias.js +68 -0
  90. package/dist/config/contact-book-store.js +454 -0
  91. package/dist/config/contact-book-v2-startup.js +35 -0
  92. package/dist/config/contact-book.js +224 -0
  93. package/dist/config/contact-operation-service.js +110 -0
  94. package/dist/config/gateway-config.js +858 -0
  95. package/dist/config/lifecycle.js +17 -0
  96. package/dist/config/mention-mode.js +27 -0
  97. package/dist/config/merge.js +161 -0
  98. package/dist/config/owner-policy.js +4 -0
  99. package/dist/config/peer-role-resolver.js +218 -0
  100. package/dist/config/resolved-config-op.js +483 -0
  101. package/dist/config/role-config-v4-startup.js +32 -0
  102. package/dist/config/role-config-v5-startup.js +27 -0
  103. package/dist/config/role-ranks.js +18 -0
  104. package/dist/config/role-schema.js +105 -0
  105. package/dist/config/role-service.js +156 -0
  106. package/dist/config/role-store.js +215 -0
  107. package/dist/config/roles.js +64 -0
  108. package/dist/config/schema-registry.js +154 -0
  109. package/dist/config/snapshot.js +598 -0
  110. package/dist/config-store.js +501 -0
  111. package/dist/core/auth/agent-delegation.js +111 -0
  112. package/dist/core/auth/auth-gateway.js +166 -0
  113. package/dist/core/auth/authenticated-actor.js +6 -0
  114. package/dist/core/auth/authorization-audit.js +119 -0
  115. package/dist/core/auth/operation-authorizer.js +720 -0
  116. package/dist/core/auth/operation-catalog.js +731 -0
  117. package/dist/core/baseagent-loader.js +54 -0
  118. package/dist/core/bootstrap-service.js +175 -0
  119. package/dist/core/capability/capability-manager.js +316 -0
  120. package/dist/core/capability/providers/claude-capability-provider.js +176 -0
  121. package/dist/core/capability/providers/codex-capability-provider.js +148 -0
  122. package/dist/core/capability/providers/gemini-capability-provider.js +10 -0
  123. package/dist/core/capability/types.js +27 -0
  124. package/dist/core/causation/audit.js +103 -0
  125. package/dist/core/causation/aun-association.js +111 -0
  126. package/dist/core/causation/context.js +93 -0
  127. package/dist/core/causation/index.js +4 -0
  128. package/dist/core/causation/types.js +2 -0
  129. package/dist/core/channel-loader.js +277 -0
  130. package/dist/core/command/agent-control.js +616 -0
  131. package/dist/core/command/cli-intent-parser.js +225 -0
  132. package/dist/core/command/command-handler.js +1733 -0
  133. package/dist/core/command/connect-menu.js +374 -0
  134. package/dist/core/command/menu-handler.js +3452 -0
  135. package/dist/core/command/menu-protocol.js +247 -0
  136. package/dist/core/command/role-menu.js +1623 -0
  137. package/dist/core/command/slash-gate.js +148 -0
  138. package/dist/core/command/slash-handler.js +3066 -0
  139. package/dist/core/daemon-file-cache.js +222 -0
  140. package/dist/core/event-bus.js +32 -0
  141. package/dist/core/event-catalog.js +810 -0
  142. package/dist/core/evolagent-registry.js +545 -0
  143. package/dist/core/evolagent.js +342 -0
  144. package/dist/core/handoff/dispatcher.js +229 -0
  145. package/dist/core/handoff/mutex.js +46 -0
  146. package/dist/core/handoff/runtime.js +324 -0
  147. package/dist/core/handoff/store.js +537 -0
  148. package/dist/core/handoff/types.js +12 -0
  149. package/dist/core/inference/text-inference.js +173 -0
  150. package/dist/core/interaction-registration.js +10 -0
  151. package/dist/core/interaction-router.js +278 -0
  152. package/dist/core/message/create-status.js +67 -0
  153. package/dist/core/message/im-renderer.js +659 -0
  154. package/dist/core/message/items-formatter.js +76 -0
  155. package/dist/core/message/logical-queue-bridge.js +123 -0
  156. package/dist/core/message/message-bridge.js +874 -0
  157. package/dist/core/message/message-cache.js +56 -0
  158. package/dist/core/message/message-log.js +339 -0
  159. package/dist/core/message/message-processor.js +4 -0
  160. package/dist/core/message/message-queue.js +1446 -0
  161. package/dist/core/message/message-utils.js +76 -0
  162. package/dist/core/message/peer-mode.js +105 -0
  163. package/dist/core/message/pending-hints.js +232 -0
  164. package/dist/core/message/response-depth.js +33 -0
  165. package/dist/core/message/response-engine.js +4020 -0
  166. package/dist/core/message/response-snapshot.js +83 -0
  167. package/dist/core/message/send-receipt.js +24 -0
  168. package/dist/core/message/stream-debouncer.js +139 -0
  169. package/dist/core/message/stream-idle-monitor.js +124 -0
  170. package/dist/core/model/config-scope.js +162 -0
  171. package/dist/core/model/field-scope.js +78 -0
  172. package/dist/core/model/model-catalog.js +227 -0
  173. package/dist/core/model/model-diagnostics.js +182 -0
  174. package/dist/core/model/model-permission.js +90 -0
  175. package/dist/core/permission/approval-gateway.js +1017 -0
  176. package/dist/core/permission/ec-command-parser.js +347 -0
  177. package/dist/core/permission/execution-sandbox.js +16 -0
  178. package/dist/core/permission/index.js +6 -0
  179. package/dist/core/permission/mode.js +24 -0
  180. package/dist/core/permission/sandbox-runtime.js +265 -0
  181. package/dist/core/permission/tool-policy.js +1019 -0
  182. package/dist/core/permission/unix-socket-policy.js +99 -0
  183. package/dist/core/protected-paths.js +332 -0
  184. package/dist/core/relation/peer-identity.js +222 -0
  185. package/dist/core/relation/peer-key.js +1 -0
  186. package/dist/core/role/runtime-policy.js +141 -0
  187. package/dist/core/session/adapters/claude-session-file-adapter.js +218 -0
  188. package/dist/core/session/adapters/codex-session-file-adapter.js +333 -0
  189. package/dist/core/session/adapters/gemini-session-file-adapter.js +181 -0
  190. package/dist/core/session/session-file-adapter.js +7 -0
  191. package/dist/core/session/session-file-health.js +45 -0
  192. package/dist/core/session/session-fs-store.js +273 -0
  193. package/dist/core/session/session-key.js +24 -0
  194. package/dist/core/session/session-manager.js +1643 -0
  195. package/dist/core/session/session-mapper.js +100 -0
  196. package/dist/core/session/session-renew.js +314 -0
  197. package/dist/core/session/session-title.js +128 -0
  198. package/dist/core/session/session-turn-coordinator.js +205 -0
  199. package/dist/core/session/session-turns.js +67 -0
  200. package/dist/core/system-channels.js +29 -0
  201. package/dist/eck/baseagent-caps.js +18 -0
  202. package/dist/eck/detect.js +47 -0
  203. package/dist/eck/group-rules-sync.js +345 -0
  204. package/dist/eck/init.js +77 -0
  205. package/dist/eck/kit-renderer.js +359 -0
  206. package/dist/eck/manifest-engine.js +446 -0
  207. package/dist/eck/message-renderer.js +199 -0
  208. package/dist/eck/rules-loader.js +28 -0
  209. package/dist/index.js +2926 -4
  210. package/dist/ipc.js +777 -0
  211. package/dist/paths.js +262 -0
  212. package/dist/product.js +18 -0
  213. package/dist/response-system/context-builder.js +71 -0
  214. package/dist/response-system/coordinator.js +117 -0
  215. package/dist/response-system/decision-executor.js +86 -0
  216. package/dist/response-system/engines/v1/index.js +21 -0
  217. package/dist/response-system/engines/v1/interactive-flow.js +27 -0
  218. package/dist/response-system/engines/v1/proactive-flow.js +137 -0
  219. package/dist/response-system/engines/v1/types.js +1 -0
  220. package/dist/response-system/extensions.js +41 -0
  221. package/dist/response-system/index.js +6 -0
  222. package/dist/response-system/modes/index.js +7 -0
  223. package/dist/response-system/modes/single-session/index.js +72 -0
  224. package/dist/response-system/queues/fifo-queue.js +44 -0
  225. package/dist/response-system/queues/index.js +6 -0
  226. package/dist/response-system/queues/lifo-queue.js +42 -0
  227. package/dist/response-system/queues/priority-queue.js +63 -0
  228. package/dist/response-system/registry.js +97 -0
  229. package/dist/response-system/resolver.js +37 -0
  230. package/dist/response-system/selector.js +23 -0
  231. package/dist/response-system/types.js +7 -0
  232. package/dist/stats/billing.js +163 -0
  233. package/dist/stats/budget.js +93 -0
  234. package/dist/stats/db.js +403 -0
  235. package/dist/stats/eck-vars.js +89 -0
  236. package/dist/stats/index.js +11 -0
  237. package/dist/stats/normalizer.js +80 -0
  238. package/dist/stats/price-resolver.js +138 -0
  239. package/dist/stats/query.js +763 -0
  240. package/dist/stats/role-budget.js +168 -0
  241. package/dist/stats/writer.js +151 -0
  242. package/dist/trigger/anomaly-store.js +258 -0
  243. package/dist/trigger/audit.js +152 -0
  244. package/dist/trigger/event-source.js +119 -0
  245. package/dist/trigger/feedback.js +685 -0
  246. package/dist/trigger/history.js +290 -0
  247. package/dist/trigger/manager.js +294 -0
  248. package/dist/trigger/parser.js +595 -0
  249. package/dist/trigger/patch.js +155 -0
  250. package/dist/trigger/scheduler.js +1602 -0
  251. package/dist/trigger/script-executor.js +155 -0
  252. package/dist/trigger/state.js +145 -0
  253. package/dist/trigger/types.js +1 -0
  254. package/dist/trigger/validation.js +634 -0
  255. package/dist/types.js +12 -0
  256. package/dist/utils/aid-bind.js +313 -0
  257. package/dist/utils/atomic-write.js +95 -0
  258. package/dist/utils/avatar-upload.js +123 -0
  259. package/dist/utils/cross-platform.js +297 -0
  260. package/dist/utils/ecweb-utils.js +73 -0
  261. package/dist/utils/error-dict.json +153 -0
  262. package/dist/utils/error-utils.js +349 -0
  263. package/dist/utils/instance-registry.js +444 -0
  264. package/dist/utils/locale.js +21 -0
  265. package/dist/utils/log-writer.js +270 -0
  266. package/dist/utils/logger.js +89 -0
  267. package/dist/utils/markdown-to-plain-text.js +20 -0
  268. package/dist/utils/media-cache.js +274 -0
  269. package/dist/utils/model-prices.jsonl +20 -0
  270. package/dist/utils/npm-ops.js +210 -0
  271. package/dist/utils/process-introspect.js +133 -0
  272. package/dist/utils/process-tree-stats.js +271 -0
  273. package/dist/utils/project-path.js +74 -0
  274. package/dist/utils/restart-safety.js +31 -0
  275. package/dist/utils/stats.js +410 -0
  276. package/dist/utils/system-memory.js +62 -0
  277. package/dist/utils/tool-summary.js +284 -0
  278. package/dist/utils/welcome.js +268 -0
  279. package/kits/docs/GUIDE.md +20 -0
  280. package/kits/docs/INDEX.md +66 -0
  281. package/kits/docs/aun/CHEATSHEET.md +19 -0
  282. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
  283. package/kits/docs/channels/aun.md +65 -0
  284. package/kits/docs/channels/feishu.md +56 -0
  285. package/kits/docs/context-assembly.md +366 -0
  286. package/kits/docs/eck_templates/GUIDE.template.md +22 -0
  287. package/kits/docs/eck_templates/INDEX.template.md +28 -0
  288. package/kits/docs/eck_templates/path-registry.template.md +33 -0
  289. package/kits/docs/eck_templates/runtime.template.md +19 -0
  290. package/kits/docs/evolcore/INDEX.md +68 -0
  291. package/kits/docs/evolcore/agent.md +77 -0
  292. package/kits/docs/evolcore/aid.md +52 -0
  293. package/kits/docs/evolcore/config.md +149 -0
  294. package/kits/docs/evolcore/contact.md +57 -0
  295. package/kits/docs/evolcore/ctl.md +46 -0
  296. package/kits/docs/evolcore/event.md +216 -0
  297. package/kits/docs/evolcore/fs-architecture.md +1215 -0
  298. package/kits/docs/evolcore/fs.md +110 -0
  299. package/kits/docs/evolcore/group-fs.md +17 -0
  300. package/kits/docs/evolcore/group-rules.md +226 -0
  301. package/kits/docs/evolcore/group.md +150 -0
  302. package/kits/docs/evolcore/model.md +50 -0
  303. package/kits/docs/evolcore/msg.md +136 -0
  304. package/kits/docs/evolcore/response.md +75 -0
  305. package/kits/docs/evolcore/rpc.md +37 -0
  306. package/kits/docs/evolcore/self-summary.md +29 -0
  307. package/kits/docs/evolcore/stats.md +83 -0
  308. package/kits/docs/evolcore/storage.md +50 -0
  309. package/kits/docs/evolcore/trigger.md +539 -0
  310. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -0
  311. package/kits/docs/identity/PATH_OPS.md +16 -0
  312. package/kits/docs/identity/ROLE_DETAIL.md +23 -0
  313. package/kits/docs/identity/identity-tools.md +26 -0
  314. package/kits/docs/path-registry.md +43 -0
  315. package/kits/docs/prompt-loading-architecture.md +266 -0
  316. package/kits/docs/venues/aun-group.md +45 -0
  317. package/kits/docs/venues/aun-private.md +10 -0
  318. package/kits/docs/venues/client-desktop.md +10 -0
  319. package/kits/docs/venues/client-mobile.md +10 -0
  320. package/kits/docs/venues/feishu-group.md +13 -0
  321. package/kits/docs/venues/feishu-private.md +9 -0
  322. package/kits/docs/venues/group.md +25 -0
  323. package/kits/docs/venues/private.md +10 -0
  324. package/kits/eck_manifest.auxiliary.json +43 -0
  325. package/kits/eck_manifest.json +203 -0
  326. package/kits/eck_message_manifest.json +63 -0
  327. package/kits/migrations/README-role-config-v4.md +32 -0
  328. package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
  329. package/kits/migrations/migrate-role-config-v4.mjs +623 -0
  330. package/kits/migrations/migrate-role-config-v5.mjs +346 -0
  331. package/kits/migrations/rename-config-file.mjs +99 -0
  332. package/kits/rules/01-overview.md +142 -0
  333. package/kits/rules/02-navigation.md +76 -0
  334. package/kits/rules/03-identity.md +34 -0
  335. package/kits/rules/04-relation.md +59 -0
  336. package/kits/rules/05-venue.md +44 -0
  337. package/kits/rules/06-channel.md +59 -0
  338. package/kits/schemas/_meta.json +32 -0
  339. package/kits/schemas/agent-config.schema.1.json +177 -0
  340. package/kits/schemas/agent-config.schema.2.json +239 -0
  341. package/kits/schemas/agent-config.schema.3.json +119 -0
  342. package/kits/schemas/agent-config.schema.4.json +208 -0
  343. package/kits/schemas/agent-config.schema.5.json +326 -0
  344. package/kits/schemas/agent-config.schema.6.json +322 -0
  345. package/kits/schemas/contact-book.schema.1.json +36 -0
  346. package/kits/schemas/contact-book.schema.2.json +43 -0
  347. package/kits/schemas/daemon.schema.1.json +90 -0
  348. package/kits/schemas/defaults.schema.1.json +81 -0
  349. package/kits/schemas/menu-exec-schema-commands.md +208 -0
  350. package/kits/schemas/migrations/README.md +28 -0
  351. package/kits/schemas/relation-config.schema.1.json +158 -0
  352. package/kits/schemas/relation-config.schema.2.json +73 -0
  353. package/kits/schemas/relation-config.schema.3.json +50 -0
  354. package/kits/schemas/relation-config.schema.4.json +47 -0
  355. package/kits/schemas/relation-config.schema.5.json +47 -0
  356. package/kits/schemas/role-config.schema.1.json +201 -0
  357. package/kits/schemas/role-registry.schema.1.json +35 -0
  358. package/kits/schemas/single-session.schema.1.json +31 -0
  359. package/kits/templates/bootstrap-welcome.md +15 -0
  360. package/kits/templates/message-fragments/handoff-request-to-target.md +13 -0
  361. package/kits/templates/message-fragments/handoff-response-to-origin.md +10 -0
  362. package/kits/templates/message-fragments/inject-default.md +2 -0
  363. package/kits/templates/message-fragments/item.md +2 -0
  364. package/kits/templates/roles/admin.json +9 -0
  365. package/kits/templates/roles/member.json +41 -0
  366. package/kits/templates/roles/owner.json +9 -0
  367. package/kits/templates/roles/visitor.json +40 -0
  368. package/kits/templates/system-fragments/baseagent.md +14 -0
  369. package/kits/templates/system-fragments/bootstrap.md +16 -0
  370. package/kits/templates/system-fragments/channel.md +48 -0
  371. package/kits/templates/system-fragments/commands.md +28 -0
  372. package/kits/templates/system-fragments/identity.md +11 -0
  373. package/kits/templates/system-fragments/relation.md +19 -0
  374. package/kits/templates/system-fragments/session.md +53 -0
  375. package/kits/templates/system-fragments/venue.md +31 -0
  376. package/package.json +50 -15
  377. package/dist/index.d.ts +0 -7
  378. package/dist/index.d.ts.map +0 -1
  379. package/dist/index.js.map +0 -1
@@ -0,0 +1,4020 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import crypto from 'crypto';
5
+ import { BaseagentRunnerUnavailableError, hasCompact, hasClearSession, autoCompactWindowForModel, isClaudeContextUsageModel } from '../../agents/runner-types.js';
6
+ import { buildTaskRuntimeEnv } from '../../cli/task-context.js';
7
+ import { IMRenderer } from './im-renderer.js';
8
+ import { createTextInferenceProvider } from '../inference/text-inference.js';
9
+ import { StreamIdleMonitor } from './stream-idle-monitor.js';
10
+ import { logger } from '../../utils/logger.js';
11
+ import { isHostChinese } from '../../utils/locale.js';
12
+ import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, prefixErrorType, isRetryableError, isContextTooLongText } from '../../utils/error-utils.js';
13
+ import { isEvolcoreSendCommandForSession } from '../permission/ec-command-parser.js';
14
+ import { summarizeToolInput } from '../../utils/tool-summary.js';
15
+ import { getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
16
+ import { renderKitSections } from '../../eck/kit-renderer.js';
17
+ import { renderMessageBody } from '../../eck/message-renderer.js';
18
+ import { syncGroupRulesContext } from '../../eck/group-rules-sync.js';
19
+ import { consumeHints, hintsToSubMessages, composeHintFallback } from './pending-hints.js';
20
+ import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
21
+ import { recordCausationLink, recordCausationSpan } from '../causation/audit.js';
22
+ import { normalizeBaseagent } from '../../agents/baseagent.js';
23
+ import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
24
+ import { buildEnvelope } from './message-utils.js';
25
+ import { isSystemOrServicePeer, resolveChatMode } from './peer-mode.js';
26
+ import { AGENT_DELEGATION_TOKEN_ENV } from '../auth/agent-delegation.js';
27
+ import { hasTrustedPrincipal } from '../auth/authenticated-actor.js';
28
+ // Re-export 工具函数(向后兼容,让其他模块可以从 response-engine 导入)
29
+ export { buildEnvelope, sendInteractionPayload } from './message-utils.js';
30
+ import { constrainResolvedModelForRole } from '../model/model-permission.js';
31
+ import { constrainRuntimePermissionMode, resolveRuntimeStringField } from '../role/runtime-policy.js';
32
+ import { resolveEffective, resolveEffectiveFieldWithSource } from '../../config/config-manager.js';
33
+ import { dispatchToMentionMode } from '../../config/mention-mode.js';
34
+ import { authorizationConfigRevision, checkRoleAccess, getFirstStaticAgentOwner, listStaticAgentAdmins, listStaticAgentOwners, resolvePeerRoleDetail, roleToSessionIdentity, } from '../../config/peer-role-resolver.js';
35
+ import { insertUsageEvent, insertContextBreakdown, insertModelCalls } from '../../stats/writer.js';
36
+ import { normalizeUsage } from '../../stats/normalizer.js';
37
+ import { resolvePrices } from '../../stats/price-resolver.js';
38
+ import { getBudgetStatus } from '../../stats/budget.js';
39
+ import { formatUsageSubjectKey, getRoleBudgetStatus } from '../../stats/role-budget.js';
40
+ import { snapshot } from './response-snapshot.js';
41
+ import { ResponseModeCoordinator } from '../../response-system/coordinator.js';
42
+ import { ResponseModeRegistry } from '../../response-system/registry.js';
43
+ import { registerBuiltinModes } from '../../response-system/modes/index.js';
44
+ import { deriveSessionTitle, shouldAutoFillSessionTitle } from '../session/session-title.js';
45
+ import { SessionTurnCoordinator } from '../session/session-turn-coordinator.js';
46
+ import { recordTriggerExecutionAnomaly } from '../../trigger/anomaly-store.js';
47
+ function isShowActivitiesMode(value) {
48
+ return value === 'all' || value === 'text' || value === 'none';
49
+ }
50
+ function approvalActorRevision(actor, owners = [], admins = [], selfAid) {
51
+ if (!actor)
52
+ return undefined;
53
+ const assignment = actor.principalId && owners.includes(actor.principalId)
54
+ ? 'owner'
55
+ : actor.principalId && admins.includes(actor.principalId)
56
+ ? 'admin'
57
+ : 'none';
58
+ return crypto.createHash('sha256').update(JSON.stringify([
59
+ actor.operatorChannelKey,
60
+ actor.operatorChannelType,
61
+ actor.operatorId,
62
+ actor.principalStatus,
63
+ actor.principalId ?? null,
64
+ assignment,
65
+ selfAid ? authorizationConfigRevision(selfAid) : null,
66
+ ])).digest('hex');
67
+ }
68
+ /** Keep runner terminal fields internally consistent before any status routing. */
69
+ export function normalizeCompleteAgentEvent(event) {
70
+ const subtype = event.subtype?.trim().toLowerCase();
71
+ const terminalReason = event.terminalReason?.trim().toLowerCase();
72
+ const hasErrorText = event.errors?.some(error => typeof error === 'string' && error.trim().length > 0) === true;
73
+ const subtypeSignalsError = subtype === 'error' || subtype === 'failed' || subtype?.startsWith('error_') === true;
74
+ const isError = event.isError === true || hasErrorText || subtypeSignalsError;
75
+ const normalizedSubtype = isError && (!subtype || subtype === 'success' || subtype === 'completed')
76
+ ? 'error'
77
+ : event.subtype;
78
+ const normalizedTerminalReason = terminalReason === 'success' || terminalReason === 'completed'
79
+ ? undefined
80
+ : event.terminalReason;
81
+ if (event.isError === isError
82
+ && event.subtype === normalizedSubtype
83
+ && event.terminalReason === normalizedTerminalReason) {
84
+ return event;
85
+ }
86
+ return {
87
+ ...event,
88
+ isError,
89
+ subtype: normalizedSubtype,
90
+ terminalReason: normalizedTerminalReason,
91
+ };
92
+ }
93
+ const RETRY_EXHAUSTED_MARK = Symbol('evolcore.retryExhausted');
94
+ const RETRY_MADE_PROGRESS_MARK = Symbol('evolcore.retryMadeProgress');
95
+ const RETRY_HEALTH_RECORDED_MARK = Symbol('evolcore.retryHealthRecorded');
96
+ function markRetryExhausted(error, retries) {
97
+ if (error && typeof error === 'object') {
98
+ error[RETRY_EXHAUSTED_MARK] = retries;
99
+ // processEventStream may have already flushed the raw transient error as an
100
+ // activity notice. The exhausted-retry message is the real terminal state.
101
+ delete error._errorAlreadySent;
102
+ }
103
+ }
104
+ function getRetryExhaustedCount(error) {
105
+ const retries = error && typeof error === 'object'
106
+ ? error[RETRY_EXHAUSTED_MARK]
107
+ : undefined;
108
+ return typeof retries === 'number' ? retries : undefined;
109
+ }
110
+ function markRetryMadeProgress(error) {
111
+ if (error && typeof error === 'object') {
112
+ error[RETRY_MADE_PROGRESS_MARK] = true;
113
+ }
114
+ }
115
+ function didRetryMakeProgress(error) {
116
+ return !!(error && typeof error === 'object' && error[RETRY_MADE_PROGRESS_MARK] === true);
117
+ }
118
+ function markRetryHealthRecorded(error) {
119
+ if (error && typeof error === 'object') {
120
+ error[RETRY_HEALTH_RECORDED_MARK] = true;
121
+ }
122
+ }
123
+ function wasRetryHealthRecorded(error) {
124
+ return !!(error && typeof error === 'object' && error[RETRY_HEALTH_RECORDED_MARK] === true);
125
+ }
126
+ function formatRetryableErrorFinalMessage(error, retries) {
127
+ const reason = getErrorMessage(error, undefined, false) || 'API 暂时不可用';
128
+ return `❌ API 暂时不可用,已自动重试 ${retries} 次仍失败,任务已停止。\n原因:${reason}`;
129
+ }
130
+ function getStreamErrorText(result) {
131
+ return [
132
+ result.errors?.join('\n'),
133
+ result.lastReplyText,
134
+ result.fullText,
135
+ result.subtype,
136
+ result.terminalReason,
137
+ ]
138
+ .filter((part) => typeof part === 'string' && part.trim().length > 0)
139
+ .join('\n');
140
+ }
141
+ function getStreamErrorMessage(result, includeEmoji = false) {
142
+ const raw = getStreamErrorText(result) || '任务执行失败';
143
+ const mapped = getErrorMessage(new Error(raw), result.terminalReason, includeEmoji);
144
+ const generic = includeEmoji ? '❌ 处理消息时出错,请稍后重试' : '处理消息时出错,请稍后重试';
145
+ if (!result.terminalReason && raw && mapped === generic) {
146
+ return includeEmoji ? `❌ ${raw}` : raw;
147
+ }
148
+ return mapped;
149
+ }
150
+ function getRuntimeErrorMessage(raw, includeEmoji = false) {
151
+ const fallback = raw || '任务执行失败';
152
+ const mapped = getErrorMessage(new Error(fallback), undefined, includeEmoji);
153
+ const generic = includeEmoji ? '❌ 处理消息时出错,请稍后重试' : '处理消息时出错,请稍后重试';
154
+ if (mapped === generic)
155
+ return includeEmoji ? `❌ ${fallback}` : fallback;
156
+ return mapped;
157
+ }
158
+ function normalizeComparableText(text) {
159
+ return text.replace(/\s+/g, ' ').trim();
160
+ }
161
+ function isPendingTextSameAsStreamError(pendingText, errorText) {
162
+ const pending = normalizeComparableText(pendingText);
163
+ const error = normalizeComparableText(errorText);
164
+ if (!pending || !error || pending.length < 20)
165
+ return false;
166
+ return pending === error || pending.includes(error) || error.includes(pending);
167
+ }
168
+ function streamHitContextLimit(result) {
169
+ return result.terminalReason === 'prompt_too_long' ||
170
+ isContextTooLongText(result.lastReplyText) ||
171
+ isContextTooLongText(result.errors?.join(' ') || '') ||
172
+ isContextTooLongText(result.fullText);
173
+ }
174
+ const SHELL_CONTROL_RE = /[;&|`]|[$][(]|\r|\n/;
175
+ function isCtlQueueReadCommand(toolName, input) {
176
+ if (toolName !== 'Bash' && toolName !== 'Shell')
177
+ return false;
178
+ const command = typeof input?.command === 'string' ? input.command.trim() : '';
179
+ if (!command || SHELL_CONTROL_RE.test(command))
180
+ return false;
181
+ if (!/^(?:ec|evolcore)\s+ctl\s+queue(?:\s|$)/.test(command))
182
+ return false;
183
+ return !/(?:^|\s)--(?:clear|cancel|interrupt)(?:\s|$)/.test(command);
184
+ }
185
+ /** OS 信息在进程生命周期内是常量,模块加载时算一次。例: "Windows 11 Pro (win32 10.0.26200)" */
186
+ const OS_INFO = (() => {
187
+ let label = '';
188
+ try {
189
+ label = os.version();
190
+ }
191
+ catch { /* 旧 Node 无 os.version */ }
192
+ return `${label ? label + ' ' : ''}(${os.platform()} ${os.release()})`;
193
+ })();
194
+ /** 当前 UTC 偏移,格式 +08:00 / -05:00。每条消息算(DST 安全)。 */
195
+ function currentTzOffset() {
196
+ const off = -new Date().getTimezoneOffset(); // 分钟,东区为正
197
+ const sign = off >= 0 ? '+' : '-';
198
+ const abs = Math.abs(off);
199
+ return `${sign}${String(Math.floor(abs / 60)).padStart(2, '0')}:${String(abs % 60).padStart(2, '0')}`;
200
+ }
201
+ /** 当前本地日期 YYYY-MM-DD(按运行环境时区)。系统提示词用,一天才变一次(缓存友好)。 */
202
+ function currentLocalDate() {
203
+ const d = new Date();
204
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
205
+ }
206
+ /** 当前本地星期几(中文,如「星期四」)。 */
207
+ function currentWeekday() {
208
+ try {
209
+ return new Intl.DateTimeFormat('zh-CN', { weekday: 'long' }).format(new Date());
210
+ }
211
+ catch {
212
+ return ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'][new Date().getDay()];
213
+ }
214
+ }
215
+ function getContextTooLongHint(agent) {
216
+ if (canCompactAgent(agent)) {
217
+ return '上下文过长,请精简提问或使用 /compact 压缩上下文';
218
+ }
219
+ return '上下文过长,请精简提问,或使用 /new 新建会话后继续';
220
+ }
221
+ function getContextCompactFailedHint(agent) {
222
+ if (canCompactAgent(agent)) {
223
+ return '上下文过长,自动压缩失败,请手动输入 /compact 重试';
224
+ }
225
+ return '上下文过长,请精简提问,或使用 /new 新建会话后继续';
226
+ }
227
+ function canCompactAgent(agent) {
228
+ return hasCompact(agent) && agent.capabilities?.compact !== false;
229
+ }
230
+ function canClearAgent(agent) {
231
+ return hasClearSession(agent) && agent.capabilities?.clear !== false;
232
+ }
233
+ function autoCompactTokensFromMaxTokens(maxTokens) {
234
+ if (!maxTokens || maxTokens <= 0)
235
+ return undefined;
236
+ return maxTokens >= 1000000 ? maxTokens - 100000 : maxTokens;
237
+ }
238
+ /**
239
+ * 构造 OutboundEnvelope —— 出站三件套的信封部分。
240
+ *
241
+ * 用于所有走 adapter.send 的出站路径:
242
+ * - 任务流内的 IMRenderer 投影(chatmode 由会话决定)
243
+ * - 命令回显(MessageBridge.handleCommand,taskId 用合成 ID `cmd-...`)
244
+ * - 网关层系统通知(src/index.ts,taskId 用 `system-...` / `restart-...` 等便于 events.log 关联)
245
+ *
246
+ * 注意:
247
+ * - chatmode 缺省 `'interactive'`(系统通知 / 命令回显都属于同步交互);
248
+ * - timestamp 可由调用方注入(便于测试),缺省 `Date.now()`。
249
+ */
250
+ /**
251
+ * 统一消息处理器
252
+ * 负责处理来自不同渠道的消息,协调事件流处理
253
+ */
254
+ export class ResponseEngine {
255
+ sessionManager;
256
+ globalSettings;
257
+ messageCache;
258
+ eventBus;
259
+ commandHandler;
260
+ channels = new Map();
261
+ channelTypeMap = new Map(); // channelType → channelName(首个实例)
262
+ currentRenderer;
263
+ shouldSuppressActivities = false;
264
+ agentMap;
265
+ primaryRunnerKey;
266
+ interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
267
+ timeoutErrors = new Map();
268
+ /** sessionId → 模型降级状态(带退避探测,进程重启清零) */
269
+ modelFallbackMap = new Map();
270
+ interactionRouter;
271
+ messageQueue;
272
+ handoffRuntime;
273
+ /** sessionId → 活跃的空闲监控器,用于等待用户交互期间暂停/恢复计时 */
274
+ activeMonitors = new Map();
275
+ /** sessionId → 当前正在处理任务的运行时上下文,供 in-task CLI 通过 IPC 查询。 */
276
+ activeTaskRuntimeContexts = new Map();
277
+ triggerTerminalMessages = new WeakSet();
278
+ agentDelegationRegistry;
279
+ turnCoordinator;
280
+ /** 响应模式协调器(插件化机制中枢)。内置模式在构造时注册。 */
281
+ responseCoordinator;
282
+ /**
283
+ * Get the runner for a given (channel, baseagent) pair.
284
+ *
285
+ * - `channel` is used to look up the owning EvolAgent (via registry).
286
+ * - `baseagent` (e.g. 'claude') comes from `session.baseagent`.
287
+ *
288
+ * Falls back only when the channel is not owned by a known EvolAgent. If the
289
+ * owner is known but its requested baseagent runner is missing, this is a
290
+ * session/config mismatch and must not silently route to a different backend.
291
+ */
292
+ getAgent(channel, baseagent, selfAID) {
293
+ if (selfAID && baseagent) {
294
+ const key = `${selfAID}::${baseagent}`;
295
+ if (this.agentMap.has(key))
296
+ return this.agentMap.get(key);
297
+ }
298
+ if (channel && baseagent) {
299
+ const owner = this.agentRegistry?.resolveByChannel(channel);
300
+ const evolName = owner?.name || '<unknown>';
301
+ const key = `${evolName}::${baseagent}`;
302
+ if (this.agentMap.has(key))
303
+ return this.agentMap.get(key);
304
+ const singleRunnerKey = `<unknown>::${baseagent}`;
305
+ if (this.agentMap.has(singleRunnerKey))
306
+ return this.agentMap.get(singleRunnerKey);
307
+ if (owner) {
308
+ throw new BaseagentRunnerUnavailableError(evolName, baseagent, this.getAvailableBaseagentsForOwner(evolName));
309
+ }
310
+ }
311
+ if (this.agentMap.has(this.primaryRunnerKey))
312
+ return this.agentMap.get(this.primaryRunnerKey);
313
+ return this.agentMap.values().next().value;
314
+ }
315
+ getTextInferenceProvider(channel, baseagent, selfAID) {
316
+ if (!baseagent)
317
+ return undefined;
318
+ const aid = selfAID || (channel ? this.agentRegistry?.resolveByChannel(channel)?.aid : undefined);
319
+ if (!aid)
320
+ return undefined;
321
+ const config = resolveEffective({ self: aid }, { cache: true, expand: true });
322
+ return createTextInferenceProvider(baseagent, config);
323
+ }
324
+ getAvailableBaseagentsForOwner(evolName) {
325
+ const prefix = `${evolName}::`;
326
+ return [...this.agentMap.keys()]
327
+ .filter(key => key.startsWith(prefix))
328
+ .map(key => key.slice(prefix.length));
329
+ }
330
+ async sendBaseagentMismatch(channelKey, channelId, session, error, replyContext) {
331
+ const channelInfo = this.resolveChannelInfo(channelKey);
332
+ if (!channelInfo)
333
+ return;
334
+ const available = error.availableBaseagents.length ? error.availableBaseagents.join(', ') : '(none)';
335
+ const text = `❌ 当前会话绑定的 baseagent 不可用: ${error.baseagent}\nAgent: ${error.evolagentName}\n可用: ${available}\n请使用 /baseagent 切换到可用后端。`;
336
+ const sessionReplyContext = { ...(replyContext ?? {}), sessionId: session.id };
337
+ await channelInfo.adapter.send(buildEnvelope({
338
+ taskId: `system-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`,
339
+ sessionId: session.id,
340
+ channel: channelKey,
341
+ channelId,
342
+ agentName: error.evolagentName,
343
+ chatmode: this.resolveEffectiveChatmodeForSession(session, channelKey, channelId),
344
+ replyContext: sessionReplyContext,
345
+ }), { kind: 'system.error', text, subtype: 'baseagent_unavailable', recoverable: true });
346
+ }
347
+ resolveEffectiveChatmodeForSession(session, channelKey, channelId) {
348
+ const peerType = session.metadata?.peerType;
349
+ try {
350
+ const self = session.selfAID || this.agentRegistry?.resolveByChannel(channelKey)?.aid;
351
+ const channelType = session.channelType || channelKey.split('#')[0] || channelKey;
352
+ const peerKeyId = session.chatType === 'group'
353
+ ? (session.metadata?.groupId || channelId)
354
+ : (session.metadata?.peerId || channelId);
355
+ const peerKey = channelType && peerKeyId ? formatPeerKey(channelType, peerKeyId) : undefined;
356
+ return resolveChatMode({
357
+ self: self || undefined,
358
+ peerKey,
359
+ role: session.identity?.role,
360
+ chatType: session.chatType,
361
+ peerType,
362
+ });
363
+ }
364
+ catch {
365
+ return 'interactive';
366
+ }
367
+ }
368
+ /** 获取可用 agent 列表 */
369
+ getAvailableAgents() {
370
+ return [...this.agentMap.keys()];
371
+ }
372
+ /** 判断是否为后台会话(仅主会话参与判断,话题会话独立) */
373
+ isBackgroundSession(session, _channel, _channelId) {
374
+ if (session.threadId)
375
+ return false;
376
+ // 使用 session 自身的 channelType 精确定位 active.json,避免扫描误匹配
377
+ const active = this.sessionManager.getActiveSessionSync(session.channel, session.channelId, session.channelType, session.selfAID);
378
+ return active ? session.id !== active.id : false;
379
+ }
380
+ constructor(agentRunnerOrMap, sessionManager, globalSettings, messageCache, eventBus, commandHandler, primaryRunnerKey) {
381
+ this.sessionManager = sessionManager;
382
+ this.globalSettings = globalSettings;
383
+ this.messageCache = messageCache;
384
+ this.eventBus = eventBus;
385
+ this.commandHandler = commandHandler;
386
+ this.turnCoordinator = new SessionTurnCoordinator(sessionManager);
387
+ if (agentRunnerOrMap instanceof Map) {
388
+ this.agentMap = agentRunnerOrMap;
389
+ this.primaryRunnerKey = primaryRunnerKey || '<unknown>::claude';
390
+ }
391
+ else {
392
+ // 测试 / 单 runner 路径:占位 agent name 用 '<unknown>'
393
+ this.agentMap = new Map([[`<unknown>::${agentRunnerOrMap.name}`, agentRunnerOrMap]]);
394
+ this.primaryRunnerKey = `<unknown>::${agentRunnerOrMap.name}`;
395
+ }
396
+ // 监听中断事件,标记被中断的 session
397
+ this.eventBus.subscribe('task:interrupted', (event) => {
398
+ if ('sessionId' in event && event.sessionId) {
399
+ const reason = (event.reason || 'new_message');
400
+ this.turnCoordinator.invalidate(event.sessionId, reason);
401
+ this.interruptedSessions.set(event.sessionId, reason);
402
+ this.agentDelegationRegistry?.revokeSession(event.sessionId);
403
+ }
404
+ });
405
+ // 初始化响应模式协调器,注册内置模式(interactive/proactive)
406
+ const registry = new ResponseModeRegistry();
407
+ registerBuiltinModes(registry);
408
+ this.responseCoordinator = new ResponseModeCoordinator(registry);
409
+ }
410
+ setInteractionRouter(router) {
411
+ this.interactionRouter = router;
412
+ // 等待用户交互期间暂停 idle 监控,应答/取消/超时后恢复——
413
+ // 避免把「正在等用户点按钮」误判为「任务卡死」而中断任务。
414
+ router.setWaitHooks({
415
+ onWaitStart: (sessionId) => {
416
+ this.activeMonitors.get(sessionId)?.pause();
417
+ },
418
+ onWaitEnd: (sessionId) => {
419
+ this.activeMonitors.get(sessionId)?.resume();
420
+ },
421
+ });
422
+ }
423
+ setMessageQueue(queue) {
424
+ this.messageQueue = queue;
425
+ }
426
+ async interruptSession(sessionId, reason) {
427
+ const session = await this.sessionManager.getSessionById(sessionId);
428
+ if (!session)
429
+ return;
430
+ this.turnCoordinator.hydrate(session);
431
+ const interruptedTurn = this.turnCoordinator.invalidate(sessionId, reason);
432
+ const interruptedTaskId = interruptedTurn?.active?.taskId;
433
+ this.interruptedSessions.set(sessionId, reason);
434
+ await this.interactionRouter?.cancelAll(sessionId, reason);
435
+ const channelKey = session.metadata?.channelKey || session.channel;
436
+ const agent = this.getAgent(channelKey, session.baseagent, session.selfAID);
437
+ const terminalStatus = (() => {
438
+ // TimeoutController owns timeout delivery (including user-facing timeout
439
+ // details) and already guarantees a single terminal status. Sending it
440
+ // here as well would race that barrier and duplicate status.timeout.
441
+ if (!interruptedTaskId || reason === 'timeout')
442
+ return Promise.resolve();
443
+ const channelInfo = this.resolveChannelInfo(channelKey);
444
+ if (!channelInfo)
445
+ return Promise.resolve();
446
+ const envelope = buildEnvelope({
447
+ taskId: interruptedTaskId,
448
+ sessionId: session.id,
449
+ channel: channelInfo.adapter.channelName,
450
+ channelId: session.channelId,
451
+ agentName: agent.name,
452
+ chatmode: this.resolveEffectiveChatmodeForSession(session, channelKey, session.channelId),
453
+ replyContext: {
454
+ ...(session.metadata?.replyContext ?? {}),
455
+ sessionId: session.id,
456
+ threadId: session.threadId || session.metadata?.replyContext?.threadId,
457
+ },
458
+ });
459
+ const payload = {
460
+ kind: 'status.interrupted',
461
+ metadata: { reason },
462
+ };
463
+ return channelInfo.adapter.send(envelope, payload).then(() => undefined).catch(error => {
464
+ logger.debug(`[ResponseEngine] interrupt terminal status failure: session=${sessionId} task=${interruptedTaskId} error=${error instanceof Error ? error.message : String(error)}`);
465
+ });
466
+ })();
467
+ const runnerInterrupt = agent.interrupt(sessionId).catch(error => {
468
+ logger.debug(`[ResponseEngine] interrupt barrier runner failure: session=${sessionId} error=${error instanceof Error ? error.message : String(error)}`);
469
+ });
470
+ await Promise.all([terminalStatus, runnerInterrupt]);
471
+ const taskId = (await this.turnCoordinator.completeInterrupt(session, reason)) ?? interruptedTaskId;
472
+ if (taskId) {
473
+ this.sessionManager.clearProcessingIfTask(sessionId, taskId);
474
+ if (this.activeTaskRuntimeContexts.get(sessionId)?.taskId === taskId) {
475
+ this.activeTaskRuntimeContexts.delete(sessionId);
476
+ }
477
+ this.agentDelegationRegistry?.revokeTask(sessionId, taskId);
478
+ }
479
+ }
480
+ setHandoffRuntime(runtime) {
481
+ this.handoffRuntime = runtime;
482
+ }
483
+ getTaskRuntimeContext(sessionId) {
484
+ return this.activeTaskRuntimeContexts.get(sessionId) ?? null;
485
+ }
486
+ setAgentDelegationRegistry(registry) {
487
+ this.agentDelegationRegistry = registry;
488
+ }
489
+ async returnHandoffResult(params) {
490
+ if (!this.handoffRuntime) {
491
+ return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff runtime not configured' };
492
+ }
493
+ if (!params.sessionId)
494
+ return { ok: false, code: 'HANDOFF_ID_REQUIRED', error: 'current session is required' };
495
+ if (this.agentDelegationRegistry) {
496
+ const delegation = this.agentDelegationRegistry.validate(params.delegationToken, params.sessionId);
497
+ if (!delegation.ok)
498
+ return { ok: false, code: delegation.code, error: delegation.reason };
499
+ }
500
+ const runtime = this.activeTaskRuntimeContexts.get(params.sessionId);
501
+ const session = await this.sessionManager.getSessionById(params.sessionId);
502
+ const selfAid = runtime?.selfAid || session?.selfAID;
503
+ if (!selfAid)
504
+ return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'self agent not found' };
505
+ return this.handoffRuntime.returnHandoff({
506
+ selfAid,
507
+ currentSessionId: params.sessionId,
508
+ handoffId: params.handoffId,
509
+ currentTaskHandoffIds: runtime?.handoffIds,
510
+ content: params.content,
511
+ causation: runtime?.causation,
512
+ });
513
+ }
514
+ agentRegistry;
515
+ setAgentRegistry(registry) {
516
+ this.agentRegistry = registry;
517
+ }
518
+ /** 更新 EvolAgent.lastActivity —— 每次发出 status.* 事件(含 progress)时调用 */
519
+ touchAgentActivity(channelKey) {
520
+ const owning = this.agentRegistry?.resolveByChannel(channelKey);
521
+ if (owning)
522
+ owning.lastActivity = Date.now();
523
+ }
524
+ inferPrimaryBaseagent() {
525
+ const idx = this.primaryRunnerKey.lastIndexOf('::');
526
+ return idx >= 0 ? this.primaryRunnerKey.slice(idx + 2) : this.primaryRunnerKey || undefined;
527
+ }
528
+ ensureSessionBaseagent(session, fallback) {
529
+ const legacyAgentId = typeof session.agentId === 'string' ? session.agentId : undefined;
530
+ if (!session.baseagent) {
531
+ session.baseagent = fallback || legacyAgentId || this.inferPrimaryBaseagent();
532
+ }
533
+ }
534
+ inferDirectSessionIdentity(message, owningAgent) {
535
+ const actorId = message.peerId || message.channelId;
536
+ if (!actorId)
537
+ return undefined;
538
+ const channel = message.channel;
539
+ const parts = channel.split('#');
540
+ const parsedType = parts.length >= 3 ? parts[0] : undefined;
541
+ const parsedSelfAid = parts.length >= 3 ? parts[1] : undefined;
542
+ const selfAid = message.selfAID || owningAgent?.aid || parsedSelfAid;
543
+ const channelType = message.channelType || parsedType || channel;
544
+ const chatType = message.chatType || 'private';
545
+ if (selfAid) {
546
+ const detail = resolvePeerRoleDetail({
547
+ selfAid,
548
+ channelKey: channel,
549
+ channelType,
550
+ chatType,
551
+ actorId,
552
+ conversationId: chatType === 'group' ? message.channelId : actorId,
553
+ peerType: message.peerType,
554
+ });
555
+ return roleToSessionIdentity(detail.effectiveRole);
556
+ }
557
+ if (!this.agentRegistry)
558
+ return undefined;
559
+ const registryRoles = this.agentRegistry;
560
+ const agentRoles = owningAgent;
561
+ if (registryRoles.isOwner?.(channel, actorId) || agentRoles?.isOwner?.(channel, actorId)) {
562
+ return { role: 'owner', mode: 'interactive' };
563
+ }
564
+ if (registryRoles.isAdmin?.(channel, actorId) || agentRoles?.isAdmin?.(channel, actorId)) {
565
+ return { role: 'admin', mode: 'interactive' };
566
+ }
567
+ return undefined;
568
+ }
569
+ getAgentContext(channelName, chatType) {
570
+ if (!this.agentRegistry)
571
+ return null;
572
+ const agent = this.agentRegistry.resolveByChannel(channelName);
573
+ if (!agent)
574
+ return null;
575
+ // chatmode 解析优先级:agent.config.chatmode > globalSettings.chatmode
576
+ const globalCm = agent.config?.chatmode ?? this.globalSettings.chatmode;
577
+ return agent.getContext(channelName, chatType, globalCm);
578
+ }
579
+ /**
580
+ * 观察者插话(v0.3):消费当前 (对端, thread) 的待用提示,转成 owner-hint SubMessage。
581
+ * 一次性语义:consumeHints 回放算有效集后清该 thread(其它 thread 残留则保留,否则删文件)。
582
+ * 仅 aun 渠道(pending-hints 落在 sessions/aun/<self>/<对端>/)。
583
+ */
584
+ consumeOwnerHints(session, message) {
585
+ const channelType = session.channelType || message.channelType || session.channel;
586
+ if (channelType !== 'aun')
587
+ return [];
588
+ const selfAID = session.selfAID || message.selfAID;
589
+ if (!selfAID)
590
+ return [];
591
+ // 会话定位键:私聊=对端 AID,群聊=groupId(均为 session.channelId)。
592
+ const peerChannelId = session.channelId;
593
+ if (!peerChannelId)
594
+ return [];
595
+ try {
596
+ const hints = consumeHints(resolvePaths().sessionsDir, 'aun', peerChannelId, selfAID, session.threadId);
597
+ if (hints.length === 0)
598
+ return [];
599
+ logger.info(`[ResponseEngine] consumed ${hints.length} owner-hint(s) for ${peerChannelId} thread=${session.threadId || 'main'}`);
600
+ return hintsToSubMessages(hints);
601
+ }
602
+ catch (e) {
603
+ logger.warn(`[ResponseEngine] consumeOwnerHints failed: ${e instanceof Error ? e.message : String(e)}`);
604
+ return [];
605
+ }
606
+ }
607
+ /**
608
+ * 注册渠道适配器
609
+ */
610
+ registerChannel(adapter, policy, options) {
611
+ this.channels.set(adapter.channelName, { adapter, options, policy });
612
+ // 维护 channelType → channelName 映射(首个实例优先)
613
+ const type = options?.channelType || adapter.channelName;
614
+ if (!this.channelTypeMap.has(type)) {
615
+ this.channelTypeMap.set(type, adapter.channelName);
616
+ }
617
+ }
618
+ /**
619
+ * 注销渠道适配器(热重载断开渠道时调用,避免遗留死实例)。
620
+ * channelTypeMap 若指向被删实例,重定向到同类型的另一存活实例(无则删除映射)。
621
+ */
622
+ unregisterChannel(channelName) {
623
+ const info = this.channels.get(channelName);
624
+ this.channels.delete(channelName);
625
+ const type = info?.options?.channelType || channelName;
626
+ if (this.channelTypeMap.get(type) === channelName) {
627
+ this.channelTypeMap.delete(type);
628
+ // 重定向到同类型的另一存活实例(保持按类型名路由可用)
629
+ for (const [name, ci] of this.channels) {
630
+ if ((ci.options?.channelType || name) === type) {
631
+ this.channelTypeMap.set(type, name);
632
+ break;
633
+ }
634
+ }
635
+ }
636
+ }
637
+ /**
638
+ * 获取渠道适配器(支持实例名和 channelType)
639
+ */
640
+ getAdapter(channelName) {
641
+ return this.resolveChannelInfo(channelName)?.adapter;
642
+ }
643
+ /**
644
+ * 获取渠道信息(含 policy,支持实例名和 channelType)
645
+ */
646
+ getChannelInfo(channelName) {
647
+ return this.resolveChannelInfo(channelName);
648
+ }
649
+ /**
650
+ * 处理 compact 开始事件
651
+ */
652
+ handleCompactStart(sessionId) {
653
+ if (sessionId) {
654
+ this.eventBus.publish({ type: 'runner:compact-start', sessionId });
655
+ }
656
+ if (this.currentRenderer && !this.shouldSuppressActivities) {
657
+ this.currentRenderer.addNotice('\u23f3 会话压缩中...', 'info', 'compact-start', true);
658
+ }
659
+ }
660
+ async retryAfterContextRecovery(prompt, opts) {
661
+ const { streamKey, renderer, agent, session, absoluteProjectPath, effectiveSystemPrompt, modelOverride, runtimeEnv, resetTimer, shouldSuppress, proactive, turnLease, } = opts;
662
+ if (!session.agentSessionId || !canCompactAgent(agent)) {
663
+ throw new Error('CONTEXT_COMPACT_FAILED');
664
+ }
665
+ renderer.addNotice('上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
666
+ await renderer.flush();
667
+ const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
668
+ if (compacted) {
669
+ renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
670
+ const retryStream = await agent.runQuery(session.id, prompt, absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
671
+ agent.registerStream(streamKey, retryStream);
672
+ return await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress, proactive, undefined, // 重试分支不调插件钩子
673
+ undefined, turnLease);
674
+ }
675
+ renderer.addNotice('⚠️ 压缩失败,尝试清空会话历史后重试...', 'warn', 'compact-failed-clear', true);
676
+ await renderer.flush();
677
+ if (!canClearAgent(agent)) {
678
+ throw new Error('CONTEXT_COMPACT_FAILED');
679
+ }
680
+ const previousAgentSessionId = session.agentSessionId;
681
+ let cleared = false;
682
+ try {
683
+ cleared = await agent.clearSession(session.id, previousAgentSessionId, absoluteProjectPath);
684
+ }
685
+ catch (error) {
686
+ logger.warn(`[ResponseEngine] clearSession failed after compact failure: ${error}`);
687
+ }
688
+ if (!cleared) {
689
+ throw new Error('CONTEXT_COMPACT_FAILED');
690
+ }
691
+ session.agentSessionId = undefined;
692
+ await this.sessionManager.updateSession(session.id, { agentSessionId: null });
693
+ renderer.addNotice('✅ 会话已清空,继续处理...', 'info', 'clear-retry', true);
694
+ const retryStream = await agent.runQuery(session.id, '会话历史已清空,请继续之前未完成的任务。', absoluteProjectPath, undefined, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
695
+ agent.registerStream(streamKey, retryStream);
696
+ return await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress, proactive, undefined, // 重试分支不调插件钩子
697
+ undefined, turnLease);
698
+ }
699
+ /**
700
+ * 根据 channel 标识查找渠道信息
701
+ * 先按实例名精确匹配,再按 channelType 映射到实例名
702
+ */
703
+ resolveChannelInfo(channel) {
704
+ // 1. 精确匹配实例名
705
+ let info = this.channels.get(channel);
706
+ if (info)
707
+ return info;
708
+ // 2. 按 channelType 查找(兼容按类型名路由)
709
+ const instanceName = this.channelTypeMap.get(channel);
710
+ if (instanceName)
711
+ info = this.channels.get(instanceName);
712
+ return info;
713
+ }
714
+ messageChannelKey(message, session) {
715
+ return message.channel === 'daemon'
716
+ ? message.channel
717
+ : (session.metadata?.channelKey || message.channel);
718
+ }
719
+ isTrustedDaemonTrigger(message) {
720
+ return message.channel === 'daemon'
721
+ && message.source === 'trigger'
722
+ && !!message.triggerMeta?.triggerId;
723
+ }
724
+ resolveTriggerExecutionIdentity(message, selfAid) {
725
+ if (message.source !== 'trigger' || !message.triggerMeta?.triggerId)
726
+ return undefined;
727
+ const actorId = message.triggerMeta.originPeerId;
728
+ const channelType = message.triggerMeta.originChannelType;
729
+ const originChannelKey = message.triggerMeta.originChannelKey;
730
+ if (!selfAid || !actorId || !channelType || !originChannelKey)
731
+ return undefined;
732
+ const control = message.triggerMeta.originControl === true;
733
+ let actor;
734
+ let resolvedRole = 'none';
735
+ try {
736
+ const detail = resolvePeerRoleDetail({
737
+ selfAid,
738
+ channelKey: originChannelKey,
739
+ channelType,
740
+ chatType: 'private',
741
+ actorId,
742
+ conversationId: actorId,
743
+ });
744
+ actor = detail.actor;
745
+ resolvedRole = detail.effectiveRole || 'none';
746
+ }
747
+ catch {
748
+ actor = {
749
+ operatorId: actorId,
750
+ operatorChannelKey: originChannelKey,
751
+ operatorChannelType: channelType,
752
+ principalStatus: 'error',
753
+ };
754
+ }
755
+ return {
756
+ role: control ? 'owner' : resolvedRole,
757
+ actor,
758
+ actorId,
759
+ channelType,
760
+ originChannelKey,
761
+ control,
762
+ peerKey: formatPeerKey(channelType, actorId),
763
+ };
764
+ }
765
+ resolveHandoffOriginExecutionIdentity(message, selfAid) {
766
+ const authorization = message.handoffDelivery?.originAuthorization;
767
+ if (message.source !== 'handoff'
768
+ || message.handoffDelivery?.direction !== 'origin'
769
+ || !authorization
770
+ || !selfAid) {
771
+ return undefined;
772
+ }
773
+ try {
774
+ const peer = parsePeerKey(authorization.peerKey);
775
+ if (peer.channelType !== authorization.channelType)
776
+ return undefined;
777
+ const conversationId = authorization.chatType === 'group'
778
+ ? peer.channelId
779
+ : authorization.actorId;
780
+ const detail = resolvePeerRoleDetail({
781
+ selfAid,
782
+ channelKey: authorization.channelKey,
783
+ channelType: authorization.channelType,
784
+ chatType: authorization.chatType,
785
+ actorId: authorization.actorId,
786
+ conversationId,
787
+ });
788
+ return {
789
+ role: detail.effectiveRole || 'none',
790
+ actor: detail.actor,
791
+ actorId: authorization.actorId,
792
+ channelType: authorization.channelType,
793
+ originChannelKey: authorization.channelKey,
794
+ peerKey: authorization.peerKey,
795
+ control: false,
796
+ };
797
+ }
798
+ catch {
799
+ return undefined;
800
+ }
801
+ }
802
+ claimTriggerTerminal(message) {
803
+ const trigger = message.triggerMeta;
804
+ if (message.source !== 'trigger' || !trigger?.triggerId || this.triggerTerminalMessages.has(message)) {
805
+ return undefined;
806
+ }
807
+ const runId = trigger.runId ?? message.messageId;
808
+ if (!runId)
809
+ return undefined;
810
+ this.triggerTerminalMessages.add(message);
811
+ return { trigger, runId };
812
+ }
813
+ /** Publish a terminal outcome before an early return from Trigger execution. */
814
+ publishTriggerExecutionFailure(message, error, opts = {}) {
815
+ const terminal = this.claimTriggerTerminal(message);
816
+ if (!terminal)
817
+ return;
818
+ const { trigger, runId } = terminal;
819
+ const attemptId = trigger.attemptId;
820
+ this.eventBus.publish({
821
+ type: 'trigger:failed',
822
+ triggerId: trigger.triggerId,
823
+ name: trigger.triggerName ?? '',
824
+ runId,
825
+ originTriggerId: trigger.triggerId,
826
+ messageId: opts.messageId ?? message.messageId ?? runId,
827
+ error,
828
+ targetChannel: message.channel,
829
+ targetChannelId: message.channelId,
830
+ fireTime: trigger.fireTime ?? 0,
831
+ phase: opts.phase ?? 'execute',
832
+ ...(attemptId ? { attemptId } : {}),
833
+ causation: opts.causation ?? message.causation,
834
+ });
835
+ }
836
+ publishTriggerExecutionSkipped(message, reason, causation) {
837
+ const terminal = this.claimTriggerTerminal(message);
838
+ if (!terminal)
839
+ return;
840
+ const { trigger, runId } = terminal;
841
+ this.eventBus.publish({
842
+ type: 'trigger:skipped',
843
+ triggerId: trigger.triggerId,
844
+ name: trigger.triggerName ?? '',
845
+ runId,
846
+ attemptId: trigger.attemptId,
847
+ originTriggerId: trigger.triggerId,
848
+ reason,
849
+ targetChannel: message.channel,
850
+ targetChannelId: message.channelId,
851
+ fireTime: trigger.fireTime,
852
+ causation: causation ?? message.causation,
853
+ });
854
+ }
855
+ /** Close the internal daemon conversation before returning from an interrupted Trigger turn. */
856
+ async publishTriggerExecutionInterrupted(message, adapter, envelope, reason, causation) {
857
+ if (this.isTrustedDaemonTrigger(message)) {
858
+ await adapter.send(envelope, {
859
+ kind: 'status.interrupted',
860
+ metadata: { reason },
861
+ }).catch(error => {
862
+ logger.warn(`[ResponseEngine] Failed to close interrupted Trigger run=${message.triggerMeta?.runId ?? '<unknown>'}: ${error instanceof Error ? error.message : String(error)}`);
863
+ });
864
+ }
865
+ this.publishTriggerExecutionSkipped(message, reason, causation);
866
+ }
867
+ publishTriggerExecutionCompleted(message, messageId, durationMs, causation) {
868
+ const terminal = this.claimTriggerTerminal(message);
869
+ if (!terminal)
870
+ return;
871
+ const { trigger, runId } = terminal;
872
+ this.eventBus.publish({
873
+ type: 'trigger:completed',
874
+ triggerId: trigger.triggerId,
875
+ name: trigger.triggerName ?? '',
876
+ runId,
877
+ attemptId: trigger.attemptId,
878
+ originTriggerId: trigger.triggerId,
879
+ messageId,
880
+ durationMs,
881
+ targetChannel: message.channel,
882
+ targetChannelId: message.channelId,
883
+ fireTime: trigger.fireTime ?? 0,
884
+ causation: causation ?? message.causation,
885
+ });
886
+ }
887
+ // 命令前缀列表(与 CommandHandler.quickCommandPrefixes 保持同步)
888
+ static COMMAND_PREFIXES = [
889
+ '/new', '/pwd', '/help', '/status', '/restart',
890
+ '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
891
+ '/stop', '/clear', '/compact', '/del', '/perm', '/file', '/check',
892
+ '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/observable', '/chatmode',
893
+ '/aid', '/upgrade', '/evolagent',
894
+ ];
895
+ /** 判断消息内容是否为已知命令 */
896
+ isKnownCommand(content) {
897
+ return content === '/s' ||
898
+ ResponseEngine.COMMAND_PREFIXES.some(cmd => content.startsWith(cmd));
899
+ }
900
+ /**
901
+ * 处理消息(主入口)
902
+ */
903
+ async processMessage(message) {
904
+ const idleMs = (this.globalSettings.idleMonitor?.timeout ?? 120) * 1000;
905
+ const totalExecutionMs = this.totalExecutionLimitMs();
906
+ // 先解析会话,再优先用 session.metadata.channelKey 精确定位实例级 adapter
907
+ // message.channel 现在存实例名(channelName),可直接用于精确路由
908
+ const { session, absoluteProjectPath } = await this.resolveSession(message);
909
+ const accessChannelKey = this.messageChannelKey(message, session);
910
+ const accessChannelInfo = this.resolveChannelInfo(accessChannelKey);
911
+ const accessAdapter = accessChannelInfo?.adapter;
912
+ const accessAdapterSelfAid = typeof accessAdapter?._selfAid === 'function' ? accessAdapter._selfAid() : undefined;
913
+ const selfAidForAccess = accessAdapterSelfAid || message.selfAID || session.selfAID || undefined;
914
+ const triggerExecutionIdentity = this.resolveTriggerExecutionIdentity(message, selfAidForAccess);
915
+ const handoffExecutionIdentity = this.resolveHandoffOriginExecutionIdentity(message, selfAidForAccess);
916
+ const executionIdentity = triggerExecutionIdentity ?? handoffExecutionIdentity;
917
+ if (message.source === 'trigger' && !triggerExecutionIdentity) {
918
+ logger.warn(`[ResponseEngine] Trigger execution identity missing: trigger=${message.triggerMeta?.triggerId ?? '<none>'}`);
919
+ const channelInfo = this.resolveChannelInfo(accessChannelKey);
920
+ if (channelInfo) {
921
+ await channelInfo.adapter.send({
922
+ taskId: `trigger-identity-missing-${Date.now()}`,
923
+ sessionId: session.id,
924
+ channel: accessChannelKey,
925
+ channelId: message.channelId,
926
+ agentName: 'evolcore',
927
+ chatmode: 'interactive',
928
+ replyContext: message.replyContext,
929
+ timestamp: Date.now(),
930
+ }, {
931
+ kind: 'system.error',
932
+ text: 'Trigger 创建者身份缺失,已拒绝执行',
933
+ subtype: 'trigger_identity_missing',
934
+ recoverable: false,
935
+ }).catch(() => { });
936
+ }
937
+ this.publishTriggerExecutionFailure(message, 'trigger_identity_missing');
938
+ return;
939
+ }
940
+ if (executionIdentity) {
941
+ session.identity = roleToSessionIdentity(executionIdentity.role);
942
+ }
943
+ // ── 角色访问控制检查:读取该用户角色的 allowAccess 配置,false 则拦截并回复权限不足 ──
944
+ const userRole = session.identity?.role || 'none';
945
+ const isInternalHandoff = message.source === 'handoff';
946
+ if (!isInternalHandoff && !checkRoleAccess(userRole, selfAidForAccess)) {
947
+ logger.warn(`[ResponseEngine] Access denied: role=${userRole} peerKey=${message.channelId} session=${session.id}`);
948
+ const channelKey = session.metadata?.channelKey || message.channel;
949
+ const channelInfo = this.resolveChannelInfo(channelKey);
950
+ if (channelInfo) {
951
+ try {
952
+ await channelInfo.adapter.send({
953
+ taskId: `access-denied-${Date.now()}`,
954
+ sessionId: session.id,
955
+ channel: channelKey,
956
+ channelId: message.channelId,
957
+ agentName: 'evolcore',
958
+ chatmode: 'interactive',
959
+ replyContext: message.replyContext,
960
+ timestamp: Date.now(),
961
+ }, {
962
+ kind: 'system.error',
963
+ text: '暂无权限访问本 agent,请联系 agent 管理员授权访问',
964
+ subtype: 'access_denied',
965
+ recoverable: false,
966
+ });
967
+ }
968
+ catch (err) {
969
+ logger.error(`[ResponseEngine] Failed to send access-denied message:`, err);
970
+ }
971
+ }
972
+ this.publishTriggerExecutionFailure(message, 'access_denied');
973
+ return;
974
+ }
975
+ // thread(feishu) pending strategy: inject replyContext so first reply creates the thread
976
+ if (message.triggerMeta?.pendingThread && message.triggerMeta?.rootMessageId) {
977
+ message.replyContext = {
978
+ ...(message.replyContext ?? {}),
979
+ replyToMessageId: message.triggerMeta.rootMessageId,
980
+ replyInThread: true,
981
+ };
982
+ }
983
+ const channelKey = this.messageChannelKey(message, session);
984
+ const channelInfo = this.resolveChannelInfo(channelKey);
985
+ if (!channelInfo) {
986
+ logger.error(`[ResponseEngine] Unknown channel: ${channelKey}`);
987
+ this.publishTriggerExecutionFailure(message, `unknown_channel:${channelKey}`);
988
+ return;
989
+ }
990
+ const { policy } = channelInfo;
991
+ const streamKey = session.id;
992
+ const chatType = message.chatType || 'private';
993
+ const identityRole = session.identity?.role || 'none';
994
+ const monitorEnabled = this.globalSettings.idleMonitor?.enabled !== false;
995
+ // 按 session.baseagent 选择 agent 后端(idle-kill 路径需要 interrupt)
996
+ let agent;
997
+ try {
998
+ agent = this.getAgent(channelKey, session.baseagent, session.selfAID || message.selfAID);
999
+ }
1000
+ catch (error) {
1001
+ if (error instanceof BaseagentRunnerUnavailableError) {
1002
+ logger.error(`[ResponseEngine] baseagent mismatch blocked: session=${session.id} channel=${channelKey} requested=${session.baseagent} owner=${error.evolagentName} available=${error.availableBaseagents.join(',') || '<none>'}`);
1003
+ await this.sendBaseagentMismatch(channelKey, message.channelId, session, error, message.replyContext);
1004
+ this.publishTriggerExecutionFailure(message, `baseagent_unavailable:${error.baseagent}`);
1005
+ return;
1006
+ }
1007
+ throw error;
1008
+ }
1009
+ // 计算是否抑制中间输出(工具活动 + 流式文本)。具体三态在 chatMode/effective config
1010
+ // 解析完成后赋值;闭包让后续事件处理路径保持兼容。
1011
+ const outputState = { middleOutputMode: 'all' };
1012
+ const shouldSuppress = () => {
1013
+ return outputState.middleOutputMode === 'none';
1014
+ };
1015
+ this.shouldSuppressActivities = shouldSuppress();
1016
+ let monitor;
1017
+ let monitorInterval;
1018
+ let totalExecutionTimer;
1019
+ let rejectFn;
1020
+ let lastIdleSec = 0;
1021
+ let timeoutTriggered = false;
1022
+ const timeoutControl = {
1023
+ claimedByInternal: false,
1024
+ terminalDelivered: false,
1025
+ claim: () => {
1026
+ if (!timeoutControl.error || this.interruptedSessions.get(session.id) !== 'timeout')
1027
+ return undefined;
1028
+ timeoutControl.claimedByInternal = true;
1029
+ return timeoutControl.error;
1030
+ },
1031
+ };
1032
+ const rejectAfterInterruptBarrier = (error) => {
1033
+ if (timeoutTriggered)
1034
+ return;
1035
+ timeoutTriggered = true;
1036
+ timeoutControl.error = error;
1037
+ this.timeoutErrors.set(session.id, error);
1038
+ this.turnCoordinator.hydrate(session);
1039
+ this.turnCoordinator.invalidate(session.id, 'timeout');
1040
+ this.interruptedSessions.set(session.id, 'timeout');
1041
+ const barrier = this.interruptSession(session.id, 'timeout')
1042
+ .catch(interruptError => {
1043
+ logger.debug(`[ResponseEngine] Timeout interrupt barrier failed: ${interruptError instanceof Error ? interruptError.message : String(interruptError)}`);
1044
+ });
1045
+ timeoutControl.barrier = barrier;
1046
+ void barrier.finally(() => {
1047
+ if (!timeoutControl.claimedByInternal)
1048
+ rejectFn(error);
1049
+ });
1050
+ };
1051
+ const triggerAttemptId = message.replyContext?.metadata?.triggerAttemptId;
1052
+ const resetTimer = (eventType, toolName) => {
1053
+ monitor?.recordEvent(eventType || 'unknown', toolName);
1054
+ if (typeof triggerAttemptId === 'string') {
1055
+ const activityRecorder = channelInfo.adapter.recordExecutionActivity;
1056
+ activityRecorder?.call(channelInfo.adapter, triggerAttemptId);
1057
+ }
1058
+ };
1059
+ // Cache background status to avoid async call inside setInterval
1060
+ const isBackground = this.isBackgroundSession(session, message.channel, message.channelId);
1061
+ const timeoutPromise = new Promise((_, reject) => {
1062
+ rejectFn = reject;
1063
+ if (!monitorEnabled)
1064
+ return;
1065
+ monitor = new StreamIdleMonitor(idleMs);
1066
+ this.activeMonitors.set(streamKey, monitor);
1067
+ monitorInterval = setInterval(() => {
1068
+ // Drain all pending levels in one tick
1069
+ let result = monitor.check();
1070
+ while (result) {
1071
+ if (result.action === 'kill') {
1072
+ lastIdleSec = result.idleSec;
1073
+ logger.warn(`[ResponseEngine] Idle monitor: kill after ${result.idleSec}s idle, stream: ${streamKey}`);
1074
+ this.eventBus.publish({ type: 'runner:idle-timeout', sessionId: streamKey, idleSec: result.idleSec });
1075
+ logger.info(`[ResponseEngine] timeout interrupt barrier invoked (idle-kill) stream=${streamKey}`);
1076
+ rejectAfterInterruptBarrier(new Error('SDK_TIMEOUT'));
1077
+ return;
1078
+ }
1079
+ else {
1080
+ // notify or warn: publish event, task continues
1081
+ logger.info(`[ResponseEngine] Idle monitor: ${result.action} after ${result.idleSec}s idle, stream: ${streamKey}`);
1082
+ this.eventBus.publish({
1083
+ type: result.action === 'notify' ? 'runner:idle-notify' : 'runner:idle-warn',
1084
+ sessionId: streamKey,
1085
+ idleSec: result.idleSec,
1086
+ totalEvents: result.state.totalEvents,
1087
+ totalToolCalls: result.state.totalToolCalls,
1088
+ lastToolName: result.state.lastToolName,
1089
+ });
1090
+ }
1091
+ result = monitor.check();
1092
+ }
1093
+ }, 30000);
1094
+ });
1095
+ const totalExecutionPromise = new Promise((_, reject) => {
1096
+ totalExecutionTimer = setTimeout(() => {
1097
+ logger.warn(`[ResponseEngine] Total execution timeout after ${totalExecutionMs}ms, stream: ${streamKey}`);
1098
+ rejectAfterInterruptBarrier(new Error('TOTAL_EXECUTION_TIMEOUT'));
1099
+ }, totalExecutionMs);
1100
+ totalExecutionTimer.unref?.();
1101
+ });
1102
+ try {
1103
+ const processingPromise = this._processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, () => lastIdleSec, outputState, timeoutControl);
1104
+ const guardedProcessingPromise = processingPromise.then(async () => {
1105
+ if (!timeoutControl.error)
1106
+ return;
1107
+ await timeoutControl.barrier;
1108
+ throw timeoutControl.error;
1109
+ });
1110
+ await Promise.race([
1111
+ guardedProcessingPromise,
1112
+ timeoutPromise,
1113
+ totalExecutionPromise,
1114
+ ]);
1115
+ }
1116
+ catch (error) {
1117
+ if (error instanceof Error && (error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT')) {
1118
+ await timeoutControl.barrier;
1119
+ if (!timeoutControl.terminalDelivered) {
1120
+ await timeoutControl.deliverUnhandled?.(error);
1121
+ }
1122
+ this.timeoutErrors.delete(session.id);
1123
+ }
1124
+ // 超时错误:kill 级别已发送诊断信息,无需再发
1125
+ // 非超时错误走通用处理
1126
+ // 记录错误到健康状态(复用已有 session)
1127
+ if (channelInfo) {
1128
+ try {
1129
+ const errorType = classifyError(error);
1130
+ // 上下文过长是可恢复错误,不累计错误计数
1131
+ if (errorType === ErrorType.CONTEXT_TOO_LONG) {
1132
+ logger.info(`[ResponseEngine] Context too long error, skipping error accumulation`);
1133
+ // 认证错误(401 / Invalid API Key)不是会话问题,不累计
1134
+ }
1135
+ else if (errorType === ErrorType.AUTH_ERROR) {
1136
+ logger.info(`[ResponseEngine] Auth error (invalid API key), skipping error accumulation`);
1137
+ // API 临时错误如果走过重试链路,已按每次失败计数;不要在最终 catch 再重复加 1。
1138
+ }
1139
+ else if (errorType === ErrorType.API_ERROR && wasRetryHealthRecorded(error)) {
1140
+ logger.info(`[ResponseEngine] API retry health already recorded, skipping duplicate accumulation`);
1141
+ }
1142
+ else if (!policy.accumulateErrors(chatType, identityRole)) {
1143
+ logger.info(`[ResponseEngine] Non-accumulating error (chatType=${chatType}, identity=${identityRole}), skipping error accumulation`);
1144
+ }
1145
+ else {
1146
+ const prefixed = prefixErrorType(ERROR_PREFIX.INFRA, errorType);
1147
+ await this.sessionManager.recordError(session.id, prefixed, error.message);
1148
+ }
1149
+ }
1150
+ catch (statusError) {
1151
+ logger.error('[ResponseEngine] Failed to update health status:', statusError);
1152
+ }
1153
+ }
1154
+ throw error;
1155
+ }
1156
+ finally {
1157
+ if (monitorInterval)
1158
+ clearInterval(monitorInterval);
1159
+ if (totalExecutionTimer)
1160
+ clearTimeout(totalExecutionTimer);
1161
+ this.activeMonitors.delete(streamKey);
1162
+ }
1163
+ }
1164
+ /** 获取回复上下文(跟着任务走) */
1165
+ getReplyContext(message) {
1166
+ return message.replyContext;
1167
+ }
1168
+ totalExecutionLimitMs() {
1169
+ const configuredSeconds = this.globalSettings.idleMonitor?.maxExecutionTime;
1170
+ return typeof configuredSeconds === 'number'
1171
+ && Number.isFinite(configuredSeconds)
1172
+ && configuredSeconds > 0
1173
+ ? configuredSeconds * 1000
1174
+ : 60 * 60 * 1000;
1175
+ }
1176
+ /** 自动安全模式已禁用:仅保留错误计数,不再自动切换状态 */
1177
+ async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, getLastIdleSec, outputState = { middleOutputMode: 'all' }, timeoutControl) {
1178
+ const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
1179
+ const channelKey = this.messageChannelKey(message, session);
1180
+ const channelInfo = this.resolveChannelInfo(channelKey);
1181
+ const taskAgentAid = message.selfAID || session.selfAID;
1182
+ const owningAgentForTask = this.agentRegistry?.resolveByChannel(channelKey)
1183
+ ?? (taskAgentAid ? this.agentRegistry?.get(taskAgentAid) : null);
1184
+ // Per-method agent name for stats bucketing (agent.name or '<unknown>')
1185
+ const agentNameForStats = owningAgentForTask?.name ?? taskAgentAid ?? '<unknown>';
1186
+ if (!channelInfo) {
1187
+ logger.error(`[ResponseEngine] Unknown channel: ${channelKey}`);
1188
+ this.publishTriggerExecutionFailure(message, `unknown_channel:${channelKey}`);
1189
+ return;
1190
+ }
1191
+ // 二次拦截:如果命令消息绕过 MessageBridge 的 handleCommand 泄漏到这里,
1192
+ // 静默丢弃而不是发送给 Agent(命令已在 MessageBridge 层处理过)
1193
+ const rawContent = message.content.replace(/^(>[^\n]*\n)+\n?/, '').trim();
1194
+ if (rawContent.startsWith('/') && this.isKnownCommand(rawContent)) {
1195
+ logger.warn(`[ResponseEngine] Command leaked past MessageBridge, dropped: "${rawContent.substring(0, 40)}"`);
1196
+ this.publishTriggerExecutionFailure(message, 'trigger_command_not_supported');
1197
+ return;
1198
+ }
1199
+ const { adapter, options, policy } = channelInfo;
1200
+ const chatType = message.chatType || 'private';
1201
+ const identityRole = session.identity?.role || 'none';
1202
+ let agent;
1203
+ try {
1204
+ agent = this.getAgent(channelKey, session.baseagent, session.selfAID || message.selfAID);
1205
+ }
1206
+ catch (error) {
1207
+ if (error instanceof BaseagentRunnerUnavailableError) {
1208
+ logger.error(`[ResponseEngine] baseagent mismatch blocked: session=${session.id} channel=${channelKey} requested=${session.baseagent} owner=${error.evolagentName} available=${error.availableBaseagents.join(',') || '<none>'}`);
1209
+ await this.sendBaseagentMismatch(channelKey, message.channelId, session, error, message.replyContext);
1210
+ this.publishTriggerExecutionFailure(message, `baseagent_unavailable:${error.baseagent}`);
1211
+ return;
1212
+ }
1213
+ throw error;
1214
+ }
1215
+ const streamKey = session.id;
1216
+ // 密文优先归一化:合并批次的 replyContext.metadata.encrypted 默认取自最后一条,
1217
+ // 这里改用 message.encrypted(mergeItems 算出的密文优先值)覆盖,使本轮所有出站路径
1218
+ // (IMRenderer.send / taskReplyContext / setSessionEncrypt / task:started)看到一致的加密态。
1219
+ // 仅 aun 入站会设 message.encrypted;非 aun 渠道为 undefined,不覆盖、保持原状。
1220
+ if (message.encrypted != null) {
1221
+ if (!message.replyContext)
1222
+ message.replyContext = {};
1223
+ message.replyContext.metadata = { ...(message.replyContext.metadata ?? {}), encrypted: message.encrypted };
1224
+ }
1225
+ // 为本次任务处理生成唯一 task_id(客户端生成,格式 task-{10hex})
1226
+ const taskId = `task-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`;
1227
+ let turnLease;
1228
+ const inputCausation = normalizeCausation(message.causation) ?? createRootCausation();
1229
+ const taskCausation = deriveCausation(inputCausation);
1230
+ recordCausationSpan(taskCausation, 'task.run', {
1231
+ status: 'started',
1232
+ refs: { taskId, sessionId: session.id, messageId: message.messageId },
1233
+ });
1234
+ for (const item of message.items ?? []) {
1235
+ const linked = normalizeCausation(item.causation);
1236
+ if (!linked || linked.spanId === inputCausation.spanId)
1237
+ continue;
1238
+ recordCausationLink({
1239
+ spanId: taskCausation.spanId,
1240
+ linkedTraceId: linked.traceId,
1241
+ linkedSpanId: linked.spanId,
1242
+ relation: 'batch_input',
1243
+ });
1244
+ }
1245
+ const triggerRunId = message.triggerMeta?.runId;
1246
+ const withTaskMetadata = (metadata) => ({
1247
+ ...(metadata ?? {}),
1248
+ taskId,
1249
+ chatmode,
1250
+ ...(triggerRunId ? { triggerRunId } : {}),
1251
+ causation: taskCausation,
1252
+ });
1253
+ // ─── 解析 self/peer/config(响应模式解析的输入)───
1254
+ const currentChannelType = options?.channelType || message.channel;
1255
+ const adapterAny = channelInfo.adapter;
1256
+ const adapterSelfAid = typeof adapterAny._selfAid === 'function' ? adapterAny._selfAid() : undefined;
1257
+ const selfAid = adapterSelfAid || message.selfAID || session.selfAID || undefined;
1258
+ const selfName = typeof adapterAny._selfName === 'function' ? adapterAny._selfName() : undefined;
1259
+ const peerName = message.peerName || session.metadata?.peerName;
1260
+ const peerIdRaw = message.peerId;
1261
+ const triggerExecutionIdentity = this.resolveTriggerExecutionIdentity(message, selfAid);
1262
+ const authorizationIdentity = triggerExecutionIdentity
1263
+ ?? this.resolveHandoffOriginExecutionIdentity(message, selfAid);
1264
+ const configChannelType = authorizationIdentity?.channelType ?? currentChannelType;
1265
+ const configChatType = authorizationIdentity
1266
+ ? (message.handoffDelivery?.originAuthorization?.chatType ?? 'private')
1267
+ : (chatType === 'group' ? 'group' : 'private');
1268
+ // A group channel ID identifies the conversation, not the authenticated
1269
+ // operator. Keep the historical private-chat fallback (where channelId
1270
+ // is the peer identity for AUN), but fail closed for group messages that
1271
+ // arrive without a sender/operator ID.
1272
+ const configActorId = authorizationIdentity?.actorId
1273
+ ?? (message.peerId || (configChatType === 'private' ? message.channelId : undefined));
1274
+ const peerKeyId = authorizationIdentity?.actorId ?? (session.chatType === 'group'
1275
+ ? (session.metadata?.groupId || message.channelId)
1276
+ : peerIdRaw);
1277
+ const peerKey = authorizationIdentity?.peerKey ?? ((configChannelType && peerKeyId)
1278
+ ? formatPeerKey(configChannelType, peerKeyId)
1279
+ : undefined);
1280
+ const peerType = message.peerType ?? session.metadata?.peerType;
1281
+ const peerRoleDetail = authorizationIdentity ? undefined : (() => {
1282
+ try {
1283
+ return resolvePeerRoleDetail({
1284
+ selfAid: selfAid || '',
1285
+ channelKey,
1286
+ channelType: configChannelType,
1287
+ chatType: configChatType,
1288
+ actorId: configActorId || '',
1289
+ conversationId: configChatType === 'group' ? (peerKeyId || message.channelId) : (configActorId || ''),
1290
+ peerType,
1291
+ });
1292
+ }
1293
+ catch {
1294
+ return undefined;
1295
+ }
1296
+ })();
1297
+ const authenticatedActor = authorizationIdentity?.actor
1298
+ ?? peerRoleDetail?.actor
1299
+ ?? {
1300
+ operatorId: String(configActorId || '').trim(),
1301
+ operatorChannelKey: authorizationIdentity?.originChannelKey ?? channelKey,
1302
+ operatorChannelType: configChannelType,
1303
+ principalStatus: 'error',
1304
+ };
1305
+ // A resolved detail (including an explicit `none`) is authoritative for
1306
+ // this inbound actor. Do not fall back to a stale session owner/admin
1307
+ // after an alias or Agent-role revocation; only use the persisted session
1308
+ // identity when role resolution itself was unavailable.
1309
+ const peerRole = authorizationIdentity?.role
1310
+ ?? (peerRoleDetail
1311
+ ? (peerRoleDetail.effectiveRole ?? 'none')
1312
+ : (session.identity?.role ?? 'none'));
1313
+ const effectiveAgentConfig = (() => {
1314
+ try {
1315
+ return resolveEffective({ self: selfAid || undefined, peerKey }, { cache: true });
1316
+ }
1317
+ catch (e) {
1318
+ logger.warn(`[ResponseEngine] resolveEffective failed: ${e instanceof Error ? e.message : String(e)}`);
1319
+ return undefined;
1320
+ }
1321
+ })();
1322
+ // ─── 响应模式解析(插件化机制中枢)───
1323
+ // trigger override 绝对优先;否则由 Coordinator 解析(responseMode 标量 > 注册表首选)。
1324
+ const systemOrServicePeer = isSystemOrServicePeer(peerType);
1325
+ const triggerChatModeOverride = systemOrServicePeer ? undefined : message.triggerMeta?.chatModeOverride;
1326
+ // chatmode 是顶层字典(agent 级与关系级同名,已由 ConfigManager 逐键合并)。
1327
+ // 实际生效值由对端类型选键(private/nothuman/group),出厂默认走 schema。
1328
+ const chatModeFallback = resolveChatMode({
1329
+ self: selfAid,
1330
+ peerKey,
1331
+ role: peerRole,
1332
+ chatType,
1333
+ peerType,
1334
+ });
1335
+ // mentionMode:顶层通用参数(先读出备用,投递/入队策略后续接入)
1336
+ const mentionMode = effectiveAgentConfig?.mentionMode ?? 'disabled';
1337
+ const resolvedMode = triggerChatModeOverride || systemOrServicePeer
1338
+ ? null // trigger 强制覆盖或 system/service 强制 interactive 时,不走插件解析
1339
+ : this.responseCoordinator.resolveMode(effectiveAgentConfig?.responseMode, // 标量:关系级>agent级>注册表首选
1340
+ chatModeFallback, effectiveAgentConfig?.responseModeParams, // 按模式分桶的参数字典
1341
+ {
1342
+ session,
1343
+ agentConfig: effectiveAgentConfig,
1344
+ runner: undefined, // 处理钩子用不到 runner;辅助会话工厂 Phase 后续接入
1345
+ channel: {
1346
+ type: currentChannelType,
1347
+ capabilities: {
1348
+ supportsThought: !!channelInfo.adapter.capabilities?.thought,
1349
+ supportsInteraction: !!channelInfo.adapter.capabilities?.interaction,
1350
+ supportsRichText: !!channelInfo.adapter.capabilities?.markdown,
1351
+ supportsFile: !!channelInfo.adapter.capabilities?.file,
1352
+ supportsImage: !!channelInfo.adapter.capabilities?.image,
1353
+ },
1354
+ send: async () => { }, // 引擎自行发送,插件 handleOutbound 只做决策
1355
+ },
1356
+ logger,
1357
+ agentDir: resolvePaths().agentsDir,
1358
+ });
1359
+ if (resolvedMode) {
1360
+ logger.info('[ResponseSystem] selected mode=' + resolvedMode.mode.id + ' source=' + resolvedMode.source + ' chatType=' + chatType + ' peerKey=' + (peerKey ?? 'none') + ' chatMode=' + chatModeFallback);
1361
+ }
1362
+ else {
1363
+ logger.info('[ResponseSystem] selected mode=override/fallback source=trigger-or-resolve-failed chatType=' + chatType + ' peerKey=' + (peerKey ?? 'none') + ' chatMode=' + chatModeFallback);
1364
+ }
1365
+ // 最终 chatMode(怎么投递,与「选哪个模式」正交):
1366
+ // system/service 运行时硬约束 > trigger override > 配置层级解析(chatModeFallback)。
1367
+ // 注:mode.id 不再参与——单会话合并后 mode.id 恒为 single-session,
1368
+ // chatMode 由 resolveChatModeForPeer 按配置层级解析(步骤 1/4)。
1369
+ const effectiveChatMode = systemOrServicePeer
1370
+ ? 'interactive'
1371
+ : triggerChatModeOverride
1372
+ ?? chatModeFallback;
1373
+ const chatmode = effectiveChatMode;
1374
+ const isProactive = effectiveChatMode === 'proactive';
1375
+ const legacyMiddleOutputMode = () => {
1376
+ const mode = policy.middleOutputMode?.(chatType, identityRole, peerType);
1377
+ if (isShowActivitiesMode(mode))
1378
+ return mode;
1379
+ return policy.showMiddleResult(chatType, identityRole) ? 'all' : 'none';
1380
+ };
1381
+ const configuredMiddleOutputMode = isShowActivitiesMode(message.triggerMeta?.showActivitiesOverride)
1382
+ ? message.triggerMeta.showActivitiesOverride
1383
+ : isShowActivitiesMode(effectiveAgentConfig?.show_activities)
1384
+ ? effectiveAgentConfig.show_activities
1385
+ : legacyMiddleOutputMode();
1386
+ const middleOutputMode = isProactive
1387
+ ? 'all'
1388
+ : systemOrServicePeer
1389
+ ? 'none'
1390
+ : configuredMiddleOutputMode;
1391
+ outputState.middleOutputMode = middleOutputMode;
1392
+ this.shouldSuppressActivities = shouldSuppress();
1393
+ // 诊断日志:记录 inbound message_id 和生成的 task_id 的对应关系
1394
+ logger.info(`[ResponseEngine] Task created: inboundMsgId=${message.messageId ?? 'none'} taskId=${taskId} sessionId=${session.id} chatmode=${chatmode} mode=${resolvedMode?.mode.id ?? 'override/fallback'}`);
1395
+ // 构建带 taskId/chatmode 的 ReplyContext(本次任务所有出站消息共用)
1396
+ const taskReplyContext = () => {
1397
+ const base = this.getReplyContext(message);
1398
+ return {
1399
+ ...(base ?? {}),
1400
+ sessionId: session.id,
1401
+ threadId: session.threadId || base?.threadId,
1402
+ metadata: withTaskMetadata(base?.metadata),
1403
+ };
1404
+ };
1405
+ // ─── 响应模式运行时状态(迁移点1:beforeProcess 构造 ProactiveRuntimeState)───
1406
+ // 插件的 beforeProcess 把状态写入 modeState(per-message Map);引擎从中读出 proactive。
1407
+ const modeState = new Map();
1408
+ const modeProcessCtx = resolvedMode ? {
1409
+ session,
1410
+ message: {
1411
+ messageId: message.messageId, peerId: message.peerId, content: message.content,
1412
+ peerType: message.peerType || session.metadata?.peerType,
1413
+ chatType: chatType, isMentioned: message.isMentioned,
1414
+ mentionAids: message.mentionAids, source: message.source,
1415
+ },
1416
+ modeConfig: resolvedMode.context.modeConfig,
1417
+ state: modeState,
1418
+ isSendCommand: (toolName, toolInput) => isEvolcoreSendCommandForSession(toolName, toolInput, session.channelId),
1419
+ logger,
1420
+ } : null;
1421
+ if (resolvedMode?.mode.beforeProcess && modeProcessCtx) {
1422
+ await resolvedMode.mode.beforeProcess(modeProcessCtx);
1423
+ }
1424
+ // 从插件状态读出 proactive 运行时状态(替代原硬编码构造)
1425
+ const proactive = modeState.get('proactive') ?? null;
1426
+ // [迁移探针] 记录 chatMode 判定 + proactiveState 构造(防线 1:行为快照)
1427
+ snapshot.begin(session.id, taskId, 'plugin', message.messageId);
1428
+ snapshot.set(session.id, taskId, {
1429
+ chatMode: effectiveChatMode,
1430
+ proactiveState: proactive
1431
+ ? {
1432
+ preTool1stMsgChk: proactive.preTool1stMsgChk,
1433
+ toolUseReminder: proactive.toolUseReminder,
1434
+ firstSendRequired: proactive.firstSendRequired,
1435
+ toolReportRequired: proactive.toolReportRequired,
1436
+ chatType: proactive.chatType,
1437
+ peerType: proactive.peerType,
1438
+ }
1439
+ : null,
1440
+ });
1441
+ const envelope = buildEnvelope({
1442
+ taskId,
1443
+ sessionId: session.id,
1444
+ channel: message.channel,
1445
+ channelId: message.channelId,
1446
+ agentName: agentNameForStats,
1447
+ chatmode: isProactive ? 'proactive' : 'interactive',
1448
+ replyContext: taskReplyContext(),
1449
+ causation: taskCausation,
1450
+ });
1451
+ if (timeoutControl) {
1452
+ timeoutControl.deliverUnhandled = async (timeoutError) => {
1453
+ if (timeoutControl.terminalDelivered)
1454
+ return;
1455
+ timeoutControl.terminalDelivered = true;
1456
+ const isTotalExecutionTimeout = timeoutError.message === 'TOTAL_EXECUTION_TIMEOUT';
1457
+ const totalExecutionMs = this.totalExecutionLimitMs();
1458
+ const statusPayload = {
1459
+ kind: 'status.timeout',
1460
+ metadata: isTotalExecutionTimeout
1461
+ ? { totalExecutionMs }
1462
+ : { idleSec: getLastIdleSec?.() || undefined },
1463
+ };
1464
+ const idleSec = getLastIdleSec?.() || 0;
1465
+ const userMessage = isTotalExecutionTimeout
1466
+ ? `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`
1467
+ : idleSec > 0
1468
+ ? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断`
1469
+ : '⚠️ 任务超时,已自动中断';
1470
+ agent.cleanupStream(streamKey);
1471
+ this.sessionManager.clearProcessingIfTask(session.id, taskId);
1472
+ if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
1473
+ this.activeTaskRuntimeContexts.delete(session.id);
1474
+ }
1475
+ this.agentDelegationRegistry?.revokeTask(session.id, taskId);
1476
+ const daemonTrigger = this.isTrustedDaemonTrigger(message);
1477
+ if (!daemonTrigger) {
1478
+ await adapter.send(envelope, statusPayload).catch(() => { });
1479
+ }
1480
+ await adapter.send(envelope, {
1481
+ kind: 'result.error',
1482
+ text: userMessage,
1483
+ reason: isTotalExecutionTimeout ? 'total_execution_timeout' : 'timeout',
1484
+ }).catch(() => { });
1485
+ if (daemonTrigger) {
1486
+ await adapter.send(envelope, statusPayload).catch(() => { });
1487
+ }
1488
+ this.touchAgentActivity(channelKey);
1489
+ this.publishTriggerExecutionFailure(message, timeoutError.message, {
1490
+ messageId,
1491
+ causation: taskCausation,
1492
+ });
1493
+ const errorType = prefixErrorType(ERROR_PREFIX.INFRA, classifyError(timeoutError));
1494
+ this.eventBus.publish({
1495
+ type: 'task:error',
1496
+ sessionId: session.id,
1497
+ error: timeoutError.message,
1498
+ errorType,
1499
+ agentName: agentNameForStats,
1500
+ causation: taskCausation,
1501
+ });
1502
+ recordCausationSpan(taskCausation, 'task.run', {
1503
+ status: 'failed',
1504
+ refs: { taskId, sessionId: session.id, messageId },
1505
+ reason: errorType,
1506
+ });
1507
+ logger.message({
1508
+ msgId: messageId,
1509
+ sessionId: session.id,
1510
+ dir: 'inbound',
1511
+ status: 'failed',
1512
+ error: timeoutError.message,
1513
+ });
1514
+ };
1515
+ }
1516
+ try {
1517
+ const isBackground = this.isBackgroundSession(session, message.channel, message.channelId);
1518
+ // 记录收到消息
1519
+ logger.message({
1520
+ msgId: messageId,
1521
+ sessionId: session.id,
1522
+ dir: 'inbound',
1523
+ status: 'received'
1524
+ });
1525
+ this.eventBus.publish({
1526
+ type: 'message:received',
1527
+ sessionId: session.id,
1528
+ channel: message.channel,
1529
+ channelId: message.channelId,
1530
+ content: message.content,
1531
+ agentName: agentNameForStats,
1532
+ timestamp: Date.now()
1533
+ });
1534
+ // ── 硬上限检查:超限直接返回提示,不调模型 ──
1535
+ {
1536
+ const budgetAgentAid = selfAid || session.selfAID || message.selfAID || '';
1537
+ const budgetPeerKey = peerKey || formatPeerKey(currentChannelType, message.channelId);
1538
+ const budgetStatus = getBudgetStatus(resolveRoot(), budgetAgentAid, budgetPeerKey);
1539
+ if (budgetStatus.hard_blocked) {
1540
+ logger.warn(`[ResponseEngine] Budget hard limit reached: agent=${budgetAgentAid} peer=${budgetPeerKey} pct=${budgetStatus.pct_used.toFixed(1)}%`);
1541
+ this.touchAgentActivity(channelKey);
1542
+ adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs: 0 } }).catch(() => { });
1543
+ this.publishTriggerExecutionFailure(message, 'budget_exceeded');
1544
+ return;
1545
+ }
1546
+ const roleBudgetStatus = getRoleBudgetStatus(resolveRoot(), {
1547
+ selfAid: budgetAgentAid,
1548
+ role: peerRole,
1549
+ channelType: configChannelType,
1550
+ chatType: configChatType,
1551
+ channelId: configActorId || message.channelId,
1552
+ peerId: configActorId || undefined,
1553
+ });
1554
+ if (roleBudgetStatus.hard_blocked) {
1555
+ const digits = roleBudgetStatus.currency === 'USD' ? 4 : 2;
1556
+ const usageText = roleBudgetStatus.limit_amount >= 0
1557
+ ? `${roleBudgetStatus.currency} ${roleBudgetStatus.used_amount.toFixed(digits)}/${roleBudgetStatus.limit_amount.toFixed(digits)}`
1558
+ : '';
1559
+ logger.warn(`[ResponseEngine] Role budget hard limit reached: agent=${budgetAgentAid} role=${peerRole} subject=${roleBudgetStatus.usage_subject_key} usage=${usageText}`);
1560
+ // 用户可见文案:按宿主机系统语言在中/英间切换,币种符号本地化,按重置周期给恢复提示,不暴露 role 英文名
1561
+ const budgetZh = isHostChinese();
1562
+ const budgetSymbol = roleBudgetStatus.currency === 'USD' ? '$' : '¥';
1563
+ const budgetAmountText = roleBudgetStatus.limit_amount >= 0
1564
+ ? `${budgetSymbol}${roleBudgetStatus.used_amount.toFixed(digits)} / ${budgetSymbol}${roleBudgetStatus.limit_amount.toFixed(digits)}`
1565
+ : '';
1566
+ const budgetText = budgetZh
1567
+ ? `⚠️ 用量已达上限${budgetAmountText ? `(${budgetAmountText})` : ''},${roleBudgetStatus.reset_mode === 'daily'
1568
+ ? '今日额度已用完,明日 0 点自动恢复'
1569
+ : roleBudgetStatus.reset_mode === 'weekly'
1570
+ ? '本周额度已用完,下周一 0 点重置'
1571
+ : roleBudgetStatus.reset_mode === 'monthly'
1572
+ ? '本月额度已用完,下月 1 日重置'
1573
+ : '额度已用完,无自动重置,请联系管理员'}。`
1574
+ : `⚠️ Usage limit reached${budgetAmountText ? ` (${budgetAmountText})` : ''}. ${roleBudgetStatus.reset_mode === 'daily'
1575
+ ? "Today's quota is used up; it resets at midnight."
1576
+ : roleBudgetStatus.reset_mode === 'weekly'
1577
+ ? "This week's quota is used up; it resets Monday at midnight."
1578
+ : roleBudgetStatus.reset_mode === 'monthly'
1579
+ ? "This month's quota is used up; it resets on the 1st."
1580
+ : 'Quota is used up; no automatic reset — please contact an administrator.'}`;
1581
+ this.touchAgentActivity(channelKey);
1582
+ adapter.send(envelope, {
1583
+ kind: 'system.error',
1584
+ text: budgetText,
1585
+ subtype: 'role_budget_exceeded',
1586
+ recoverable: false,
1587
+ metadata: { roleBudget: roleBudgetStatus },
1588
+ }).catch(() => { });
1589
+ adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs: 0, roleBudget: roleBudgetStatus } }).catch(() => { });
1590
+ this.publishTriggerExecutionFailure(message, 'role_budget_exceeded');
1591
+ return;
1592
+ }
1593
+ }
1594
+ const imageInfo = message.images && message.images.length > 0 ? ` [${message.images.length} image(s)]` : '';
1595
+ const modeInfo = isBackground ? ' [\u540e\u53f0]' : '';
1596
+ const e2eeInfo = message.replyContext?.metadata?.encrypted != null ? ` encrypt=${message.replyContext.metadata.encrypted}` : '';
1597
+ logger.info(`[${message.channel}] ${message.channelId}: ${message.content}${imageInfo}${modeInfo}${e2eeInfo}`);
1598
+ // 构建 peer 标识(优先 peerName,退化到 peerId / channelId)
1599
+ const peerName = session.metadata?.peerName ?? message.peerName;
1600
+ const peerId = session.metadata?.peerId ?? message.peerId ?? message.channelId;
1601
+ const peerShort = peerId ? peerId.split('.')[0].split(':')[0] : '?';
1602
+ const peerLabel = peerName && peerName !== peerShort ? `${peerShort}(${peerName})` : peerShort;
1603
+ logger.info(`[ResponseEngine] session=${session.id} task=${taskId} peer=${peerLabel} chatType=${session.chatType} chatMode=${effectiveChatMode} baseagent=${session.baseagent} msgChatType=${message.chatType ?? 'n/a'}`);
1604
+ // Establish the durable turn before exposing any processing state. This
1605
+ // lets an immediate /stop or replacement message close the same task and
1606
+ // prevents a BusinessTrip reaction from becoming orphaned during setup.
1607
+ turnLease = await this.turnCoordinator.begin(session, taskId);
1608
+ // 记录开始处理
1609
+ const taskEncrypt = message.replyContext?.metadata?.encrypted != null ? !!(message.replyContext.metadata.encrypted) : undefined;
1610
+ this.eventBus.publish({ type: 'task:started', sessionId: session.id, agentName: agentNameForStats, encrypt: taskEncrypt, chatmode, causation: taskCausation });
1611
+ this.touchAgentActivity(channelKey);
1612
+ // Upgrade the channel acknowledgement at task start, before compaction and
1613
+ // runner setup, so processing feedback remains visible for the whole run.
1614
+ // Trigger messageId is an internal runId and has no channel reaction.
1615
+ if (message.messageId && message.source !== 'trigger') {
1616
+ adapter.promoteAck?.(message.messageId, {
1617
+ taskId,
1618
+ channelId: message.channelId,
1619
+ messageIds: message.sourceMessageIds,
1620
+ }).catch(() => { });
1621
+ }
1622
+ adapter.send(envelope, { kind: 'status.started' }).catch(() => { });
1623
+ await this.runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, adapter, envelope, false);
1624
+ logger.message({
1625
+ msgId: messageId,
1626
+ sessionId: session.id,
1627
+ dir: 'inbound',
1628
+ status: 'processing'
1629
+ });
1630
+ const startTime = Date.now();
1631
+ // 创建 IMRenderer(统一 interactive/proactive 两条路径)
1632
+ let firstReply = true;
1633
+ const renderer = new IMRenderer({
1634
+ adapter,
1635
+ envelope,
1636
+ flushDelay: (options?.flushDelay ?? this.agentRegistry?.resolveByChannel(channelKey)?.config?.flush_delay ?? 3) * 1000,
1637
+ suppressActivityItems: isProactive ? false : middleOutputMode !== 'all',
1638
+ suppressIntermediateText: isProactive ? false : middleOutputMode === 'none',
1639
+ fileMarkerPattern: options?.fileMarkerPattern,
1640
+ diagEnabled: this.globalSettings.debug?.flusherDiag,
1641
+ send: async (payload) => {
1642
+ if (turnLease && !this.turnCoordinator.canPublish(turnLease)) {
1643
+ snapshot.pushOutbound(session.id, taskId, { kind: payload.kind, decision: 'suppressed-stale-turn' });
1644
+ return;
1645
+ }
1646
+ // proactive 模式:activity.batch 是 thought 协议内容,只发给支持 thought 的 channel
1647
+ // (不支持 thought 的 channel 静默丢弃,避免降级为普通消息)
1648
+ if (isProactive && payload.kind === 'activity.batch' && !adapter.capabilities?.thought) {
1649
+ snapshot.pushOutbound(session.id, taskId, { kind: payload.kind, decision: 'suppressed-thought' });
1650
+ return;
1651
+ }
1652
+ const isCurrentlyBackground = this.isBackgroundSession(session, message.channel, message.channelId);
1653
+ if (isCurrentlyBackground) {
1654
+ snapshot.pushOutbound(session.id, taskId, { kind: payload.kind, decision: 'suppressed-bg' });
1655
+ return;
1656
+ }
1657
+ const opts = {};
1658
+ const baseReplyCtx = this.getReplyContext(message);
1659
+ if (baseReplyCtx) {
1660
+ Object.assign(opts, baseReplyCtx);
1661
+ // Trigger messageId is the internal runId, not an external channel
1662
+ // message id. Do not use it as replyToMessageId.
1663
+ }
1664
+ else if (firstReply && message.messageId && message.source !== 'trigger') {
1665
+ if (payload.kind === 'result.text' && payload.text) {
1666
+ opts.replyToMessageId = message.messageId;
1667
+ firstReply = false;
1668
+ }
1669
+ }
1670
+ opts.sessionId = session.id;
1671
+ if (payload.kind === 'result.text' && payload.isFinal) {
1672
+ opts.title = '\u2705 \u6700\u7ec8\u56de\u590d:';
1673
+ }
1674
+ opts.metadata = withTaskMetadata(opts.metadata);
1675
+ if (payload.kind.startsWith('status.'))
1676
+ this.touchAgentActivity(channelKey);
1677
+ const enrichedEnvelope = { ...envelope, replyContext: opts };
1678
+ snapshot.pushOutbound(session.id, taskId, { kind: payload.kind, decision: 'sent' });
1679
+ await adapter.send(enrichedEnvelope, payload);
1680
+ },
1681
+ });
1682
+ this.currentRenderer = renderer;
1683
+ renderer.addLifecycle('started');
1684
+ if (isProactive) {
1685
+ logger.info(`[ResponseEngine] proactive mode: outputs via thought.put task=${taskId}`);
1686
+ }
1687
+ // 调用 AgentRunner(含上下文过长自动 compact 重试)
1688
+ // 捕获当前消息的上下文(闭包),避免后续消息处理时串台
1689
+ const capturedChannelId = message.channelId;
1690
+ const capturedReplyContext = taskReplyContext();
1691
+ // 设置权限审批的消息发送回调(指向当前渠道)
1692
+ const permissionPrompt = async (text) => {
1693
+ await adapter.send({ ...envelope, replyContext: capturedReplyContext }, { kind: 'result.text', text, isFinal: false });
1694
+ };
1695
+ const authChatType = configChatType;
1696
+ const authConversationId = authChatType === 'group'
1697
+ ? (session.metadata?.groupId || session.channelId || capturedChannelId)
1698
+ : configActorId;
1699
+ const authPeerKey = configChannelType && authConversationId
1700
+ ? formatPeerKey(configChannelType, authConversationId)
1701
+ : undefined;
1702
+ const triggerOriginChannelInfo = authorizationIdentity
1703
+ ? this.resolveChannelInfo(authorizationIdentity.originChannelKey)
1704
+ : undefined;
1705
+ const permissionAdapter = triggerOriginChannelInfo?.adapter ?? adapter;
1706
+ const permissionChannelId = authorizationIdentity?.actorId ?? capturedChannelId;
1707
+ const permissionReplyContext = authorizationIdentity ? undefined : capturedReplyContext;
1708
+ let permissionTerminalFailure;
1709
+ const permissionPromptForOrigin = async (text) => {
1710
+ await permissionAdapter.send({
1711
+ ...envelope,
1712
+ channel: authorizationIdentity?.originChannelKey ?? envelope.channel,
1713
+ channelId: permissionChannelId,
1714
+ replyContext: permissionReplyContext,
1715
+ }, { kind: 'result.text', text, isFinal: false });
1716
+ };
1717
+ agent.setSendPrompt(permissionPromptForOrigin);
1718
+ const owningAgentForAuth = owningAgentForTask;
1719
+ const approvalRouting = (() => {
1720
+ if (!owningAgentForAuth)
1721
+ return undefined;
1722
+ // Approval recipients are security-sensitive and must not come from a
1723
+ // potentially stale in-memory AgentHandle snapshot after a config edit.
1724
+ const resolveApproverCandidates = () => ({
1725
+ owners: Array.from(new Set(listStaticAgentOwners(owningAgentForAuth.aid, { fresh: true }))),
1726
+ admins: Array.from(new Set(listStaticAgentAdmins(owningAgentForAuth.aid, { fresh: true }))),
1727
+ });
1728
+ const { owners, admins } = resolveApproverCandidates();
1729
+ const aunAdapter = owningAgentForAuth.channels?.get(`aun#${owningAgentForAuth.aid}#main`);
1730
+ const actorIsManager = hasTrustedPrincipal(authenticatedActor)
1731
+ && (owners.includes(authenticatedActor.principalId) || admins.includes(authenticatedActor.principalId));
1732
+ const originAuthRevision = approvalActorRevision(authenticatedActor, owners, admins, owningAgentForAuth.aid);
1733
+ return {
1734
+ approverPolicy: actorIsManager ? 'agent_manager' : 'requester',
1735
+ owners,
1736
+ admins,
1737
+ resolveApproverCandidates,
1738
+ ownerAdapter: aunAdapter,
1739
+ forceHandoff: !!authorizationIdentity,
1740
+ selfAid: owningAgentForAuth.aid,
1741
+ originSessionId: session.id,
1742
+ originMessageId: message.messageId,
1743
+ originChannel: configChannelType,
1744
+ originChannelId: authConversationId,
1745
+ originPeerId: configActorId,
1746
+ originPeerName: peerName || undefined,
1747
+ originPeerType: message.peerType || session.metadata?.peerType || undefined,
1748
+ originPrincipalId: hasTrustedPrincipal(authenticatedActor)
1749
+ ? authenticatedActor.principalId
1750
+ : undefined,
1751
+ originAuthRevision,
1752
+ originRole: peerRole,
1753
+ originThreadId: session.threadId || message.threadId || undefined,
1754
+ originChatDir: this.sessionManager.getChatDir(session),
1755
+ approvalTtlMs: 20 * 60 * 1000,
1756
+ reauthorizeApproval: (request) => {
1757
+ try {
1758
+ const approverChannelInfo = this.resolveChannelInfo(request.operatorChannelKey);
1759
+ const approverChannelType = approverChannelInfo?.options?.channelType
1760
+ ?? request.operatorChannelKey.split('#')[0]
1761
+ ?? request.operatorChannelKey;
1762
+ const currentApprover = resolvePeerRoleDetail({
1763
+ selfAid: owningAgentForAuth.aid,
1764
+ channelKey: request.operatorChannelKey,
1765
+ channelType: approverChannelType,
1766
+ chatType: 'private',
1767
+ actorId: request.operatorId,
1768
+ conversationId: request.operatorId,
1769
+ }).actor;
1770
+ const currentOrigin = resolvePeerRoleDetail({
1771
+ selfAid: owningAgentForAuth.aid,
1772
+ channelKey: authenticatedActor.operatorChannelKey,
1773
+ channelType: authenticatedActor.operatorChannelType,
1774
+ chatType: 'private',
1775
+ actorId: authenticatedActor.operatorId,
1776
+ conversationId: authenticatedActor.operatorId,
1777
+ }).actor;
1778
+ const currentOwners = listStaticAgentOwners(owningAgentForAuth.aid, { fresh: true });
1779
+ const currentAdmins = listStaticAgentAdmins(owningAgentForAuth.aid, { fresh: true });
1780
+ return {
1781
+ ok: true,
1782
+ actor: currentApprover,
1783
+ owners: currentOwners,
1784
+ admins: currentAdmins,
1785
+ originPrincipalId: hasTrustedPrincipal(currentOrigin)
1786
+ ? currentOrigin.principalId
1787
+ : undefined,
1788
+ originAuthRevision: approvalActorRevision(currentOrigin, currentOwners, currentAdmins, owningAgentForAuth.aid),
1789
+ };
1790
+ }
1791
+ catch (error) {
1792
+ logger.warn(`[ResponseEngine] approval reauthorization failed: agent=${owningAgentForAuth.aid} operator=${request.operatorId} error=${error instanceof Error ? error.message : String(error)}`);
1793
+ return { ok: false, reason: 'approval_reauthorization_failed' };
1794
+ }
1795
+ },
1796
+ };
1797
+ })();
1798
+ const recordExecutionAnomaly = triggerRunId
1799
+ ? (anomaly) => {
1800
+ recordTriggerExecutionAnomaly(triggerRunId, anomaly);
1801
+ }
1802
+ : undefined;
1803
+ // 设置权限审批的交互上下文(支持交互卡片)
1804
+ agent.setPermissionContext?.(session.id, {
1805
+ sendPrompt: permissionPromptForOrigin,
1806
+ adapter: permissionAdapter,
1807
+ channelId: permissionChannelId,
1808
+ replyContext: permissionReplyContext,
1809
+ interactionRouter: this.interactionRouter,
1810
+ userId: configActorId || undefined,
1811
+ actor: authenticatedActor,
1812
+ channel: permissionAdapter.channelKey,
1813
+ agentName: agentNameForStats,
1814
+ taskId,
1815
+ chatmode: isProactive ? 'proactive' : 'interactive',
1816
+ role: peerRole,
1817
+ chatType: authChatType,
1818
+ selfAid: session.selfAID || message.selfAID,
1819
+ peerKey: authPeerKey,
1820
+ causation: taskCausation,
1821
+ approvalRouting,
1822
+ approvalInteractionPolicy: authorizationIdentity ? 'deny' : 'interactive',
1823
+ recordExecutionAnomaly,
1824
+ turn: {
1825
+ taskId,
1826
+ turnId: turnLease.turnId,
1827
+ generation: turnLease.generation,
1828
+ onInteractionOpen: async (interaction) => {
1829
+ await this.turnCoordinator.openInteraction(session, turnLease, {
1830
+ ...interaction,
1831
+ taskId,
1832
+ turnId: turnLease.turnId,
1833
+ generation: turnLease.generation,
1834
+ createdAt: Date.now(),
1835
+ });
1836
+ },
1837
+ onInteractionSettled: async (interactionId, settlement) => {
1838
+ await this.turnCoordinator.settleInteraction(session, turnLease, interactionId, settlement);
1839
+ },
1840
+ },
1841
+ permissionStateChanged: async (permissionEvent) => {
1842
+ const executionEnvelope = { ...envelope, replyContext: capturedReplyContext };
1843
+ if (permissionEvent.state === 'waiting') {
1844
+ const payload = {
1845
+ kind: 'status.requires_action',
1846
+ metadata: {
1847
+ action: 'owner_approval',
1848
+ requestId: permissionEvent.requestId,
1849
+ toolName: permissionEvent.toolName,
1850
+ message: '等待 owner 审批',
1851
+ expiresAt: permissionEvent.expiresAt,
1852
+ },
1853
+ };
1854
+ await adapter.send(executionEnvelope, payload);
1855
+ if (permissionAdapter !== adapter || permissionChannelId !== capturedChannelId) {
1856
+ await permissionAdapter.send({
1857
+ ...envelope,
1858
+ channel: authorizationIdentity?.originChannelKey ?? envelope.channel,
1859
+ channelId: permissionChannelId,
1860
+ replyContext: permissionReplyContext,
1861
+ }, payload);
1862
+ }
1863
+ return;
1864
+ }
1865
+ if (permissionEvent.state === 'approved') {
1866
+ await adapter.send(executionEnvelope, {
1867
+ kind: 'status.progress',
1868
+ metadata: { activityType: 'progress', state: 'processing', text: 'owner 已批准,继续执行' },
1869
+ });
1870
+ return;
1871
+ }
1872
+ permissionTerminalFailure = {
1873
+ reason: permissionEvent.reason,
1874
+ message: permissionEvent.message,
1875
+ requestId: permissionEvent.requestId,
1876
+ };
1877
+ try {
1878
+ await adapter.send(executionEnvelope, {
1879
+ kind: 'status.error',
1880
+ metadata: {
1881
+ errorType: `permission_${permissionEvent.reason}`,
1882
+ message: permissionEvent.message,
1883
+ requestId: permissionEvent.requestId,
1884
+ },
1885
+ });
1886
+ }
1887
+ finally {
1888
+ await agent.interrupt(session.id).catch(error => {
1889
+ logger.debug(`[ResponseEngine] permission terminal interrupt failed: session=${session.id} error=${error instanceof Error ? error.message : String(error)}`);
1890
+ });
1891
+ }
1892
+ },
1893
+ flushPending: async () => {
1894
+ await renderer.flush(false);
1895
+ },
1896
+ interceptNextMessage: this.messageQueue
1897
+ ? (sessionKey, handler) => this.messageQueue.interceptNext(sessionKey, handler)
1898
+ : undefined,
1899
+ cancelIntercept: this.messageQueue
1900
+ ? (sessionKey) => this.messageQueue.cancelIntercept(sessionKey)
1901
+ : undefined,
1902
+ // [迁移点2] policyHook 由 ProactiveMode.configureRun 提供(插件决策);
1903
+ // 引擎补充副作用:违规时注入提醒到模型上下文 + 行为探针。
1904
+ policyHook: (() => {
1905
+ const runConfig = resolvedMode?.mode.configureRun && modeProcessCtx
1906
+ ? resolvedMode.mode.configureRun(modeProcessCtx)
1907
+ : undefined;
1908
+ const pluginHook = runConfig?.policyHook;
1909
+ if (!pluginHook)
1910
+ return undefined;
1911
+ return (toolName, toolInput) => {
1912
+ const stBefore = modeState.get('proactive');
1913
+ const wasFirstPending = stBefore && !stBefore.firstToolDone;
1914
+ const result = pluginHook(toolName, toolInput);
1915
+ if (result?.block) {
1916
+ // 拦截事件(首工具非表态 / 工具进展汇报未完成)
1917
+ snapshot.set(session.id, taskId, { policyHook: { triggered: true, blocked: true, toolName } });
1918
+ const errorMsg = `⚠️ proactive 模式违规:${result.reason ?? '请先发送必要说明'}。请重新执行正确的命令。`;
1919
+ agent.injectUserMessage?.(session.id, errorMsg);
1920
+ return result;
1921
+ }
1922
+ // 首工具检查通过(firstToolDone 从 false 翻成 true 且未拦截)
1923
+ const stAfter = modeState.get('proactive');
1924
+ if (wasFirstPending && stAfter?.firstToolDone) {
1925
+ snapshot.set(session.id, taskId, { policyHook: { triggered: true, blocked: false, toolName } });
1926
+ }
1927
+ return undefined;
1928
+ };
1929
+ })(),
1930
+ });
1931
+ // per-session 权限模式在 try 内、peerKey 解析后设置(见 resolvePermissionMode 调用)
1932
+ // 标记会话为处理中(实时持久化,重启后可恢复)
1933
+ this.sessionManager.markProcessing(session.id, taskId);
1934
+ if (message.replyContext?.metadata?.encrypted != null) {
1935
+ this.sessionManager.setSessionEncrypt(session.id, !!(message.replyContext.metadata.encrypted));
1936
+ }
1937
+ logger.info(`[ResponseEngine] session ${session.id} marked as processing task=${taskId}`);
1938
+ // 检查是否因新消息自动中断 — 包装 prompt 让 Agent 知道上下文
1939
+ const prevInterruptReason = this.interruptedSessions.get(session.id);
1940
+ this.interruptedSessions.delete(session.id);
1941
+ const wasInterrupted = prevInterruptReason === 'new_message' && !!session.agentSessionId;
1942
+ const wrapPrompt = (body) => wasInterrupted
1943
+ ? `【新消息插入】\n\n${body}\n\n【请根据前后消息酌情处理】`
1944
+ : body;
1945
+ // 先用裸文本兜底;vars 构造完成后用消息渲染层重算(见下方 effectivePrompt 重赋值)。
1946
+ let effectivePrompt = wrapPrompt(message.content);
1947
+ let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
1948
+ let renderResult;
1949
+ let effectiveSystemPrompt;
1950
+ let modelOverride;
1951
+ let usedFallback = false;
1952
+ let skipEvolcoreModel = false;
1953
+ let agentModel;
1954
+ let v2HandoffIds = [];
1955
+ let v2HandoffDirection;
1956
+ let handoffPromptRendered = false;
1957
+ let runtimeEnv;
1958
+ try {
1959
+ // 动态构建运行时上下文提示
1960
+ const contextParts = [];
1961
+ // 通道能力
1962
+ const supportsFileMarker = currentChannelType !== 'aun' && !isProactive && !!channelInfo.adapter.capabilities?.file;
1963
+ const capParts = [];
1964
+ if (options?.supportsImages)
1965
+ capParts.push('图片输入');
1966
+ if (channelInfo.adapter.capabilities?.image)
1967
+ capParts.push('图片输出');
1968
+ if (!isProactive && channelInfo.adapter.capabilities?.file)
1969
+ capParts.push('文件发送');
1970
+ // Personal layer
1971
+ const owningAgent = owningAgentForTask;
1972
+ const persona = owningAgent?.getPersona?.() || undefined;
1973
+ const working = owningAgent?.getWorkingMemory?.() || undefined;
1974
+ if (persona)
1975
+ contextParts.push(persona);
1976
+ if (working)
1977
+ contextParts.push(`[当前关注]\n${working}`);
1978
+ // 计算 peerKey:群聊固定按 groupId/channelId,私聊按发送者 peerId。
1979
+ // 这样单条和积压合并批次不会因队列状态不同而切换关系级配置。
1980
+ const normalizedBaseagent = normalizeBaseagent(agent.name);
1981
+ // 设置 per-call 权限模式:只按当前角色定义解析(不读物理配置层或 session.metadata)。
1982
+ // Trigger 只能降低本次调用权限,不能突破当前角色的权限上限。
1983
+ // 作为 per-call 入参随 modelOverride 传入 runQuery —— 与 model/effort 同构,
1984
+ // 不写 AgentRunner 实例字段,多对端/多会话并发共享同一 runner 实例时互不污染。
1985
+ let effectivePermissionMode;
1986
+ const triggerPermissionModeOverride = message.triggerMeta?.permissionModeOverride;
1987
+ try {
1988
+ effectivePermissionMode = constrainRuntimePermissionMode({
1989
+ selfAid: selfAid || undefined,
1990
+ role: peerRole,
1991
+ requestedValue: triggerPermissionModeOverride,
1992
+ }).effectiveValue;
1993
+ }
1994
+ catch (e) {
1995
+ logger.warn(`[ResponseEngine] permission mode resolution failed, using fallback: ${e instanceof Error ? e.message : String(e)}`);
1996
+ effectivePermissionMode = 'readonly';
1997
+ }
1998
+ // 按 关系级 > agent级 > 全局 解析本次调用的模型/强度,作为 per-call 入参传入 runQuery。
1999
+ // 不缓存、不绑会话——改关系级/agent级后该范围所有会话的下条消息即时生效;
2000
+ // 多对端并发各自独立解析、各自传参,无共享状态可被污染。
2001
+ let effectiveModel;
2002
+ let effectiveModelSource;
2003
+ let effectiveEffort;
2004
+ let effectiveEffortSource;
2005
+ const triggerModelOverride = message.triggerMeta?.modelOverride;
2006
+ const triggerEffortOverride = message.triggerMeta?.effortOverride;
2007
+ let appliedTriggerModelOverride = false;
2008
+ let effectiveTriggerEffortOverride;
2009
+ // 取降级状态,按退避策略决定是否跳过 evolcore 作用域模型
2010
+ const fbState = this.modelFallbackMap.get(session.id) ?? {
2011
+ failCount: 0, fallbackActive: false,
2012
+ messagesSinceFallback: 0, nextProbeAt: 2, hintShown: false,
2013
+ };
2014
+ // 退避期内递增消息计数,判断是否到探测点
2015
+ if (fbState.fallbackActive) {
2016
+ fbState.messagesSinceFallback++;
2017
+ skipEvolcoreModel = fbState.messagesSinceFallback < fbState.nextProbeAt;
2018
+ this.modelFallbackMap.set(session.id, fbState);
2019
+ }
2020
+ // 非跳过时:尝试解析 evolcore 作用域模型
2021
+ let evolcoreModelOverride;
2022
+ if (!skipEvolcoreModel) {
2023
+ try {
2024
+ const selector = { self: selfAid || undefined, peerKey };
2025
+ const modelResolution = resolveEffectiveFieldWithSource(`baseagents.${normalizedBaseagent.canonical}.model`, selector, { cache: true });
2026
+ const effortResolution = resolveEffectiveFieldWithSource(`baseagents.${normalizedBaseagent.canonical}.${normalizedBaseagent.canonical === 'codex' ? 'reasoning' : 'effort'}`, selector, { cache: true });
2027
+ const modelDecision = constrainResolvedModelForRole({
2028
+ role: peerRole,
2029
+ baseagent: normalizedBaseagent.canonical,
2030
+ model: modelResolution.value,
2031
+ resolveModelId: typeof agent.resolveModelId === 'function'
2032
+ ? agent.resolveModelId.bind(agent)
2033
+ : undefined,
2034
+ selfAid: selfAid || undefined,
2035
+ });
2036
+ const model = modelDecision.model;
2037
+ const effortField = `baseagents.${normalizedBaseagent.canonical}.${normalizedBaseagent.canonical === 'codex' ? 'reasoning' : 'effort'}`;
2038
+ const effortDecision = resolveRuntimeStringField({
2039
+ selfAid: selfAid || undefined,
2040
+ role: peerRole,
2041
+ field: effortField,
2042
+ configuredValue: effortResolution.value,
2043
+ });
2044
+ const effectiveEffortValue = effortDecision.effectiveValue;
2045
+ const effort = effectiveEffortValue === 'auto' ? undefined : effectiveEffortValue;
2046
+ effectiveModelSource = modelDecision.constrained
2047
+ ? 'role'
2048
+ : modelResolution.source ? `${modelResolution.source.target}:${modelResolution.source.file}` : undefined;
2049
+ effectiveEffortSource = effortDecision.decidedBy === 'role'
2050
+ ? 'role'
2051
+ : effortResolution.source
2052
+ ? `${effortResolution.source.target}:${effortResolution.source.file}`
2053
+ : undefined;
2054
+ if (model || effort) {
2055
+ evolcoreModelOverride = { model, effort };
2056
+ effectiveModel = model;
2057
+ }
2058
+ effectiveEffort = effectiveEffortValue;
2059
+ }
2060
+ catch (e) {
2061
+ logger.warn(`[ResponseEngine] effective model config resolution failed: ${e instanceof Error ? e.message : String(e)}`);
2062
+ }
2063
+ modelOverride = evolcoreModelOverride;
2064
+ }
2065
+ if (triggerModelOverride || triggerEffortOverride) {
2066
+ const constrainedOverride = { ...(modelOverride || {}) };
2067
+ if (triggerModelOverride) {
2068
+ const triggerModelDecision = constrainResolvedModelForRole({
2069
+ role: peerRole,
2070
+ baseagent: normalizedBaseagent.canonical,
2071
+ model: triggerModelOverride,
2072
+ resolveModelId: typeof agent.resolveModelId === 'function'
2073
+ ? agent.resolveModelId.bind(agent)
2074
+ : undefined,
2075
+ selfAid: selfAid || undefined,
2076
+ });
2077
+ if (triggerModelDecision.model)
2078
+ constrainedOverride.model = triggerModelDecision.model;
2079
+ else
2080
+ delete constrainedOverride.model;
2081
+ effectiveModel = triggerModelDecision.model;
2082
+ effectiveModelSource = triggerModelDecision.constrained ? 'role' : 'trigger';
2083
+ appliedTriggerModelOverride = !triggerModelDecision.constrained;
2084
+ }
2085
+ if (triggerEffortOverride) {
2086
+ const effortField = `baseagents.${normalizedBaseagent.canonical}.${normalizedBaseagent.canonical === 'codex' ? 'reasoning' : 'effort'}`;
2087
+ const triggerEffortDecision = resolveRuntimeStringField({
2088
+ selfAid: selfAid || undefined,
2089
+ role: peerRole,
2090
+ field: effortField,
2091
+ configuredValue: triggerEffortOverride,
2092
+ });
2093
+ const triggerEffort = triggerEffortDecision.effectiveValue;
2094
+ effectiveTriggerEffortOverride = triggerEffort === 'auto' ? undefined : triggerEffort;
2095
+ if (effectiveTriggerEffortOverride)
2096
+ constrainedOverride.effort = effectiveTriggerEffortOverride;
2097
+ else
2098
+ delete constrainedOverride.effort;
2099
+ effectiveEffort = triggerEffort;
2100
+ effectiveEffortSource = triggerEffortDecision.decidedBy === 'role' ? 'role' : 'trigger';
2101
+ }
2102
+ modelOverride = constrainedOverride;
2103
+ }
2104
+ // permissionMode 随角色策略或 trigger override 传入;单 runner
2105
+ // 嵌入/测试路径没有 self/peer 作用域时,避免制造无配置来源的 override。
2106
+ const shouldPassPermissionMode = !!message.triggerMeta?.permissionModeOverride || !!selfAid;
2107
+ if (shouldPassPermissionMode) {
2108
+ modelOverride = { ...(modelOverride || {}), permissionMode: effectivePermissionMode };
2109
+ }
2110
+ if (normalizedBaseagent.canonical === 'claude') {
2111
+ modelOverride = {
2112
+ ...(modelOverride || {}),
2113
+ sessionTitle: deriveSessionTitle(session.name, message.content, session.threadId),
2114
+ };
2115
+ }
2116
+ agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
2117
+ const causationPath = inputCausation.trigger?.path ?? [];
2118
+ const originNode = causationPath[0];
2119
+ logger.info(`[ResponseEngine] execution context session=${session.id}`
2120
+ + ` originRunId=${originNode?.runId ?? '<none>'}`
2121
+ + ` triggerRunId=${message.triggerMeta?.runId ?? '<none>'}`
2122
+ + ` selfAID=${selfAid ?? session.selfAID ?? '<none>'}`
2123
+ + ` baseagent=${normalizedBaseagent.canonical}`
2124
+ + ` model=${effectiveModel ?? agentModel ?? '<runner-default>'}`
2125
+ + ` modelSource=${effectiveModelSource ?? 'runner-default'}`
2126
+ + ` effort=${effectiveEffort ?? '<runner-default>'}`
2127
+ + ` effortSource=${effectiveEffortSource ?? 'runner-default'}`);
2128
+ const groupRulesVars = session.chatType === 'group' && currentChannelType === 'aun'
2129
+ ? await syncGroupRulesContext({
2130
+ selfAid,
2131
+ groupId: session.metadata?.groupId || message.channelId,
2132
+ channel: currentChannelType || message.channel,
2133
+ })
2134
+ : {};
2135
+ // Kit renderer: 组装上下文
2136
+ const pkgRoot = getPackageRoot();
2137
+ const kitCtx = {
2138
+ vars: {
2139
+ EVOLCORE_HOME: resolveRoot(),
2140
+ PACKAGE_ROOT: pkgRoot,
2141
+ CURRENT_PROJECT: absoluteProjectPath,
2142
+ // ECK 派生路径(manifest 引用时需要展开)
2143
+ KITS: path.join(pkgRoot, 'kits'),
2144
+ KITS_RULES: path.join(pkgRoot, 'kits', 'rules'),
2145
+ KITS_DOCS: path.join(pkgRoot, 'kits', 'docs'),
2146
+ KITS_TEMPLATES: path.join(pkgRoot, 'kits', 'templates'),
2147
+ KITS_FRAGMENTS: path.join(pkgRoot, 'kits', 'templates', 'system-fragments'),
2148
+ KITS_MESSAGE_FRAGMENTS: path.join(pkgRoot, 'kits', 'templates', 'message-fragments'),
2149
+ // evolcore 运行模式:dev=源码仓库 | install=全局安装包
2150
+ evolcoreMode: fs.existsSync(path.join(pkgRoot, 'src', 'index.ts')) ? 'dev' : 'install',
2151
+ // 路径变量(用于 manifest 路径展开,resolvePath 用 ctx.vars 取真值)
2152
+ PERSONAL_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'personal') : undefined,
2153
+ RELATIONS_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'relations') : undefined,
2154
+ VENUES_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'venues') : undefined,
2155
+ selfAid: selfAid || undefined,
2156
+ selfName: selfName || undefined,
2157
+ hasPersona: !!persona,
2158
+ hasWorkingMemory: !!working,
2159
+ peerId: peerIdRaw || undefined,
2160
+ peerKey,
2161
+ peerName: peerName || undefined,
2162
+ peerRole,
2163
+ peerType: message.peerType || session.metadata?.peerType || undefined,
2164
+ sameDevice: message.sameDevice ?? false,
2165
+ sameNetwork: message.sameNetwork ?? false,
2166
+ sameEgressIp: message.sameEgressIp ?? false,
2167
+ groupId: session.metadata?.groupId || undefined,
2168
+ groupName: session.metadata?.groupName || undefined,
2169
+ // 信封展示用:有群名则「名<ID>」,否则纯 ID。规避模板引擎无 not/else 的限制。
2170
+ groupLabel: session.metadata?.groupId
2171
+ ? (session.metadata?.groupName ? `${session.metadata.groupName}<${session.metadata.groupId}>` : session.metadata.groupId)
2172
+ : undefined,
2173
+ chatType: session.chatType || null,
2174
+ channel: currentChannelType || null,
2175
+ venueUid: undefined,
2176
+ // 群 @ 处理模式 / 客户端类型 / 权限模式
2177
+ // 优先 agent/relation mentionMode 配置,fallback 到服务器 dispatch_mode 缓存(协议词汇需翻译)。
2178
+ mentionMode: effectiveAgentConfig?.mentionMode
2179
+ ?? dispatchToMentionMode(session.metadata?.dispatchMode ?? message.dispatchMode)
2180
+ ?? undefined,
2181
+ clientType: message.clientType || undefined,
2182
+ permissionMode: effectivePermissionMode,
2183
+ capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
2184
+ fileCapable: supportsFileMarker,
2185
+ supportsFileMarker,
2186
+ project: path.basename(absoluteProjectPath),
2187
+ sessionId: session.id,
2188
+ sessionName: session.name || undefined,
2189
+ sessionCreatedAt: session.createdAt ? new Date(session.createdAt).toISOString() : undefined,
2190
+ // 时区(把 ISO 时间戳转本地时间用)+ OS 环境
2191
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || undefined,
2192
+ tzOffset: currentTzOffset(),
2193
+ localDate: currentLocalDate(),
2194
+ weekday: currentWeekday(),
2195
+ osInfo: OS_INFO,
2196
+ threadId: session.threadId || undefined,
2197
+ // Stage 3: sessionKey 持久化字段
2198
+ sessionKey: session.sessionKey,
2199
+ chatMode: isProactive ? 'proactive' : 'interactive',
2200
+ // proactive 为 null(interactive 模式,这些开关不适用)时如实为 false——
2201
+ // 默认值不在此硬编码,出厂默认由模式 schema 声明、经 buildState 落入 proactive.*。
2202
+ proactivePreTool1stMsgChk: proactive?.preTool1stMsgChk ?? false,
2203
+ proactiveToolUseReminder: proactive?.toolUseReminder ?? false,
2204
+ proactiveFirstSendRequired: proactive?.firstSendRequired ?? false,
2205
+ proactiveToolReportRequired: proactive?.toolReportRequired ?? false,
2206
+ proactiveToolReportInterval: proactive?.toolReportInterval ?? 10,
2207
+ proactiveSendTargetLabel: chatType === 'group' ? '群里' : '对方',
2208
+ readonly: effectivePermissionMode === 'readonly',
2209
+ baseAgent: normalizedBaseagent.canonical,
2210
+ baseAgentName: normalizedBaseagent.displayName,
2211
+ baseAgentModel: agentModel || undefined,
2212
+ effectiveModel: effectiveModel || agentModel || undefined,
2213
+ modelFallbackActive: (fbState.fallbackActive || skipEvolcoreModel) ? true : undefined,
2214
+ modelFallbackModel: (fbState.fallbackActive || skipEvolcoreModel) ? (agentModel || undefined) : undefined,
2215
+ agentSessionId: session.agentSessionId || undefined,
2216
+ // 渲染模式:各类型当前激活的 modeName(从内存 config 读,渲染层据此选 manifest section)。
2217
+ renderModes: this.agentRegistry?.resolveByChannel(channelKey)?.config?.render ?? undefined,
2218
+ ...groupRulesVars,
2219
+ },
2220
+ sessionId: session.id,
2221
+ };
2222
+ // 按会话原型(sessionType)选 manifest 文件:config.sessionManifests 映射,缺省回退主 manifest。
2223
+ const sessionType = session.sessionType ?? 'main';
2224
+ const sessionManifests = this.agentRegistry?.resolveByChannel(channelKey)?.config?.sessionManifests;
2225
+ const manifestFile = sessionManifests?.[sessionType] ?? 'eck_manifest.json';
2226
+ const kitContext = renderKitSections(kitCtx, manifestFile);
2227
+ if (kitContext)
2228
+ contextParts.push(kitContext);
2229
+ effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
2230
+ // ── Stats: context_breakdown 旁路采集(各段估算 token 数,字符数/4 近似) ──
2231
+ try {
2232
+ const estTokens = (s) => s ? Math.ceil(s.length / 4) : 0;
2233
+ const cbModel = effectiveModel || agentModel || 'unknown';
2234
+ const cbMaxTokens = 200000; // 保守默认,后续可从 model-catalog 取
2235
+ const systemPromptTokens = estTokens(options?.systemPromptAppend);
2236
+ const personaTokens = estTokens(persona);
2237
+ const workingTokens = estTokens(working);
2238
+ const kitTokens = estTokens(kitContext);
2239
+ const totalEst = estTokens(effectiveSystemPrompt);
2240
+ insertContextBreakdown(resolveRoot(), {
2241
+ ts: Date.now(),
2242
+ agent_aid: selfAid || session.selfAID || '',
2243
+ session_id: session.id,
2244
+ turn_count: 0, // 按 ts 排序得轮次
2245
+ model: cbModel,
2246
+ max_tokens: cbMaxTokens,
2247
+ system_prompt: systemPromptTokens + personaTokens + workingTokens,
2248
+ system_tools: 0, // 工具 schema 不在此层,留 0(后续 runner 层补)
2249
+ mcp_tools: 0,
2250
+ custom_agents: 0,
2251
+ memory_files: kitTokens, // ECK 渲染的所有段(含 memory/skills/rules)
2252
+ skills: 0,
2253
+ messages: 0, // messages 段在 runner 层才知道
2254
+ free_space: Math.max(0, cbMaxTokens - totalEst),
2255
+ total_estimated: totalEst,
2256
+ });
2257
+ }
2258
+ catch { /* non-fatal */ }
2259
+ // 消息渲染层:用 message manifest 逐条渲染(时间 + 群聊发送者),组装成最终正文。
2260
+ // 单条消息构造单元素 items;批量合并的消息 message.items 已由队列填充。
2261
+ const hasContent = message.content.trim() || (message.items && message.items.length > 0);
2262
+ if (hasContent) {
2263
+ const rawPeerItems = message.items && message.items.length > 0
2264
+ ? message.items
2265
+ : [{
2266
+ peerId: message.peerId, peerName: peerName || undefined,
2267
+ peerType: message.peerType,
2268
+ peerRole: message.batchRole || session.identity?.role || 'none',
2269
+ sameDevice: message.sameDevice, sameNetwork: message.sameNetwork, sameEgressIp: message.sameEgressIp,
2270
+ encrypted: message.encrypted,
2271
+ content: message.content, timestamp: message.timestamp,
2272
+ images: message.images,
2273
+ mentionAids: message.mentionAids,
2274
+ }];
2275
+ const peerItems = (() => {
2276
+ if (message.handoffDelivery && this.handoffRuntime) {
2277
+ const items = this.handoffRuntime.buildPromptItems(message);
2278
+ if (items.length > 0) {
2279
+ v2HandoffIds = items
2280
+ .map(item => item.handoff?.handoffId)
2281
+ .filter((handoffId) => !!handoffId);
2282
+ v2HandoffDirection = message.handoffDelivery.direction;
2283
+ return items;
2284
+ }
2285
+ }
2286
+ return rawPeerItems;
2287
+ })();
2288
+ // 观察者插话(v0.3):消费 (对端, thread) 的待用提示,包成 owner-hint item 排在对端消息前。
2289
+ // 一次性语义:consumeOwnerHints 读取并删除(见 pending-hints.ts)。在 try 外消费,
2290
+ // 这样即便 renderMessageBody 抛错走 raw 兜底,也把提示原文拼进去——绝不静默丢提示。
2291
+ const hintItems = this.consumeOwnerHints(session, message);
2292
+ const renderItems = hintItems.length > 0 ? [...hintItems, ...peerItems] : peerItems;
2293
+ const fallbackContent = (() => {
2294
+ if (!message.restartResume?.submitted || peerItems.length === 0)
2295
+ return message.content;
2296
+ const resumeIdx = peerItems.findIndex(item => item.kind === 'restart-resume');
2297
+ if (message.restartResume.pendingInterrupted && resumeIdx >= 0 && resumeIdx < peerItems.length - 1) {
2298
+ const resumeText = peerItems.slice(0, resumeIdx + 1).map(item => item.content).join('\n');
2299
+ const pendingText = peerItems.slice(resumeIdx + 1).map(item => item.content).join('\n');
2300
+ return `${resumeText}\n\n【新消息插入】\n\n${pendingText}\n\n【请根据前后消息酌情处理】`;
2301
+ }
2302
+ return peerItems.map(item => item.content).join('\n');
2303
+ })();
2304
+ try {
2305
+ if (message.restartResume?.pendingInterrupted) {
2306
+ const resumeIdx = renderItems.findIndex(item => item.kind === 'restart-resume');
2307
+ if (resumeIdx >= 0 && resumeIdx < renderItems.length - 1) {
2308
+ const resumeRender = renderMessageBody(renderItems.slice(0, resumeIdx + 1), kitCtx.vars, session.id);
2309
+ const pendingRender = renderMessageBody(renderItems.slice(resumeIdx + 1), kitCtx.vars, session.id);
2310
+ const body = [
2311
+ resumeRender.body.trim(),
2312
+ `【新消息插入】\n\n${pendingRender.body.trim()}\n\n【请根据前后消息酌情处理】`,
2313
+ ].filter(Boolean).join('\n\n');
2314
+ renderResult = {
2315
+ body,
2316
+ images: [...resumeRender.images, ...pendingRender.images],
2317
+ };
2318
+ }
2319
+ else {
2320
+ renderResult = renderMessageBody(renderItems, kitCtx.vars, session.id);
2321
+ }
2322
+ }
2323
+ else {
2324
+ renderResult = renderMessageBody(renderItems, kitCtx.vars, session.id);
2325
+ }
2326
+ if (renderResult.body.trim())
2327
+ effectivePrompt = wrapPrompt(renderResult.body);
2328
+ else
2329
+ effectivePrompt = wrapPrompt(composeHintFallback(hintItems, fallbackContent));
2330
+ }
2331
+ catch (e) {
2332
+ logger.warn(`[ResponseEngine] renderMessageBody failed, using raw content: ${e instanceof Error ? e.message : String(e)}`);
2333
+ effectivePrompt = wrapPrompt(composeHintFallback(hintItems, fallbackContent));
2334
+ }
2335
+ if (v2HandoffIds.length > 0 && renderResult?.body.trim())
2336
+ handoffPromptRendered = true;
2337
+ }
2338
+ // 空消息防护:在 agent 调用之前检查 prompt 是否为空
2339
+ // 防止空消息(或纯空格消息)浪费 API 调用
2340
+ if (!effectivePrompt.trim()) {
2341
+ logger.info(`[ResponseEngine] Skip agent call: empty prompt after render. session=${session.id} task=${taskId}`);
2342
+ if (turnLease)
2343
+ await this.turnCoordinator.finish(session, turnLease, 'cancelled', 'empty_prompt');
2344
+ this.sessionManager.clearProcessingIfTask(session.id, taskId);
2345
+ this.publishTriggerExecutionSkipped(message, 'empty_prompt', taskCausation);
2346
+ return;
2347
+ }
2348
+ if (v2HandoffIds.length > 0 && v2HandoffDirection === 'origin' && handoffPromptRendered && this.handoffRuntime && selfAid) {
2349
+ for (const handoffId of v2HandoffIds) {
2350
+ this.handoffRuntime.completeOriginContext(selfAid, handoffId);
2351
+ }
2352
+ }
2353
+ const taskRuntimeContext = {
2354
+ taskId,
2355
+ sessionId: session.id,
2356
+ messageId: message.messageId,
2357
+ channel: configChannelType,
2358
+ channelId: message.channelId,
2359
+ chatType: configChatType,
2360
+ selfAid,
2361
+ peerId: configActorId || undefined,
2362
+ peerName: peerName || undefined,
2363
+ peerType: message.peerType || session.metadata?.peerType || undefined,
2364
+ peerRole,
2365
+ threadId: session.threadId || undefined,
2366
+ handoffIds: v2HandoffIds.length > 0 && v2HandoffDirection === 'target' ? v2HandoffIds : undefined,
2367
+ causation: taskCausation,
2368
+ };
2369
+ this.activeTaskRuntimeContexts.set(session.id, taskRuntimeContext);
2370
+ runtimeEnv = buildTaskRuntimeEnv(taskRuntimeContext);
2371
+ modelOverride = {
2372
+ ...(modelOverride || {}),
2373
+ turn: {
2374
+ taskId,
2375
+ turnId: turnLease.turnId,
2376
+ generation: turnLease.generation,
2377
+ inputId: turnLease.inputId,
2378
+ },
2379
+ };
2380
+ if (this.agentDelegationRegistry && configActorId && selfAid && peerKey) {
2381
+ const delegationToken = this.agentDelegationRegistry.issue({
2382
+ sessionId: session.id,
2383
+ taskId,
2384
+ ...(message.messageId ? { messageId: message.messageId } : {}),
2385
+ actorId: configActorId,
2386
+ channel: authorizationIdentity?.originChannelKey ?? message.channel,
2387
+ channelType: configChannelType,
2388
+ chatType: configChatType,
2389
+ selfAid,
2390
+ peerKey,
2391
+ issuedRole: peerRole,
2392
+ });
2393
+ runtimeEnv[AGENT_DELEGATION_TOKEN_ENV] = delegationToken;
2394
+ }
2395
+ // 可重试错误(403/429/5xx/模型繁忙)按明确退避序列重试。
2396
+ const RETRY_DELAYS_MS = [5_000, 10_000, 30_000];
2397
+ const MAX_RETRIES = RETRY_DELAYS_MS.length;
2398
+ let runAttempt = 1;
2399
+ let consecutiveRetryFailures = 0;
2400
+ const recordRetryHealthError = async (retryError) => {
2401
+ if (!policy.accumulateErrors(chatType, identityRole))
2402
+ return;
2403
+ const retryErrorMessage = retryError instanceof Error ? retryError.message : String(retryError);
2404
+ const retryErrorType = prefixErrorType(ERROR_PREFIX.INFRA, classifyError(retryError));
2405
+ try {
2406
+ await this.sessionManager.recordError(session.id, retryErrorType, retryErrorMessage);
2407
+ markRetryHealthRecorded(retryError);
2408
+ }
2409
+ catch (statusError) {
2410
+ logger.error('[ResponseEngine] Failed to record retry health status:', statusError);
2411
+ }
2412
+ };
2413
+ while (true) {
2414
+ let streamRegistered = false;
2415
+ try {
2416
+ logger.info(`[ResponseEngine] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${runAttempt} consecutiveFailures=${consecutiveRetryFailures} agentSessionId=${session.agentSessionId ?? 'none'}`);
2417
+ const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, renderResult?.images.length ? renderResult.images : message.images, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
2418
+ agent.registerStream(streamKey, stream);
2419
+ streamRegistered = true;
2420
+ streamResult = await this.processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease);
2421
+ if (streamResult.isError && !streamHitContextLimit(streamResult)) {
2422
+ const streamErrorText = getStreamErrorText(streamResult);
2423
+ if (streamErrorText && isRetryableError(new Error(streamErrorText))) {
2424
+ renderer.discardPending();
2425
+ throw new Error(streamErrorText);
2426
+ }
2427
+ }
2428
+ // 探测成功(退避期内到达探测点且用的是 evolcore 模型)→ 清零降级状态
2429
+ if (fbState.fallbackActive && !skipEvolcoreModel && !usedFallback) {
2430
+ this.modelFallbackMap.delete(session.id);
2431
+ logger.info(`[ResponseEngine] Model probe succeeded, cleared fallback state for session=${session.id}`);
2432
+ }
2433
+ break; // 成功,跳出重试循环
2434
+ }
2435
+ catch (retryError) {
2436
+ if (streamRegistered) {
2437
+ agent.cleanupStream(streamKey);
2438
+ }
2439
+ if (permissionTerminalFailure) {
2440
+ const permissionError = new Error(permissionTerminalFailure.message);
2441
+ permissionError._errorAlreadySent = true;
2442
+ throw permissionError;
2443
+ }
2444
+ // 模型不可用:累计计数,本次切换到 baseAgentModel 立即重试,不让用户看到失败
2445
+ if (classifyError(retryError) === ErrorType.MODEL_UNAVAILABLE && !appliedTriggerModelOverride && evolcoreModelOverride?.model) {
2446
+ fbState.failCount++;
2447
+ if (fbState.failCount >= 2) {
2448
+ fbState.fallbackActive = true;
2449
+ fbState.messagesSinceFallback = 0;
2450
+ fbState.nextProbeAt = Math.min(Math.pow(2, fbState.failCount - 1), 8);
2451
+ }
2452
+ this.modelFallbackMap.set(session.id, fbState);
2453
+ logger.warn(`[ResponseEngine] Model unavailable: ${evolcoreModelOverride.model}, failCount=${fbState.failCount}, fallbackActive=${fbState.fallbackActive}`);
2454
+ // 切换到 baseAgentModel 重试(清除 model/effort,让 runQuery 使用 this.model;
2455
+ // 保留 permissionMode —— 它与模型无关,不能因模型降级而丢失)
2456
+ modelOverride = {
2457
+ ...(effectiveTriggerEffortOverride ? { effort: effectiveTriggerEffortOverride } : {}),
2458
+ permissionMode: effectivePermissionMode,
2459
+ ...(modelOverride?.sessionTitle ? { sessionTitle: modelOverride.sessionTitle } : {}),
2460
+ ...(modelOverride?.turn ? { turn: modelOverride.turn } : {}),
2461
+ };
2462
+ usedFallback = true;
2463
+ runAttempt++;
2464
+ continue;
2465
+ }
2466
+ if (isRetryableError(retryError)) {
2467
+ if (didRetryMakeProgress(retryError)) {
2468
+ consecutiveRetryFailures = 0;
2469
+ }
2470
+ await recordRetryHealthError(retryError);
2471
+ if (consecutiveRetryFailures < MAX_RETRIES) {
2472
+ // 检查中断状态:如果任务已被中断(/stop 或新消息),立即退出重试循环
2473
+ const interruptReason = this.interruptedSessions.get(session.id);
2474
+ if (interruptReason) {
2475
+ logger.info(`[ResponseEngine] Task interrupted during retry wait (reason=${interruptReason}), aborting retry loop session=${session.id}`);
2476
+ throw new Error('TASK_INTERRUPTED');
2477
+ }
2478
+ const delay = RETRY_DELAYS_MS[consecutiveRetryFailures];
2479
+ const nextRetryNumber = consecutiveRetryFailures + 1;
2480
+ consecutiveRetryFailures++;
2481
+ logger.warn(`[ResponseEngine] Retryable error (attempt ${runAttempt}, consecutive ${consecutiveRetryFailures}/${MAX_RETRIES}), retrying in ${delay}ms:`, retryError);
2482
+ renderer.addNotice(`API 不可用,${delay / 1000}秒后重试 ${nextRetryNumber}/${MAX_RETRIES}`, 'warn', 'retry', true);
2483
+ await renderer.flush();
2484
+ await new Promise(resolve => setTimeout(resolve, delay));
2485
+ // 延迟后再次检查中断状态(延迟期间可能收到中断信号)
2486
+ const postDelayInterrupt = this.interruptedSessions.get(session.id);
2487
+ if (postDelayInterrupt) {
2488
+ logger.info(`[ResponseEngine] Task interrupted after retry delay (reason=${postDelayInterrupt}), aborting retry loop session=${session.id}`);
2489
+ throw new Error('TASK_INTERRUPTED');
2490
+ }
2491
+ runAttempt++;
2492
+ continue;
2493
+ }
2494
+ markRetryExhausted(retryError, MAX_RETRIES);
2495
+ }
2496
+ throw retryError; // 不可重试或已耗尽重试次数
2497
+ }
2498
+ }
2499
+ }
2500
+ catch (error) {
2501
+ if (classifyError(error) === ErrorType.CONTEXT_TOO_LONG && session.agentSessionId && canCompactAgent(agent)) {
2502
+ streamResult = await this.retryAfterContextRecovery('上下文已自动压缩,请继续之前未完成的任务。', {
2503
+ streamKey,
2504
+ renderer,
2505
+ agent,
2506
+ session,
2507
+ absoluteProjectPath,
2508
+ effectiveSystemPrompt,
2509
+ modelOverride,
2510
+ runtimeEnv,
2511
+ resetTimer,
2512
+ shouldSuppress,
2513
+ proactive,
2514
+ turnLease,
2515
+ });
2516
+ }
2517
+ else {
2518
+ throw error;
2519
+ }
2520
+ }
2521
+ const pendingTimeoutError = timeoutControl?.claim();
2522
+ if (pendingTimeoutError) {
2523
+ throw pendingTimeoutError;
2524
+ }
2525
+ if (permissionTerminalFailure) {
2526
+ renderer.discardPending();
2527
+ streamResult = {
2528
+ ...streamResult,
2529
+ isError: true,
2530
+ subtype: `permission_${permissionTerminalFailure.reason}`,
2531
+ errors: [permissionTerminalFailure.message],
2532
+ lastReplyText: '',
2533
+ fullText: '',
2534
+ hasReceivedText: false,
2535
+ };
2536
+ }
2537
+ // prompt_too_long:SDK 以 complete 事件(非异常)返回,需在此处触发 compact
2538
+ // 检测条件:terminalReason 明确为 prompt_too_long,或文本/errors 包含相关错误文本
2539
+ const compactAgent = canCompactAgent(agent) ? agent : undefined;
2540
+ const isPromptTooLong = streamResult.isError && !!session.agentSessionId && !!compactAgent
2541
+ && streamHitContextLimit(streamResult);
2542
+ if (isPromptTooLong) {
2543
+ streamResult = await this.retryAfterContextRecovery('上下文已自动压缩,请继续之前未完成的任务。', {
2544
+ streamKey,
2545
+ renderer,
2546
+ agent,
2547
+ session,
2548
+ absoluteProjectPath,
2549
+ effectiveSystemPrompt,
2550
+ modelOverride,
2551
+ runtimeEnv,
2552
+ resetTimer,
2553
+ shouldSuppress,
2554
+ proactive,
2555
+ turnLease,
2556
+ });
2557
+ // 重试后仍然 prompt_too_long:显示友好提示
2558
+ const retryStillTooLong = streamResult.isError && streamHitContextLimit(streamResult);
2559
+ if (retryStillTooLong) {
2560
+ renderer.addNotice(getContextTooLongHint(agent), 'warn', 'context-too-long', true);
2561
+ }
2562
+ }
2563
+ else if (streamResult.isError && streamHitContextLimit(streamResult)) {
2564
+ // 上下文过长但无法 auto-compact(无 session ID 或 agent 不支持),显示友好提示
2565
+ renderer.addNotice(getContextTooLongHint(agent), 'warn', 'context-too-long', true);
2566
+ }
2567
+ if (!streamResult.isError) {
2568
+ const commitEvidence = this.buildTurnCommitEvidence(session, streamResult);
2569
+ const commitDecision = this.turnCoordinator.evaluateCommit(turnLease, commitEvidence);
2570
+ if (!commitDecision.ok && commitDecision.reason === 'stale_generation') {
2571
+ agent.cleanupStream(streamKey);
2572
+ this.sessionManager.clearProcessingIfTask(session.id, taskId);
2573
+ if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
2574
+ this.activeTaskRuntimeContexts.delete(session.id);
2575
+ }
2576
+ this.agentDelegationRegistry?.revokeTask(session.id, taskId);
2577
+ logger.info(`[ResponseEngine] Stale turn stopped before publish: session=${session.id} task=${taskId}`);
2578
+ await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_generation', taskCausation);
2579
+ return;
2580
+ }
2581
+ if (!commitDecision.ok) {
2582
+ if (commitDecision.reason === 'stale_generation') {
2583
+ agent.cleanupStream(streamKey);
2584
+ this.sessionManager.clearProcessingIfTask(session.id, taskId);
2585
+ if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
2586
+ this.activeTaskRuntimeContexts.delete(session.id);
2587
+ }
2588
+ this.agentDelegationRegistry?.revokeTask(session.id, taskId);
2589
+ logger.info(`[ResponseEngine] Stale turn stopped before protocol rejection: session=${session.id} task=${taskId}`);
2590
+ await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_generation', taskCausation);
2591
+ return;
2592
+ }
2593
+ const reason = streamResult.protocolIncompleteReason || commitDecision.reason;
2594
+ renderer.discardPending();
2595
+ streamResult = {
2596
+ ...streamResult,
2597
+ isError: true,
2598
+ subtype: 'protocol_incomplete',
2599
+ terminalReason: 'protocol_incomplete',
2600
+ protocolIncompleteReason: reason,
2601
+ errors: [`Agent 协议未闭合(${reason}),任务未提交`],
2602
+ lastReplyText: '',
2603
+ fullText: '',
2604
+ hasReceivedText: false,
2605
+ };
2606
+ }
2607
+ }
2608
+ // 处理文件标记 - 支持 [SEND_FILE:path] 和 [SEND_FILE:channel:path]
2609
+ // 注意:始终扫描全部文本(含中间轮),因为文件标记可能出现在任意轮次
2610
+ // suppressed 模式下 renderer 只有最后一轮文本,需要用 streamResult.fullText(SDK 全文)兜底
2611
+ // proactive 模式:agent 主动调用 ctl file 发送文件,跳过标记处理
2612
+ // [迁移点6] afterProcess:文件标记(interactive)+ Unknown skill 兜底(proactive)
2613
+ // 由响应模式插件统一处理(InteractiveMode.afterProcess / ProactiveMode.afterProcess)
2614
+ if (resolvedMode?.mode.afterProcess) {
2615
+ const flusherText = renderer.getFinalText();
2616
+ const fullText = flusherText.length >= (streamResult.fullText?.length || 0) ? flusherText : streamResult.fullText;
2617
+ await resolvedMode.mode.afterProcess({
2618
+ session,
2619
+ fullText,
2620
+ streamResult,
2621
+ send: async (payload) => {
2622
+ if (!this.turnCoordinator.isCurrent(turnLease))
2623
+ return;
2624
+ const isCurrentlyBackground = this.isBackgroundSession(session, message.channel, message.channelId);
2625
+ if (!isCurrentlyBackground) {
2626
+ await adapter.send({ ...envelope, replyContext: capturedReplyContext }, payload);
2627
+ if (payload.kind === 'result.text') {
2628
+ logger.info(`[ResponseEngine] afterProcess sent: task=${taskId} text="${payload.text.slice(0, 60)}"`);
2629
+ }
2630
+ }
2631
+ },
2632
+ channelCapabilities: { file: !!adapter.capabilities?.file, thought: !!adapter.capabilities?.thought },
2633
+ processFileMarkers: async (scanText) => {
2634
+ if (!this.turnCoordinator.isCurrent(turnLease))
2635
+ return 0;
2636
+ // 仅支持公开文件标记语法:[SEND_FILE:path] 或 [SEND_FILE:channel:path]。
2637
+ const SEND_FILE_RE = options?.fileMarkerPattern ?? /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g;
2638
+ const sendFileMatches = [...scanText.matchAll(SEND_FILE_RE)];
2639
+ if (sendFileMatches.length === 0)
2640
+ return 0;
2641
+ // 记录所有文件标记(快照用)
2642
+ snapshot.set(session.id, taskId, {
2643
+ fileMarkers: sendFileMatches.map(m => (m.length >= 3 ? (m[2] ?? m[1]) : m[1]).trim()),
2644
+ });
2645
+ let sent = 0;
2646
+ for (const match of sendFileMatches) {
2647
+ if (!this.turnCoordinator.isCurrent(turnLease))
2648
+ break;
2649
+ const hasChannelGroup = match.length >= 3;
2650
+ let targetSpec = hasChannelGroup ? (match[1] ?? undefined) : undefined;
2651
+ let filePath = (hasChannelGroup ? match[2] : match[1]).trim();
2652
+ // 白名单校验:targetSpec 必须是已注册通道,否则视为路径的一部分(如 Windows 盘符 C:)
2653
+ if (targetSpec && !this.channels.has(targetSpec) && !this.channelTypeMap.has(targetSpec)) {
2654
+ filePath = `${targetSpec}:${filePath}`;
2655
+ targetSpec = undefined;
2656
+ }
2657
+ // 跳过占位符路径(如 /path/to/file.txt)
2658
+ if (this.isPlaceholderPath(filePath)) {
2659
+ logger.info(`[${adapter.channelName}] Skipped placeholder file marker: [SEND_FILE:${filePath}]`);
2660
+ continue;
2661
+ }
2662
+ // 解析目标通道
2663
+ let targetInfo = targetSpec ? this.channels.get(targetSpec) : channelInfo;
2664
+ const targetLabel = targetSpec || message.channel;
2665
+ // 按 channelType 查找首个匹配的实例
2666
+ if (targetSpec && !targetInfo) {
2667
+ const instanceName = this.channelTypeMap.get(targetSpec);
2668
+ if (instanceName)
2669
+ targetInfo = this.channels.get(instanceName);
2670
+ }
2671
+ const currentChannelType = channelInfo.options?.channelType || adapter.channelName;
2672
+ const isCrossChannel = targetSpec && targetSpec !== message.channel && targetSpec !== currentChannelType;
2673
+ // 跨通道仅限 owner
2674
+ if (isCrossChannel && session.identity?.role !== 'owner') {
2675
+ await adapter.send(envelope, { kind: 'system.error', text: `❌ 跨通道发送仅限管理员`, subtype: 'fatal' });
2676
+ continue;
2677
+ }
2678
+ // 解析文件路径
2679
+ const agentProjectPath = session.projectPath || process.cwd();
2680
+ const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(agentProjectPath, filePath);
2681
+ if (!fs.existsSync(resolvedPath)) {
2682
+ logger.warn(`[${adapter.channelName}] File not found: ${resolvedPath}`);
2683
+ await adapter.send(envelope, { kind: 'system.error', text: `⚠️ 文件未找到: ${filePath}`, subtype: 'fatal' });
2684
+ continue;
2685
+ }
2686
+ // 找目标 adapter
2687
+ if (!targetInfo) {
2688
+ await adapter.send(envelope, { kind: 'system.error', text: `❌ 通道 ${targetLabel} 未启用或不存在`, subtype: 'channel_down' });
2689
+ continue;
2690
+ }
2691
+ if (!targetInfo.adapter.capabilities?.file) {
2692
+ await adapter.send(envelope, { kind: 'system.error', text: `❌ 通道 ${targetLabel} 不支持文件发送`, subtype: 'capability' });
2693
+ continue;
2694
+ }
2695
+ // 找目标 channelId
2696
+ let targetChannelId = message.channelId;
2697
+ if (isCrossChannel) {
2698
+ const targetAdapterName = targetInfo.adapter.channelName;
2699
+ const targetChannelType = targetInfo.options?.channelType || targetAdapterName;
2700
+ const targetChannelKey = targetInfo.adapter.channelKey || targetAdapterName;
2701
+ const targetAgent = this.agentRegistry?.resolveByChannel(targetChannelKey);
2702
+ const ownerPeerId = targetAgent
2703
+ ? getFirstStaticAgentOwner(targetAgent.aid)
2704
+ : undefined;
2705
+ targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannelType, ownerPeerId) ?? '') : '';
2706
+ if (!targetChannelId) {
2707
+ await adapter.send(envelope, { kind: 'system.error', text: `❌ 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`, subtype: 'channel_down' });
2708
+ continue;
2709
+ }
2710
+ }
2711
+ // 发送文件
2712
+ logger.info(`[${adapter.channelName}] Sending file via ${targetInfo.adapter.channelName}: ${resolvedPath}`);
2713
+ try {
2714
+ await targetInfo.adapter.send(buildEnvelope({ taskId, channel: targetInfo.adapter.channelName, channelId: targetChannelId, agentName: agentNameForStats, replyContext: capturedReplyContext }), { kind: 'result.file', filePath: resolvedPath });
2715
+ this.eventBus.publish({ type: 'runner:file-sent', sessionId: session.id, filePath: resolvedPath, channel: targetInfo.adapter.channelName });
2716
+ sent++;
2717
+ if (isCrossChannel) {
2718
+ await adapter.send(envelope, { kind: 'system.notice', text: `📎 文件已通过 ${targetLabel} 发送`, subtype: 'health' });
2719
+ }
2720
+ }
2721
+ catch (error) {
2722
+ logger.error(`[${adapter.channelName}] Failed to send file: ${resolvedPath}`, error);
2723
+ await adapter.send(envelope, { kind: 'system.error', text: `❌ 文件发送失败: ${filePath}`, subtype: 'fatal' });
2724
+ }
2725
+ }
2726
+ return sent;
2727
+ },
2728
+ logger,
2729
+ });
2730
+ }
2731
+ // 最终回复文本:suppressed 模式或无 text 事件时需要兜底添加
2732
+ const finalReplyText = streamResult.lastReplyText || streamResult.fullText;
2733
+ if (!this.turnCoordinator.isCurrent(turnLease)) {
2734
+ agent.cleanupStream(streamKey);
2735
+ this.sessionManager.clearProcessingIfTask(session.id, taskId);
2736
+ if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
2737
+ this.activeTaskRuntimeContexts.delete(session.id);
2738
+ }
2739
+ this.agentDelegationRegistry?.revokeTask(session.id, taskId);
2740
+ logger.info(`[ResponseEngine] Stale turn stopped before final flush: session=${session.id} task=${taskId}`);
2741
+ await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_turn', taskCausation);
2742
+ return;
2743
+ }
2744
+ if (finalReplyText) {
2745
+ if (shouldSuppress() || !streamResult.hasReceivedText) {
2746
+ renderer.addText(finalReplyText);
2747
+ }
2748
+ }
2749
+ // 先清理流和处理中状态(保证即使 flush 卡住,session 也不会永久处于"处理中")
2750
+ agent.cleanupStream(streamKey);
2751
+ logger.info(`[ResponseEngine] agent.cleanupStream ok: session=${session.id} task=${taskId}`);
2752
+ this.sessionManager.clearProcessingIfTask(session.id, taskId);
2753
+ if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
2754
+ this.activeTaskRuntimeContexts.delete(session.id);
2755
+ }
2756
+ this.agentDelegationRegistry?.revokeTask(session.id, taskId);
2757
+ logger.info(`[ResponseEngine] session ${session.id} processing cleared task=${taskId}`);
2758
+ // 降级模型回复末尾追加标记(代码层硬注入,不依赖模型输出)
2759
+ const usingFallback = usedFallback || (skipEvolcoreModel && agentModel != null);
2760
+ if (usingFallback && agentModel) {
2761
+ const curFbState = this.modelFallbackMap.get(session.id);
2762
+ const showHint = curFbState && curFbState.nextProbeAt >= 8 && !curFbState.hintShown;
2763
+ const suffix = showHint
2764
+ ? `\n\n---\n⚠️ [降级模型: ${agentModel} | 可告诉我"帮我检查可用模型"来诊断]`
2765
+ : `\n\n---\n⚠️ [降级模型: ${agentModel}]`;
2766
+ renderer.addText(suffix);
2767
+ if (showHint && curFbState) {
2768
+ curFbState.hintShown = true;
2769
+ this.modelFallbackMap.set(session.id, curFbState);
2770
+ }
2771
+ }
2772
+ // 被用户中断(新消息打断)时跳过 flush — 新 task 已接管渠道,旧 task 的 flush 无意义且可能卡住
2773
+ const preFlushInterrupt = this.interruptedSessions.get(session.id);
2774
+ if (preFlushInterrupt === 'new_message' || preFlushInterrupt === 'stop' || preFlushInterrupt === 'recalled') {
2775
+ logger.info(`[ResponseEngine] Skipping flush for interrupted task=${taskId} reason=${preFlushInterrupt}`);
2776
+ }
2777
+ else {
2778
+ // Flush 剩余内容(文件标记已在 flush 时自动移除)
2779
+ await renderer.flush(true);
2780
+ }
2781
+ if (!streamResult.isError) {
2782
+ const commitDecision = await this.turnCoordinator.commit(session, turnLease, this.buildTurnCommitEvidence(session, streamResult));
2783
+ if (!commitDecision.ok) {
2784
+ logger.info(`[ResponseEngine] Turn commit rejected after flush: session=${session.id} task=${taskId} reason=${commitDecision.reason}`);
2785
+ if (commitDecision.reason === 'stale_generation') {
2786
+ await this.publishTriggerExecutionInterrupted(message, adapter, envelope, commitDecision.reason, taskCausation);
2787
+ }
2788
+ else {
2789
+ this.publishTriggerExecutionFailure(message, `turn_commit_rejected:${commitDecision.reason}`, {
2790
+ messageId,
2791
+ causation: taskCausation,
2792
+ });
2793
+ }
2794
+ return;
2795
+ }
2796
+ }
2797
+ else {
2798
+ await this.turnCoordinator.finish(session, turnLease, 'failed', streamResult.protocolIncompleteReason || streamResult.terminalReason || streamResult.subtype);
2799
+ }
2800
+ // 注意:不在此处清除 interruptedSessions,由下一条消息的 prompt 包装逻辑消费
2801
+ const interruptReason = this.interruptedSessions.get(session.id);
2802
+ if (streamResult.isError) {
2803
+ // Agent 流正常结束但任务结果失败(权限被拒、max turns、工具链失败等)
2804
+ const errorSummary = streamResult.errors?.join('; ') || '任务执行失败';
2805
+ const userErrorSummary = streamResult.subtype === 'protocol_incomplete'
2806
+ ? errorSummary
2807
+ : streamHitContextLimit(streamResult)
2808
+ ? getContextTooLongHint(agent)
2809
+ : getStreamErrorMessage(streamResult, false);
2810
+ const rawSubtype = streamResult.subtype || 'agent_error';
2811
+ const errorType = prefixErrorType(ERROR_PREFIX.AGENT, rawSubtype);
2812
+ // 用户主动打断(新消息/​/stop/​撤回)会让 SDK 流在工具调用中途被掐断,
2813
+ // 末尾 result message 形状异常并被标记为 error(含 SDK 内部 ede_diagnostic 串)。
2814
+ // 这不是真正的失败,不应把诊断串暴露给用户,也不计入错误统计。
2815
+ const isUserInterrupt = interruptReason === 'new_message' || interruptReason === 'stop' || interruptReason === 'recalled';
2816
+ if (!isUserInterrupt && !permissionTerminalFailure) {
2817
+ await adapter.send(envelope, { kind: 'result.error', text: userErrorSummary, reason: rawSubtype }).catch(() => { });
2818
+ await adapter.send(envelope, { kind: 'status.error', metadata: { errorType: rawSubtype } }).catch(() => { });
2819
+ this.touchAgentActivity(channelKey);
2820
+ }
2821
+ if (isUserInterrupt) {
2822
+ // 用户打断:打断本身已由 message-queue 发过 task:interrupted 事件,
2823
+ // 这里不再补发 task:error(否则同一次打断被记两遍且错误归类为 error)。
2824
+ // 仅记 info 日志收尾。注意:task:interrupted 已填充 interruptedSessions,
2825
+ // stats 侧已据此收尾任务生命周期,无需在此重复发事件。
2826
+ logger.info(`[${message.channel}] Stream result error suppressed (user interrupt: ${interruptReason}): ${errorSummary}`);
2827
+ logger.message({
2828
+ msgId: messageId,
2829
+ sessionId: session.id,
2830
+ dir: 'inbound',
2831
+ status: 'interrupted',
2832
+ error: errorSummary,
2833
+ terminalReason: streamResult.terminalReason
2834
+ });
2835
+ await this.publishTriggerExecutionInterrupted(message, adapter, envelope, interruptReason ?? 'interrupted', taskCausation);
2836
+ }
2837
+ else {
2838
+ this.publishTriggerExecutionFailure(message, errorSummary, { messageId, causation: taskCausation });
2839
+ this.eventBus.publish({
2840
+ type: 'task:error',
2841
+ sessionId: session.id,
2842
+ error: errorSummary,
2843
+ errorType,
2844
+ agentName: agentNameForStats,
2845
+ terminalReason: streamResult.terminalReason,
2846
+ causation: taskCausation,
2847
+ });
2848
+ recordCausationSpan(taskCausation, 'task.run', { status: 'failed', refs: { taskId, sessionId: session.id, messageId }, reason: errorType });
2849
+ // 系统级 subtype 仍累计错误计数,供 /status 诊断使用
2850
+ if (isInfraError(rawSubtype, streamResult.terminalReason)) {
2851
+ const chatType = message.chatType || 'private';
2852
+ const identityRole = session.identity?.role || 'none';
2853
+ const { policy } = channelInfo;
2854
+ if (policy.accumulateErrors(chatType, identityRole)) {
2855
+ await this.sessionManager.recordError(session.id, errorType, errorSummary);
2856
+ }
2857
+ }
2858
+ logger.message({
2859
+ msgId: messageId,
2860
+ sessionId: session.id,
2861
+ dir: 'inbound',
2862
+ status: 'failed',
2863
+ error: errorSummary,
2864
+ terminalReason: streamResult.terminalReason
2865
+ });
2866
+ }
2867
+ }
2868
+ else {
2869
+ // 真正的成功
2870
+ if (!this.turnCoordinator.canPublish(turnLease)) {
2871
+ logger.info(`[ResponseEngine] Suppressing stale success path: session=${session.id} task=${taskId}`);
2872
+ await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_turn', taskCausation);
2873
+ return;
2874
+ }
2875
+ const durationMs = Date.now() - startTime;
2876
+ const statsModel = streamResult.lastModelCall?.model
2877
+ || streamResult.model
2878
+ || streamResult.contextUsage?.model
2879
+ || modelOverride?.model
2880
+ || agentModel
2881
+ || 'unknown';
2882
+ // ── Stats: 写入 usage_events(在 status.completed 之前,以便带上 cost) ──
2883
+ // cost 含原价(official)与网关实际价(gateway)两套,由 insertUsageEvent 写库时一并算出。
2884
+ let turnCost = { official: null, gateway: null };
2885
+ let statsCacheHitRate = 0;
2886
+ if (streamResult.tokenUsage) {
2887
+ try {
2888
+ const statsAgentAid = session.selfAID || message.selfAID || '';
2889
+ const statsPeerKey = peerKey
2890
+ || (configActorId
2891
+ ? formatPeerKey(configChannelType, configActorId)
2892
+ : formatPeerKey(configChannelType, message.channelId));
2893
+ const ctxPct = streamResult.contextUsage?.percentage;
2894
+ const event = normalizeUsage(streamResult.tokenUsage, {
2895
+ ts: Date.now(),
2896
+ agent_aid: statsAgentAid,
2897
+ peer_key: statsPeerKey,
2898
+ usage_subject_key: formatUsageSubjectKey(configChannelType, configChatType, configActorId || message.channelId, configActorId || undefined),
2899
+ role: peerRole,
2900
+ peer_type: session.chatType || undefined,
2901
+ session_id: session.id,
2902
+ model: statsModel,
2903
+ turns: streamResult.numTurns,
2904
+ duration_ms: durationMs,
2905
+ context_window_pct: ctxPct,
2906
+ });
2907
+ // 写库即得价格对(原价 + 网关价),无需再调 calcCost 重复计算。
2908
+ // 网关价格缓存(/v1/models 的 pricing/effective_pricing,1h TTL)由当前 runner 提供。
2909
+ const gwPricing = agent.getGatewayPricing?.();
2910
+ const prices = insertUsageEvent(resolveRoot(), event, gwPricing);
2911
+ turnCost = { official: prices.official, gateway: prices.gateway };
2912
+ // 逐次大模型调用明细落库(model_calls 表)
2913
+ if (streamResult.modelCalls?.length) {
2914
+ const mcRows = streamResult.modelCalls.map(mc => ({
2915
+ ts: event.ts,
2916
+ task_id: taskId,
2917
+ session_id: session.id,
2918
+ agent_session_id: session.agentSessionId ?? undefined,
2919
+ agent_aid: statsAgentAid,
2920
+ peer_key: statsPeerKey,
2921
+ call_index: mc.call_index,
2922
+ model: mc.model || statsModel,
2923
+ request_id: mc.request_id,
2924
+ message_id: mc.message_id,
2925
+ input_tokens: mc.tokenUsage.input_tokens ?? 0,
2926
+ output_tokens: mc.tokenUsage.output_tokens ?? 0,
2927
+ cache_creation_tokens: mc.tokenUsage.cache_creation_input_tokens ?? 0,
2928
+ cache_read_tokens: mc.tokenUsage.cache_read_input_tokens ?? 0,
2929
+ context_tokens: mc.contextUsage?.totalTokens,
2930
+ max_tokens: mc.contextUsage?.maxTokens,
2931
+ auto_compact_tokens: mc.contextUsage?.autoCompactTokens,
2932
+ degraded: mc.degraded ? 1 : 0,
2933
+ }));
2934
+ insertModelCalls(resolveRoot(), mcRows);
2935
+ }
2936
+ const totalIn = event.input_tokens + event.cache_read_tokens;
2937
+ statsCacheHitRate = totalIn > 0 ? Math.round((event.cache_read_tokens / totalIn) * 100) / 100 : 0;
2938
+ }
2939
+ catch (e) {
2940
+ logger.debug(`[ResponseEngine] Stats write failed (non-fatal): ${e}`);
2941
+ }
2942
+ }
2943
+ // 会话累计 + model spec(用于 status.completed 统计细目)
2944
+ // 直接读已落库的 cost 列(querySessionSummary 单条 SUM,含原价 + 网关价),不再逐行 calcCost。
2945
+ let sessionStats;
2946
+ let modelSpec;
2947
+ try {
2948
+ const { resolveModelSpec } = await import('../../stats/billing.js');
2949
+ const { querySessionSummary } = await import('../../stats/query.js');
2950
+ modelSpec = resolveModelSpec(resolveRoot(), statsModel);
2951
+ const sum = querySessionSummary(resolveRoot(), session.id);
2952
+ if (sum.calls > 0) {
2953
+ sessionStats = {
2954
+ input_tokens: sum.input_tokens,
2955
+ output_tokens: sum.output_tokens,
2956
+ cache_read_tokens: sum.cache_read_tokens,
2957
+ cache_creation_tokens: sum.cache_creation_tokens,
2958
+ // 顶层 cost_usd/cost_cny 保持向后兼容 = 网关实际价
2959
+ cost_usd: sum.cost_gateway_usd,
2960
+ cost_cny: sum.cost_gateway_cny,
2961
+ call_count: sum.calls,
2962
+ cost: {
2963
+ official: { usd: sum.cost_official_usd, cny: sum.cost_official_cny },
2964
+ gateway: { usd: sum.cost_gateway_usd, cny: sum.cost_gateway_cny },
2965
+ },
2966
+ };
2967
+ }
2968
+ }
2969
+ catch { /* non-fatal */ }
2970
+ if (!this.turnCoordinator.canPublish(turnLease)) {
2971
+ logger.info(`[ResponseEngine] Suppressing stale success after stats: session=${session.id} task=${taskId}`);
2972
+ await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_turn', taskCausation);
2973
+ return;
2974
+ }
2975
+ {
2976
+ this.touchAgentActivity(channelKey);
2977
+ if (interruptReason) {
2978
+ await this.publishTriggerExecutionInterrupted(message, adapter, envelope, interruptReason, taskCausation);
2979
+ }
2980
+ else {
2981
+ // cost 同时给原价(official)与网关实际价(gateway);顶层 cost_usd/cost_cny 保持向后兼容 = 网关价。
2982
+ const gatewayUsd = turnCost.gateway?.usd ?? turnCost.official?.usd ?? 0;
2983
+ const gatewayCny = turnCost.gateway?.cny ?? turnCost.official?.cny ?? 0;
2984
+ const turnCostBlock = {
2985
+ official: { usd: turnCost.official?.usd ?? 0, cny: turnCost.official?.cny ?? 0 },
2986
+ gateway: { usd: gatewayUsd, cny: gatewayCny },
2987
+ };
2988
+ // 最后一次访问:本轮可能有多次大模型调用(numTurns>1),整轮的 turnCostBlock 不等于
2989
+ // 最后一次的价。用 lastModelCall.tokenUsage 单独走一遍 resolvePrices(与落库同一套定价逻辑),
2990
+ // 得到「最后一次访问」自己的原价 + 网关价。
2991
+ let lastModelCall = streamResult.lastModelCall;
2992
+ if (lastModelCall?.tokenUsage) {
2993
+ try {
2994
+ const lastModel = lastModelCall.model || statsModel;
2995
+ const lastEvent = normalizeUsage(lastModelCall.tokenUsage, {
2996
+ ts: Date.now(), agent_aid: '', peer_key: '', session_id: session.id,
2997
+ model: lastModel, turns: 1,
2998
+ });
2999
+ const lp = resolvePrices(resolveRoot(), lastEvent, agent.getGatewayPricing?.());
3000
+ const lpGwUsd = lp.gateway?.usd ?? lp.official?.usd ?? 0;
3001
+ const lpGwCny = lp.gateway?.cny ?? lp.official?.cny ?? 0;
3002
+ lastModelCall = { ...lastModelCall, cost: {
3003
+ official: { usd: lp.official?.usd ?? 0, cny: lp.official?.cny ?? 0 },
3004
+ gateway: { usd: lpGwUsd, cny: lpGwCny },
3005
+ } };
3006
+ }
3007
+ catch { /* 价格解析失败时不附 cost,不影响回执 */ }
3008
+ }
3009
+ const completedMetadata = {
3010
+ durationMs,
3011
+ ttftMs: streamResult.ttftMs,
3012
+ numTurns: streamResult.numTurns,
3013
+ toolCallCount: streamResult.toolUseCount ?? 0,
3014
+ tokenUsage: streamResult.tokenUsage,
3015
+ contextUsage: streamResult.contextUsage,
3016
+ lastModelCall,
3017
+ cost_usd: gatewayUsd,
3018
+ cost_cny: gatewayCny,
3019
+ cost: turnCostBlock,
3020
+ cache_hit_rate: statsCacheHitRate,
3021
+ model_spec: modelSpec,
3022
+ session_total: sessionStats,
3023
+ queue: {
3024
+ pending: this.messageQueue?.getQueueLength(session.id) ?? 0,
3025
+ processing: this.messageQueue?.isProcessing(session.id) ? 1 : 0,
3026
+ },
3027
+ };
3028
+ renderer.addLifecycle('completed', completedMetadata);
3029
+ renderer.flushActivitiesOnly().catch(() => { });
3030
+ adapter.send(envelope, { kind: 'status.completed', metadata: completedMetadata }).catch(() => { });
3031
+ }
3032
+ }
3033
+ if (!this.turnCoordinator.canPublish(turnLease)) {
3034
+ logger.info(`[ResponseEngine] Suppressing stale trigger completion: session=${session.id} task=${taskId}`);
3035
+ await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_turn', taskCausation);
3036
+ return;
3037
+ }
3038
+ if (!interruptReason) {
3039
+ this.publishTriggerExecutionCompleted(message, messageId, durationMs, taskCausation);
3040
+ }
3041
+ if (!this.turnCoordinator.canPublish(turnLease))
3042
+ return;
3043
+ await this.sessionManager.recordSuccess(session.id);
3044
+ if (!this.turnCoordinator.canPublish(turnLease)) {
3045
+ logger.info(`[ResponseEngine] Suppressing stale completion event: session=${session.id} task=${taskId}`);
3046
+ return;
3047
+ }
3048
+ this.eventBus.publish({
3049
+ type: 'task:completed',
3050
+ sessionId: session.id,
3051
+ channel: message.channel,
3052
+ channelId: message.channelId,
3053
+ terminalReason: streamResult.terminalReason,
3054
+ finalText: streamResult.lastReplyText || undefined,
3055
+ durationMs: Date.now() - startTime,
3056
+ agentName: agentNameForStats,
3057
+ numTurns: streamResult.numTurns,
3058
+ timestamp: Date.now(),
3059
+ causation: taskCausation,
3060
+ });
3061
+ recordCausationSpan(taskCausation, 'task.run', { status: 'completed', refs: { taskId, sessionId: session.id, messageId } });
3062
+ // 记录处理完成
3063
+ logger.message({
3064
+ msgId: messageId,
3065
+ sessionId: session.id,
3066
+ dir: 'inbound',
3067
+ status: 'completed',
3068
+ duration: Date.now() - startTime
3069
+ });
3070
+ // 写入消息记录(出方向)已下沉到 aun.ts:deliverTextEntry,
3071
+ // 所有 message.send 成功后统一写入 messages.jsonl,此处不再重复写入。
3072
+ }
3073
+ const isFinallyBackground = this.isBackgroundSession(session, message.channel, message.channelId);
3074
+ if (isFinallyBackground && !streamResult.isError) {
3075
+ if (!this.turnCoordinator.canPublish(turnLease))
3076
+ return;
3077
+ this.messageCache.addEvent(session.id, {
3078
+ type: 'completed',
3079
+ message: streamResult.lastReplyText || streamResult.fullText || '',
3080
+ timestamp: Date.now(),
3081
+ metadata: { duration: Date.now() - startTime },
3082
+ });
3083
+ const projectName = path.basename(session.projectPath);
3084
+ const count = this.messageCache.getCount(session.id);
3085
+ await adapter.send(envelope, { kind: 'system.notice', text: `[\u540e\u53f0-${projectName}] \u2713 任务完成 (${count}条消息已缓存)`, subtype: 'background' });
3086
+ }
3087
+ // 记录发送响应
3088
+ logger.message({
3089
+ msgId: `${messageId}_reply`,
3090
+ sessionId: session.id,
3091
+ dir: 'outbound',
3092
+ status: 'sent'
3093
+ });
3094
+ }
3095
+ catch (error) {
3096
+ const authoritativeTimeoutError = timeoutControl?.claim();
3097
+ if (authoritativeTimeoutError) {
3098
+ error = authoritativeTimeoutError;
3099
+ await timeoutControl?.barrier;
3100
+ }
3101
+ const handlingTimeout = error instanceof Error
3102
+ && (error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT');
3103
+ if (handlingTimeout) {
3104
+ this.timeoutErrors.delete(session.id);
3105
+ }
3106
+ const leaseWasCurrent = !!turnLease && this.turnCoordinator.isCurrent(turnLease);
3107
+ // 清理流和处理中状态(异常时也要清除)
3108
+ agent.cleanupStream(streamKey);
3109
+ logger.info(`[ResponseEngine] agent.cleanupStream ok (on error): session=${session.id} task=${taskId}`);
3110
+ try {
3111
+ this.sessionManager.clearProcessingIfTask(session.id, taskId);
3112
+ if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
3113
+ this.activeTaskRuntimeContexts.delete(session.id);
3114
+ }
3115
+ this.agentDelegationRegistry?.revokeTask(session.id, taskId);
3116
+ logger.info(`[ResponseEngine] session ${session.id} processing cleared (on error) task=${taskId}`);
3117
+ }
3118
+ catch { }
3119
+ // 注意:不在此处清除 interruptedSessions,由下一条消息的 prompt 包装逻辑消费
3120
+ // 区分超时 / 中断 / 错误
3121
+ const errType = classifyError(error);
3122
+ const interruptReason = this.interruptedSessions.get(session.id);
3123
+ const isTotalExecutionTimeout = error instanceof Error && error.message === 'TOTAL_EXECUTION_TIMEOUT';
3124
+ const isTimeout = error instanceof Error && error.message === 'SDK_TIMEOUT';
3125
+ const canPublishTerminal = !turnLease || this.turnCoordinator.canPublish(turnLease);
3126
+ const staleSupersededTurn = !!turnLease
3127
+ && !canPublishTerminal
3128
+ && interruptReason !== 'timeout'
3129
+ && !isTimeout
3130
+ && !isTotalExecutionTimeout;
3131
+ const isUserInterrupt = interruptReason === 'new_message'
3132
+ || interruptReason === 'stop'
3133
+ || interruptReason === 'recalled'
3134
+ || staleSupersededTurn;
3135
+ const errorMsg = error instanceof Error ? error.message : String(error);
3136
+ if (isUserInterrupt) {
3137
+ await this.publishTriggerExecutionInterrupted(message, adapter, envelope, interruptReason ?? 'stale_turn', taskCausation);
3138
+ }
3139
+ else {
3140
+ this.publishTriggerExecutionFailure(message, errorMsg, {
3141
+ messageId,
3142
+ causation: taskCausation,
3143
+ });
3144
+ }
3145
+ if (leaseWasCurrent && turnLease) {
3146
+ await this.turnCoordinator.finish(session, turnLease, 'failed', error instanceof Error ? error.message : String(error));
3147
+ }
3148
+ const totalExecutionMs = this.totalExecutionLimitMs();
3149
+ const procStatus = errType === ErrorType.SDK_TIMEOUT || isTotalExecutionTimeout ? 'timeout'
3150
+ : errType === ErrorType.STREAM_ERROR ? 'interrupted'
3151
+ : 'error';
3152
+ const daemonTrigger = this.isTrustedDaemonTrigger(message);
3153
+ const statusPayload = procStatus === 'timeout'
3154
+ ? { kind: 'status.timeout', metadata: isTotalExecutionTimeout
3155
+ ? { totalExecutionMs }
3156
+ : { idleSec: getLastIdleSec?.() || undefined } }
3157
+ : procStatus === 'interrupted'
3158
+ ? { kind: 'status.interrupted', metadata: { reason: 'stream_error' } }
3159
+ : { kind: 'status.error' };
3160
+ // 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送中断/错误提示
3161
+ if (!isUserInterrupt && !daemonTrigger) {
3162
+ await adapter.send(envelope, statusPayload).catch(() => { });
3163
+ this.touchAgentActivity(channelKey);
3164
+ }
3165
+ if (!isUserInterrupt && daemonTrigger)
3166
+ this.touchAgentActivity(channelKey);
3167
+ // 用户主动中断时降级日志;其余仍按 error 记录
3168
+ if (isUserInterrupt) {
3169
+ logger.info(`[${message.channel}] Interrupted by user (${interruptReason})`);
3170
+ }
3171
+ else {
3172
+ logger.error(`[${message.channel}] Error:`, error);
3173
+ }
3174
+ const errorType = prefixErrorType(ERROR_PREFIX.INFRA, errType);
3175
+ // 用户主动打断:流被掐断抛出的异常不是真正的失败。打断发生时 source
3176
+ // (message-queue / slash-handler)已发过 task:interrupted(它填充了
3177
+ // interruptedSessions,isUserInterrupt 才会为真),stats 侧已据此收尾任务。
3178
+ // 此处不再发任何事件——发 task:error 会误归类,重发 task:interrupted 会重复记账。
3179
+ if (!isUserInterrupt) {
3180
+ this.eventBus.publish({
3181
+ type: 'task:error',
3182
+ sessionId: session.id,
3183
+ error: errorMsg,
3184
+ errorType,
3185
+ agentName: agentNameForStats,
3186
+ causation: taskCausation,
3187
+ });
3188
+ recordCausationSpan(taskCausation, 'task.run', { status: 'failed', refs: { taskId, sessionId: session.id, messageId }, reason: errorType });
3189
+ }
3190
+ // 记录处理失败
3191
+ logger.message({
3192
+ msgId: messageId,
3193
+ sessionId: session.id,
3194
+ dir: 'inbound',
3195
+ status: isUserInterrupt ? 'interrupted' : 'failed',
3196
+ error: error instanceof Error ? error.message : String(error)
3197
+ });
3198
+ if (error instanceof Error && !isUserInterrupt) {
3199
+ logger.error(`[${message.channel}] Error stack:`, error.stack);
3200
+ }
3201
+ // 发送用户友好的错误消息
3202
+ // 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送错误提示
3203
+ // processEventStream 已通过 renderer 发过错误时也跳过
3204
+ const retryExhaustedCount = getRetryExhaustedCount(error);
3205
+ if (isUserInterrupt) {
3206
+ logger.info(`[ResponseEngine] User interrupt by new_message, skip sending error message`);
3207
+ }
3208
+ else if (error?._errorAlreadySent && !retryExhaustedCount && !isTimeout && !isTotalExecutionTimeout) {
3209
+ logger.info(`[ResponseEngine] Error already sent via renderer, skip sending duplicate message`);
3210
+ }
3211
+ else {
3212
+ // SDK_TIMEOUT:status.timeout 已发结构化状态,此处再补一条用户可见的错误文本(result.error)
3213
+ const idleSec = getLastIdleSec?.() || 0;
3214
+ const userMessage = retryExhaustedCount
3215
+ ? formatRetryableErrorFinalMessage(error, retryExhaustedCount)
3216
+ : isTotalExecutionTimeout
3217
+ ? `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`
3218
+ : isTimeout
3219
+ ? (idleSec > 0 ? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断` : '⚠️ 任务超时,已自动中断')
3220
+ : getErrorMessage(error, undefined);
3221
+ // 获取 session 用于话题回复(如果 resolveSession 已执行)
3222
+ let sendOpts;
3223
+ try {
3224
+ await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd(), message.threadId, undefined, undefined, message.peerId, message.chatType, message.baseagent || this.agentRegistry?.resolveByChannel(message.channel)?.baseagent, message.selfAID, message.channelType, message.peerType);
3225
+ sendOpts = this.getReplyContext(message);
3226
+ }
3227
+ catch { }
3228
+ // 注入 taskId / chatmode(与任务主流程保持一致)
3229
+ sendOpts = {
3230
+ ...(sendOpts ?? {}),
3231
+ sessionId: session.id,
3232
+ metadata: withTaskMetadata(sendOpts?.metadata),
3233
+ };
3234
+ const errorPayload = {
3235
+ kind: 'result.error',
3236
+ text: userMessage,
3237
+ reason: retryExhaustedCount ? 'retry_exhausted' : isTotalExecutionTimeout ? 'total_execution_timeout' : isTimeout ? 'timeout' : errType,
3238
+ };
3239
+ await adapter.send({ ...envelope, replyContext: sendOpts }, errorPayload);
3240
+ if (!isUserInterrupt && daemonTrigger) {
3241
+ await adapter.send({ ...envelope, replyContext: sendOpts }, statusPayload).catch(() => { });
3242
+ }
3243
+ // Proactive 可观测:catch 块的基础设施错误也透传为 thought,保证按 task_id 聚合完整
3244
+ if (isProactive && adapter.capabilities?.thought) {
3245
+ await adapter.send({ ...envelope, replyContext: sendOpts }, {
3246
+ kind: 'activity.batch',
3247
+ items: [{
3248
+ kind: 'notice',
3249
+ text: userMessage,
3250
+ severity: 'warn',
3251
+ subtype: 'task-error',
3252
+ }],
3253
+ }).catch(() => { });
3254
+ }
3255
+ }
3256
+ if (handlingTimeout && timeoutControl) {
3257
+ timeoutControl.terminalDelivered = true;
3258
+ }
3259
+ }
3260
+ // [迁移探针] 任务收尾:记录工具提醒最终状态并落盘(防线 1)
3261
+ if (snapshot.isEnabled()) {
3262
+ snapshot.set(session.id, taskId, {
3263
+ toolReminder: proactive ? { queueReminders: proactive.lastQueueReminderLen, tenWarning: proactive.toolCount >= 10 } : undefined,
3264
+ });
3265
+ snapshot.end(session.id, taskId);
3266
+ }
3267
+ }
3268
+ async runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, adapter, envelope, suppressOutput = false) {
3269
+ if (!session.agentSessionId || !canCompactAgent(agent)) {
3270
+ logger.debug(`[ResponseEngine] Auto compact skipped: session=${session.id} agentSessionId=${session.agentSessionId || 'none'} canCompact=${canCompactAgent(agent)} agent=${agent.name}`);
3271
+ return;
3272
+ }
3273
+ const ctx = await this.readLastModelCallContextUsage(session.id, session.agentSessionId);
3274
+ if (!ctx || ctx.totalTokens < ctx.autoCompactTokens) {
3275
+ logger.debug(`[ResponseEngine] Auto compact skipped: session=${session.id} ctx=${ctx ? `${ctx.totalTokens}/${ctx.autoCompactTokens}` : 'none'}`);
3276
+ return;
3277
+ }
3278
+ logger.info(`[ResponseEngine] Auto compact at task.start: session=${session.id} totalTokens=${ctx.totalTokens} autoCompactTokens=${ctx.autoCompactTokens}`);
3279
+ if (!suppressOutput) {
3280
+ await adapter.send(envelope, { kind: 'system.notice', text: '上下文接近上限,正在压缩会话...', subtype: 'auto-compact-start' }).catch(() => { });
3281
+ }
3282
+ try {
3283
+ const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
3284
+ if (compacted) {
3285
+ if (!suppressOutput) {
3286
+ await adapter.send(envelope, { kind: 'system.notice', text: '✅ 上下文压缩完成,继续处理...', subtype: 'auto-compact-complete' }).catch(() => { });
3287
+ }
3288
+ }
3289
+ else {
3290
+ logger.warn(`[ResponseEngine] Auto compact at task.start returned false (session=${session.id})`);
3291
+ }
3292
+ }
3293
+ catch (err) {
3294
+ logger.warn(`[ResponseEngine] Auto compact at task.start failed (non-fatal):`, err);
3295
+ }
3296
+ }
3297
+ async readLastModelCallContextUsage(sessionId, agentSessionId) {
3298
+ try {
3299
+ const { getDb, openReadonlyDb, getDbPath } = await import('../../stats/db.js');
3300
+ const root = resolveRoot();
3301
+ const writerDb = getDb(root);
3302
+ const db = writerDb || openReadonlyDb(getDbPath(root));
3303
+ if (!db)
3304
+ return undefined;
3305
+ const shouldClose = !writerDb;
3306
+ try {
3307
+ const row = db.prepare(`SELECT model, input_tokens, cache_creation_tokens, cache_read_tokens,
3308
+ context_tokens, max_tokens, auto_compact_tokens
3309
+ FROM model_calls
3310
+ WHERE session_id = ?
3311
+ AND agent_session_id = ?
3312
+ ORDER BY ts DESC, call_index DESC
3313
+ LIMIT 1`).get(sessionId, agentSessionId);
3314
+ if (!row)
3315
+ return undefined;
3316
+ const model = row.model || '';
3317
+ const recordedTotalTokens = row.context_tokens ?? undefined;
3318
+ let totalTokens;
3319
+ if (recordedTotalTokens && recordedTotalTokens > 0) {
3320
+ totalTokens = recordedTotalTokens;
3321
+ }
3322
+ else if (isClaudeContextUsageModel(model)) {
3323
+ totalTokens = (row.input_tokens ?? 0) + (row.cache_creation_tokens ?? 0) + (row.cache_read_tokens ?? 0);
3324
+ }
3325
+ else {
3326
+ totalTokens = row.input_tokens ?? 0;
3327
+ }
3328
+ if (totalTokens <= 0)
3329
+ return undefined;
3330
+ const recordedAutoCompactTokens = row.auto_compact_tokens ?? undefined;
3331
+ const inferredAutoCompactTokens = autoCompactTokensFromMaxTokens(row.max_tokens ?? undefined);
3332
+ const autoCompactTokens = recordedAutoCompactTokens && recordedAutoCompactTokens > 0
3333
+ ? recordedAutoCompactTokens
3334
+ : inferredAutoCompactTokens ?? autoCompactWindowForModel(model);
3335
+ return { totalTokens, autoCompactTokens };
3336
+ }
3337
+ finally {
3338
+ if (shouldClose)
3339
+ db.close();
3340
+ }
3341
+ }
3342
+ catch (err) {
3343
+ logger.debug(`[ResponseEngine] Failed to read last model call context usage: ${err}`);
3344
+ return undefined;
3345
+ }
3346
+ }
3347
+ /**
3348
+ * 解析会话和项目路径
3349
+ */
3350
+ async resolveSession(message) {
3351
+ // 话题会话创建时写入创建者和 replyContext(threadId 路由);主会话不写(避免群聊覆盖)
3352
+ const metadata = message.threadId
3353
+ ? {
3354
+ ...(message.replyContext ? { replyContext: message.replyContext } : {}),
3355
+ ...(message.peerId ? { peerId: message.peerId } : {}),
3356
+ ...(message.peerName ? { peerName: message.peerName } : {}),
3357
+ }
3358
+ : undefined;
3359
+ const owningAgent = this.agentRegistry?.resolveByChannel(message.channel);
3360
+ const projectPath = owningAgent?.projectPath || process.cwd();
3361
+ const resolvedBaseagent = message.baseagent || owningAgent?.baseagent || (!this.agentRegistry ? this.inferPrimaryBaseagent() : undefined);
3362
+ const resolvedIdentity = this.inferDirectSessionIdentity(message, owningAgent);
3363
+ if (!resolvedBaseagent) {
3364
+ throw new Error(`[ResponseEngine] resolveSession: baseagent could not be determined (message.baseagent=${message.baseagent}, owningAgent=${owningAgent?.name || 'none'})`);
3365
+ }
3366
+ // 话题创建权限守卫已统一移至 MessageBridge.canCreateThreadSession(enqueue 前拦截),
3367
+ // 此处不再重复检查——bridge 层拒绝后消息根本不会到达 processMessage。
3368
+ // current strategy: resume bound session, make it active so output is not suppressed
3369
+ if (message.triggerMeta?.boundSessionId) {
3370
+ const bound = await this.sessionManager.getSessionById(message.triggerMeta.boundSessionId);
3371
+ if (bound) {
3372
+ this.ensureSessionBaseagent(bound, resolvedBaseagent);
3373
+ if (bound.threadId) {
3374
+ const absoluteProjectPath = path.isAbsolute(bound.projectPath)
3375
+ ? bound.projectPath : path.resolve(process.cwd(), bound.projectPath);
3376
+ return { session: bound, absoluteProjectPath };
3377
+ }
3378
+ const switched = await this.sessionManager.switchToSession(bound.channel, bound.channelId, bound.id);
3379
+ if (switched) {
3380
+ this.ensureSessionBaseagent(switched, resolvedBaseagent);
3381
+ const absoluteProjectPath = path.isAbsolute(switched.projectPath)
3382
+ ? switched.projectPath : path.resolve(process.cwd(), switched.projectPath);
3383
+ return { session: switched, absoluteProjectPath };
3384
+ }
3385
+ logger.warn(`[ResponseEngine] switchToSession failed for bound session ${bound.id}, falling back to latest`);
3386
+ }
3387
+ else {
3388
+ logger.warn(`[ResponseEngine] Bound session ${message.triggerMeta.boundSessionId} not found, falling back to latest`);
3389
+ }
3390
+ }
3391
+ const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, message.topicName, message.peerId, message.chatType, resolvedBaseagent, message.selfAID, message.channelType, message.peerType, resolvedIdentity);
3392
+ this.ensureSessionBaseagent(session, resolvedBaseagent);
3393
+ // 群名解析:群会话首次取群显示名(group.get),缓存到 metadata,供信封渲染。
3394
+ // 渠道私有方法 getGroupName 自带进程缓存 + 容错;取不到不阻塞(groupName 保持空,模板回退 groupId)。
3395
+ if (message.chatType === 'group' && session.metadata?.groupId && !session.metadata.groupName) {
3396
+ const adapter = this.resolveChannelInfo(message.channel)?.adapter;
3397
+ const groupName = await adapter?.getGroupName?.(session.metadata.groupId).catch(() => undefined);
3398
+ if (groupName) {
3399
+ session.metadata.groupName = groupName;
3400
+ await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
3401
+ }
3402
+ }
3403
+ // 群聊分发模式同步:aun.ts 从服务器信封解析的 dispatchMode 注入到 message,
3404
+ // 此处写入 session.metadata,确保 ECK 上下文的 venue fragment 正确渲染 dispatch 变量。
3405
+ // 仅当 message.dispatchMode 有值且与 session 记录不一致时更新。
3406
+ if (message.chatType === 'group' && message.dispatchMode && session.metadata?.dispatchMode !== message.dispatchMode) {
3407
+ logger.info(`[ResponseEngine] dispatchMode sync: sessionId=${session.id} ${session.metadata?.dispatchMode ?? 'none'} -> ${message.dispatchMode}`);
3408
+ session.metadata = { ...(session.metadata || {}), dispatchMode: message.dispatchMode };
3409
+ await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
3410
+ }
3411
+ // chatMode 策略由 agent/relation behavior 配置在处理阶段解析;此处不再写 session 级参数。
3412
+ // replyContext 不再写入 session.metadata(跟着 message 走,避免群聊多人覆盖)
3413
+ const absoluteProjectPath = path.isAbsolute(session.projectPath)
3414
+ ? session.projectPath
3415
+ : path.resolve(process.cwd(), session.projectPath);
3416
+ return { session, absoluteProjectPath };
3417
+ }
3418
+ buildTurnCommitEvidence(session, result) {
3419
+ const active = this.turnCoordinator.current(session).active;
3420
+ return {
3421
+ inputAccepted: result.inputAccepted === true,
3422
+ assistantUuid: result.assistantUuid,
3423
+ textCount: result.textCount ?? 0,
3424
+ toolUseCount: result.toolUseCount ?? 0,
3425
+ openToolUseIds: result.openToolUseIds ?? [],
3426
+ openInteractionIds: active?.interactions
3427
+ .filter(interaction => !interaction.settlement)
3428
+ .map(interaction => interaction.interactionId) ?? [],
3429
+ resultText: result.lastReplyText || result.fullText,
3430
+ inputTokens: result.tokenUsage?.input_tokens ?? 0,
3431
+ outputTokens: result.tokenUsage?.output_tokens ?? 0,
3432
+ };
3433
+ }
3434
+ /**
3435
+ * 处理标准事件流(AgentEvent)
3436
+ *
3437
+ * 此方法只消费标准 AgentEvent 类型,不引用任何 SDK 特有事件。
3438
+ * SDK 事件 → AgentEvent 的转换在 AgentRunner.transformStream() 中完成。
3439
+ */
3440
+ async processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress, proactive,
3441
+ /** [迁移点4/5] 响应模式插件 + 状态,用于调 onToolUse/onComplete 钩子 */
3442
+ modeHooks, causation, turnLease) {
3443
+ // Per-session agent name for stats bucketing
3444
+ const statsChannelKey = session.channel === 'daemon' ? session.channel : (session.metadata?.channelKey || session.channel);
3445
+ const agentNameForStats = this.agentRegistry?.resolveByChannel(statsChannelKey)?.name ?? '<unknown>';
3446
+ let hasReceivedText = false;
3447
+ let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
3448
+ let madeProgress = false;
3449
+ let completeResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
3450
+ let inputAccepted = false;
3451
+ let assistantUuid;
3452
+ let protocolIncompleteReason;
3453
+ let textCount = 0;
3454
+ let toolUseCount = 0;
3455
+ const openToolUseIds = new Set();
3456
+ const anonymousToolUseIds = new Map();
3457
+ const finalizeResult = () => ({
3458
+ ...completeResult,
3459
+ hasReceivedText,
3460
+ inputAccepted,
3461
+ assistantUuid,
3462
+ protocolIncompleteReason,
3463
+ textCount,
3464
+ toolUseCount,
3465
+ openToolUseIds: [...openToolUseIds],
3466
+ });
3467
+ // 追踪最后一轮 assistant 回复文本(tool_use 之后的纯文本)
3468
+ let lastReplyText = '';
3469
+ // callId → description 映射,用于 tool_result 回显描述
3470
+ const toolDescByCallId = new Map();
3471
+ const ctlQueueReadCallIds = new Set();
3472
+ try {
3473
+ for await (let event of stream) {
3474
+ if (turnLease && !this.turnCoordinator.isCurrent(turnLease)) {
3475
+ logger.debug(`[ResponseEngine] Ignoring stale stream event: session=${session.id} task=${turnLease.taskId} type=${event.type}`);
3476
+ continue;
3477
+ }
3478
+ if (event.type === 'complete') {
3479
+ event = normalizeCompleteAgentEvent(event);
3480
+ }
3481
+ // 每收到事件重置空闲超时
3482
+ const toolName = event.type === 'tool_use' ? event.name : undefined;
3483
+ resetTimer(event.type, toolName);
3484
+ if (event.type === 'input_accepted') {
3485
+ inputAccepted = !turnLease || event.inputId === turnLease.inputId;
3486
+ logger.debug(`[ResponseEngine] Input accepted: session=${session.id} input=${event.inputId}`);
3487
+ continue;
3488
+ }
3489
+ if (event.type === 'text') {
3490
+ if (event.text.trim())
3491
+ textCount++;
3492
+ assistantUuid = event.assistantUuid ?? assistantUuid;
3493
+ }
3494
+ else if (event.type === 'tool_use') {
3495
+ toolUseCount++;
3496
+ assistantUuid = event.assistantUuid ?? assistantUuid;
3497
+ if (event.callId) {
3498
+ openToolUseIds.add(event.callId);
3499
+ }
3500
+ else {
3501
+ const syntheticId = `__anonymous_tool_${toolUseCount}`;
3502
+ openToolUseIds.add(syntheticId);
3503
+ const pending = anonymousToolUseIds.get(event.name) ?? [];
3504
+ pending.push(syntheticId);
3505
+ anonymousToolUseIds.set(event.name, pending);
3506
+ }
3507
+ }
3508
+ else if (event.type === 'tool_result') {
3509
+ if (event.callId) {
3510
+ openToolUseIds.delete(event.callId);
3511
+ }
3512
+ else {
3513
+ const names = event.name ? [event.name] : [...anonymousToolUseIds.keys()];
3514
+ for (const name of names) {
3515
+ const pending = anonymousToolUseIds.get(name);
3516
+ const syntheticId = pending?.shift();
3517
+ if (syntheticId) {
3518
+ openToolUseIds.delete(syntheticId);
3519
+ if (pending?.length === 0)
3520
+ anonymousToolUseIds.delete(name);
3521
+ break;
3522
+ }
3523
+ }
3524
+ }
3525
+ }
3526
+ else if (event.type === 'complete') {
3527
+ assistantUuid = event.assistantUuid ?? assistantUuid;
3528
+ protocolIncompleteReason = event.protocolIncompleteReason ?? protocolIncompleteReason;
3529
+ }
3530
+ // 记录事件类型:高价值事件(含子任务生命周期)INFO,
3531
+ // 框架事件(session_id/state_changed/status)DEBUG
3532
+ let eventDetail = '';
3533
+ if (event.type === 'text' && event.text) {
3534
+ const preview = event.text.replace(/\s+/g, ' ').slice(0, 80);
3535
+ eventDetail = ` text="${preview}${event.text.length > 80 ? '…' : ''}"`;
3536
+ }
3537
+ else if (event.type === 'tool_use') {
3538
+ const input = event.input;
3539
+ const desc = input?.description
3540
+ || input?.file_path
3541
+ || input?.pattern
3542
+ || (typeof input?.command === 'string' ? input.command.slice(0, 80) : '')
3543
+ || (typeof input?.prompt === 'string' ? input.prompt.slice(0, 80) : '')
3544
+ || (typeof input?.query === 'string' ? input.query.slice(0, 80) : '')
3545
+ || '';
3546
+ eventDetail = ` tool=${event.name}${desc ? ` desc="${desc}"` : ''}`;
3547
+ }
3548
+ else if (event.type === 'tool_result') {
3549
+ eventDetail = ` tool=${event.name} ok=${!event.isError}`;
3550
+ }
3551
+ const frameworkEvents = new Set(['session_id', 'state_changed', 'status']);
3552
+ if (frameworkEvents.has(event.type)) {
3553
+ logger.debug(`[ResponseEngine] Event: type=${event.type}${eventDetail}`);
3554
+ }
3555
+ else {
3556
+ logger.info(`[ResponseEngine] Event: type=${event.type}${eventDetail}`);
3557
+ }
3558
+ // IMRenderer 旁路:proactive 模式逐事件投影为 thought(fire-and-forget)
3559
+ renderer.emit(event);
3560
+ // session_id 已在 AgentRunner.transformStream 中处理,此处仅记录
3561
+ if (event.type === 'session_id') {
3562
+ logger.debug(`[ResponseEngine] Session ID updated: ${event.sessionId} for session: ${session.id}`);
3563
+ session.agentSessionId = event.sessionId;
3564
+ continue;
3565
+ }
3566
+ // session 状态变更(idle/running/requires_action)
3567
+ if (event.type === 'state_changed') {
3568
+ logger.debug(`[ResponseEngine] Session state: ${event.state} for session: ${session.id}`);
3569
+ this.eventBus.publish({ type: 'runner:state-changed', sessionId: session.id, state: event.state });
3570
+ continue;
3571
+ }
3572
+ // agent 状态通知(仅事件,不直出给用户)
3573
+ if (event.type === 'status') {
3574
+ logger.debug(`[ResponseEngine] Agent status: ${event.subtype}: ${event.message}`);
3575
+ this.eventBus.publish({
3576
+ type: 'runner:status',
3577
+ sessionId: session.id,
3578
+ subtype: event.subtype,
3579
+ message: event.message,
3580
+ timestamp: Date.now(),
3581
+ causation,
3582
+ });
3583
+ continue;
3584
+ }
3585
+ if (event.type === 'task_started') {
3586
+ this.eventBus.publish({
3587
+ type: 'runner:task-started',
3588
+ sessionId: session.id,
3589
+ taskId: event.taskId,
3590
+ taskKind: event.taskKind,
3591
+ toolUseId: event.toolUseId,
3592
+ description: event.description,
3593
+ subagentType: event.subagentType,
3594
+ taskType: event.taskType,
3595
+ workflowName: event.workflowName,
3596
+ prompt: event.prompt,
3597
+ skipTranscript: event.skipTranscript,
3598
+ timestamp: Date.now(),
3599
+ causation,
3600
+ });
3601
+ if (!event.skipTranscript) {
3602
+ renderer.addLifecycle('started', {
3603
+ scope: 'runner_task',
3604
+ taskId: event.taskId,
3605
+ taskKind: event.taskKind,
3606
+ toolUseId: event.toolUseId,
3607
+ description: event.description,
3608
+ subagentType: event.subagentType,
3609
+ taskType: event.taskType,
3610
+ }, `${event.taskKind === 'agent' ? 'Agent' : 'Background'} task started: ${event.description}`);
3611
+ }
3612
+ continue;
3613
+ }
3614
+ if (event.type === 'background_tasks_changed') {
3615
+ this.eventBus.publish({
3616
+ type: 'runner:background-tasks-changed',
3617
+ sessionId: session.id,
3618
+ tasks: event.tasks,
3619
+ timestamp: Date.now(),
3620
+ causation,
3621
+ });
3622
+ continue;
3623
+ }
3624
+ if (event.type === 'task_notification') {
3625
+ this.eventBus.publish({
3626
+ type: 'runner:task-notification',
3627
+ sessionId: session.id,
3628
+ taskId: event.taskId,
3629
+ taskKind: event.taskKind,
3630
+ toolUseId: event.toolUseId,
3631
+ description: event.description,
3632
+ subagentType: event.subagentType,
3633
+ status: event.status,
3634
+ outputFile: event.outputFile,
3635
+ summary: event.summary,
3636
+ usage: event.usage,
3637
+ skipTranscript: event.skipTranscript,
3638
+ timestamp: Date.now(),
3639
+ causation,
3640
+ });
3641
+ if (!event.skipTranscript) {
3642
+ const label = event.description || event.taskId;
3643
+ renderer.addLifecycle('completed', {
3644
+ scope: 'runner_task',
3645
+ taskId: event.taskId,
3646
+ taskKind: event.taskKind,
3647
+ toolUseId: event.toolUseId,
3648
+ description: event.description,
3649
+ subagentType: event.subagentType,
3650
+ status: event.status,
3651
+ outputFile: event.outputFile,
3652
+ summary: event.summary,
3653
+ usage: event.usage,
3654
+ }, `${event.taskKind === 'agent' ? 'Agent' : 'Background'} task ${event.status}: ${label}`);
3655
+ }
3656
+ continue;
3657
+ }
3658
+ if (event.type === 'task_progress') {
3659
+ this.eventBus.publish({
3660
+ type: 'runner:task-progress',
3661
+ sessionId: session.id,
3662
+ taskId: event.taskId,
3663
+ taskKind: event.taskKind,
3664
+ toolUseId: event.toolUseId,
3665
+ description: event.description,
3666
+ subagentType: event.subagentType,
3667
+ summary: event.summary,
3668
+ lastToolName: event.lastToolName,
3669
+ totalTokens: event.totalTokens,
3670
+ toolUses: event.toolUses,
3671
+ durationMs: event.durationMs,
3672
+ timestamp: Date.now(),
3673
+ causation,
3674
+ });
3675
+ }
3676
+ const isCurrentlyBackground = this.isBackgroundSession(session, session.channel, session.channelId);
3677
+ // === 前台任务:正常处理所有事件 ===
3678
+ if (!isCurrentlyBackground) {
3679
+ // 流式文本
3680
+ if (event.type === 'text') {
3681
+ hasReceivedText = true;
3682
+ if (event.text.trim())
3683
+ madeProgress = true;
3684
+ lastReplyText += event.text;
3685
+ this.eventBus.publish({ type: 'message:text', sessionId: session.id, text: event.text, isFinal: false });
3686
+ if (!shouldSuppress()) {
3687
+ renderer.addText(event.text, event.outputTokens, event.turn);
3688
+ }
3689
+ }
3690
+ // compact 完成
3691
+ if (event.type === 'compact') {
3692
+ madeProgress = true;
3693
+ this.eventBus.publish({ type: 'runner:compact-complete', sessionId: session.id, preTokens: event.preTokens });
3694
+ if (!shouldSuppress()) {
3695
+ renderer.addNotice(`\ud83d\udca1 会话压缩完成,继续执行...)`, 'info', 'compact');
3696
+ }
3697
+ }
3698
+ // 子任务进度
3699
+ if (event.type === 'task_progress') {
3700
+ const tools = event.toolUses ?? 0;
3701
+ const duration = event.durationMs ? `${Math.round(event.durationMs / 1000)}s` : '';
3702
+ const stats = [tools > 0 ? `${tools}\u6b21\u5de5\u5177\u8c03\u7528` : '', duration].filter(Boolean).join(', ');
3703
+ if (event.summary || tools > 0)
3704
+ madeProgress = true;
3705
+ if (event.summary && !shouldSuppress()) {
3706
+ renderer.addProgress(`\u5b50\u4efb\u52a1: ${event.summary}${stats ? ` (${stats})` : ''}`, { state: 'processing', toolUses: event.toolUses, durationMs: event.durationMs });
3707
+ }
3708
+ else if (stats && !shouldSuppress()) {
3709
+ renderer.addProgress(`\u5b50\u4efb\u52a1\u8fdb\u884c\u4e2d: ${stats}`, { state: 'processing', toolUses: event.toolUses, durationMs: event.durationMs });
3710
+ }
3711
+ }
3712
+ // 工具调用
3713
+ if (event.type === 'tool_use') {
3714
+ madeProgress = true;
3715
+ // 工具调用意味着当前 turn 结束,flush 已累积的文本作为独立消息
3716
+ if (renderer.hasTextPending()) {
3717
+ await renderer.flushText();
3718
+ }
3719
+ // 重置最后回复追踪
3720
+ lastReplyText = '';
3721
+ this.eventBus.publish({
3722
+ type: 'tool:use',
3723
+ sessionId: session.id,
3724
+ toolName: event.name,
3725
+ input: event.input,
3726
+ timestamp: Date.now(),
3727
+ causation,
3728
+ });
3729
+ if (!shouldSuppress()) {
3730
+ const desc = summarizeToolInput(event.name, event.input || {});
3731
+ if (event.callId) {
3732
+ toolDescByCallId.set(event.callId, desc);
3733
+ }
3734
+ renderer.addToolCall(event.name, event.input, event.callId, desc, event.turn, event.outputTokens);
3735
+ }
3736
+ if (event.callId && isCtlQueueReadCommand(event.name, event.input || {})) {
3737
+ ctlQueueReadCallIds.add(event.callId);
3738
+ }
3739
+ // [迁移点4] 工具汇报提醒:由 ProactiveMode.onToolUse 实现
3740
+ if (modeHooks?.mode?.onToolUse) {
3741
+ modeHooks.mode.onToolUse({
3742
+ session,
3743
+ state: modeHooks.state,
3744
+ toolName: event.name,
3745
+ toolInput: event.input || {},
3746
+ injectToModel: (text) => { agent.injectUserMessage?.(session.id, text); },
3747
+ getQueueLength: () => this.messageQueue?.getQueueLength(session.id) ?? 0,
3748
+ isSendCommand: (toolName, toolInput) => isEvolcoreSendCommandForSession(toolName, toolInput, session.channelId),
3749
+ logger,
3750
+ });
3751
+ }
3752
+ }
3753
+ // 工具结果
3754
+ if (event.type === 'tool_result') {
3755
+ if (!event.isError)
3756
+ madeProgress = true;
3757
+ if (event.callId && ctlQueueReadCallIds.delete(event.callId) && !event.isError) {
3758
+ try {
3759
+ const clearResult = await this.commandHandler?.handleCtl?.('/queue --clear', session.id);
3760
+ if (clearResult && !clearResult.ok) {
3761
+ logger.warn(`[ResponseEngine] auto clear queue after ec ctl queue failed: ${clearResult.error || 'unknown error'}`);
3762
+ }
3763
+ }
3764
+ catch (error) {
3765
+ logger.warn('[ResponseEngine] auto clear queue after ec ctl queue failed:', error);
3766
+ }
3767
+ }
3768
+ this.eventBus.publish({
3769
+ type: 'tool:result',
3770
+ sessionId: session.id,
3771
+ toolName: event.name,
3772
+ isError: event.isError,
3773
+ agentName: agentNameForStats,
3774
+ timestamp: Date.now(),
3775
+ causation,
3776
+ });
3777
+ // 从 tool_use 阶段缓存的描述中回溯
3778
+ const cachedDesc = event.callId ? toolDescByCallId.get(event.callId) : undefined;
3779
+ if (event.isError && !shouldSuppress()) {
3780
+ hasErrorResult = true;
3781
+ let errorMsg = event.error || (typeof event.result === 'string' ? event.result : JSON.stringify(event.result)) || '\u6267\u884c\u5931\u8d25';
3782
+ // 移除 XML 风格的错误标签
3783
+ errorMsg = errorMsg.replace(/<tool_use_error>(.*?)<\/tool_use_error>/gs, '$1');
3784
+ renderer.addToolResult(event.name || '\u5de5\u5177', false, undefined, errorMsg, event.callId, event.durationMs, cachedDesc);
3785
+ }
3786
+ else if (!event.isError && !shouldSuppress()) {
3787
+ renderer.addToolResult(event.name || '\u5de5\u5177', true, event.result, undefined, event.callId, event.durationMs, cachedDesc);
3788
+ }
3789
+ }
3790
+ // 运行时错误(Codex: turn.failed / item error)
3791
+ if (event.type === 'error') {
3792
+ logger.warn(`[ResponseEngine] error event: ${event.errorType}: ${event.error}`);
3793
+ // 记录错误文本到 lastReplyText,供后续 isPromptTooLong 检测
3794
+ lastReplyText += event.error || '';
3795
+ completeResult = {
3796
+ ...completeResult,
3797
+ isError: true,
3798
+ subtype: event.errorType,
3799
+ terminalReason: event.errorType,
3800
+ errors: [event.error],
3801
+ lastReplyText,
3802
+ fullText: '',
3803
+ };
3804
+ // 上下文过长的错误不在此处输出 notice,留给外层 isPromptTooLong 触发 auto-compact
3805
+ const isContextError = isContextTooLongText(event.error || '');
3806
+ const isRetryableRuntimeError = isRetryableError(new Error(event.error || ''));
3807
+ if (!isContextError && !isRetryableRuntimeError && !hasErrorResult && !shouldSuppress()) {
3808
+ hasErrorResult = true;
3809
+ renderer.addNotice(getRuntimeErrorMessage(event.error || '任务执行失败', false), 'warn', 'runtime-error', true);
3810
+ }
3811
+ }
3812
+ // 完成事件
3813
+ // SDK 可能产生多个 complete 事件(如 subagent 或 auto-compact 二次查询),
3814
+ // 仅记录状态,最终 flush(true) 在流结束后统一执行
3815
+ if (event.type === 'complete') {
3816
+ const isAbort = event.terminalReason === 'aborted_streaming' || event.terminalReason === 'aborted_tools';
3817
+ 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}`);
3818
+ // 自动回填会话名称
3819
+ if (event.sessionTitle && shouldAutoFillSessionTitle(session.name, session.threadId)) {
3820
+ await this.sessionManager.renameSession(session.id, event.sessionTitle);
3821
+ logger.info(`[ResponseEngine] Auto-filled session name: ${event.sessionTitle}`);
3822
+ }
3823
+ // 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
3824
+ completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, model: event.model, tokenUsage: event.tokenUsage, contextUsage: event.contextUsage, lastModelCall: event.lastModelCall, modelCalls: event.modelCalls };
3825
+ if (!event.isError)
3826
+ madeProgress = true;
3827
+ // thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
3828
+ // 由那里按 LLM 输出的每个 text item 单独写一条,此处不再写。
3829
+ // 失败且无前置错误输出:显示 errors 摘要
3830
+ // 但用户主动中断(新消息打断 或 /stop 命令)时不显示错误提示
3831
+ // 上下文过长的错误留给外层 isPromptTooLong 触发 auto-compact,不在此处输出
3832
+ const interruptReason = this.interruptedSessions.get(session.id);
3833
+ const isUserInterrupt = interruptReason === 'new_message' || interruptReason === 'stop' || interruptReason === 'recalled';
3834
+ const isContextTooLong = event.terminalReason === 'prompt_too_long'
3835
+ || isContextTooLongText(event.errors?.join(' ') || '')
3836
+ || isContextTooLongText(lastReplyText);
3837
+ const completeErrorText = getStreamErrorText(completeResult);
3838
+ const isRetryableCompleteError = event.isError && !isContextTooLong && completeErrorText
3839
+ ? isRetryableError(new Error(completeErrorText))
3840
+ : false;
3841
+ if (event.isError && completeErrorText && isPendingTextSameAsStreamError(renderer.getRemainingText(), completeErrorText)) {
3842
+ renderer.discardPendingText();
3843
+ }
3844
+ if (event.isError && !hasErrorResult && !shouldSuppress() && !isUserInterrupt && !isContextTooLong && !isRetryableCompleteError) {
3845
+ // 使用 terminalReason 提供更友好的错误提示(不带 emoji,由 formatter 统一加)
3846
+ const userFriendlyMessage = event.terminalReason === 'prompt_too_long'
3847
+ ? getContextTooLongHint(agent)
3848
+ : event.terminalReason === 'context_compact_failed'
3849
+ ? getContextCompactFailedHint(agent)
3850
+ : getStreamErrorMessage(completeResult, false);
3851
+ renderer.addNotice(userFriendlyMessage, 'warn', 'task-error', true);
3852
+ }
3853
+ // 中间 complete:flush 掉已有 activities(不带 isFinal),让中间结果及时显示
3854
+ // 最终文本留给流结束后的统一 flush(true)
3855
+ if (renderer.hasContent()) {
3856
+ await renderer.flushActivitiesOnly();
3857
+ }
3858
+ // 检测 proactive 标志位,设置 lastProactiveFlag 供模式切换提示使用
3859
+ // [迁移点5] 标志位检查:由 ProactiveMode.onComplete 实现
3860
+ if (event.queryFinal !== false && modeHooks?.mode?.onComplete && lastReplyText) {
3861
+ await modeHooks.mode.onComplete({
3862
+ session,
3863
+ state: modeHooks.state,
3864
+ lastReplyText,
3865
+ updateSessionMeta: async (patch) => {
3866
+ session.metadata = { ...(session.metadata || {}), ...patch };
3867
+ await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
3868
+ },
3869
+ logger,
3870
+ });
3871
+ }
3872
+ }
3873
+ continue;
3874
+ }
3875
+ // === 后台任务:追踪最后回复文本,但只处理 complete 事件 ===
3876
+ if (event.type === 'text') {
3877
+ if (event.text.trim())
3878
+ madeProgress = true;
3879
+ lastReplyText += event.text;
3880
+ }
3881
+ else if (event.type === 'tool_use') {
3882
+ madeProgress = true;
3883
+ lastReplyText = '';
3884
+ }
3885
+ if (event.type !== 'complete') {
3886
+ continue;
3887
+ }
3888
+ // 自动回填会话名称
3889
+ if (event.sessionTitle && shouldAutoFillSessionTitle(session.name, session.threadId)) {
3890
+ await this.sessionManager.renameSession(session.id, event.sessionTitle);
3891
+ logger.info(`[ResponseEngine] Auto-filled session name: ${event.sessionTitle}`);
3892
+ }
3893
+ // 记录完成状态
3894
+ completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, model: event.model, tokenUsage: event.tokenUsage, contextUsage: event.contextUsage, lastModelCall: event.lastModelCall };
3895
+ if (event.subtype === 'success')
3896
+ madeProgress = true;
3897
+ }
3898
+ }
3899
+ catch (error) {
3900
+ // User interrupt (AbortError) is expected, log at info level
3901
+ const catchInterruptReason = this.interruptedSessions.get(session.id);
3902
+ const catchIsUserInterrupt = catchInterruptReason === 'new_message' || catchInterruptReason === 'stop' || catchInterruptReason === 'recalled';
3903
+ if (catchInterruptReason === 'timeout') {
3904
+ logger.info('[ResponseEngine] Stream closed by timeout barrier');
3905
+ throw this.timeoutErrors.get(session.id) ?? new Error('SDK_TIMEOUT');
3906
+ }
3907
+ if (error instanceof Error && error.name === 'AbortError') {
3908
+ logger.info('[ResponseEngine] Stream interrupted (AbortError)');
3909
+ // User-initiated interrupt: skip flush — new task takes over the channel,
3910
+ // flushing here would send a spurious "最终回复" before the new task's output
3911
+ if (catchIsUserInterrupt) {
3912
+ completeResult.isError = false;
3913
+ return finalizeResult();
3914
+ }
3915
+ }
3916
+ else if (catchIsUserInterrupt) {
3917
+ // SDK telemetry noise after user-initiated interrupt — not a real error
3918
+ logger.debug('[ResponseEngine] Stream ended after user interrupt:', error?.message?.split('\n')[0]);
3919
+ completeResult.isError = false;
3920
+ return finalizeResult();
3921
+ }
3922
+ else if (isRetryableError(error)) {
3923
+ // Retryable errors (network aborts, transient API failures) are noise at ERROR level
3924
+ logger.warn('[ResponseEngine] Stream processing error (retryable):', error?.message?.split('\n')[0]);
3925
+ }
3926
+ else {
3927
+ logger.error('[ResponseEngine] Stream processing error:', error);
3928
+ }
3929
+ if (error instanceof Error && error.message.includes('process exited')) {
3930
+ renderer.addNotice('Claude Code 进程异常退出,请重试', 'warn', 'process-exit', true);
3931
+ }
3932
+ if (isRetryableError(error) && madeProgress) {
3933
+ markRetryMadeProgress(error);
3934
+ }
3935
+ // Flush any pending error activities before re-throwing,
3936
+ // and mark the error so outer catch won't send a duplicate message
3937
+ const hasErrorSuppressingContent = hasErrorResult || renderer.hasNonLifecycleContent();
3938
+ if (hasErrorSuppressingContent) {
3939
+ try {
3940
+ await renderer.flush(true);
3941
+ }
3942
+ catch { }
3943
+ if (error instanceof Error) {
3944
+ error._errorAlreadySent = true;
3945
+ }
3946
+ }
3947
+ else if (renderer.hasContent()) {
3948
+ renderer.flushActivitiesOnly().catch(() => { });
3949
+ }
3950
+ throw error;
3951
+ }
3952
+ return finalizeResult();
3953
+ }
3954
+ /**
3955
+ * 解析文件路径,支持相对路径和绝对路径
3956
+ * 优先在项目根目录查找,兜底尝试 .openclaw/workspace/
3957
+ */
3958
+ resolveFilePath(filePath, projectPath) {
3959
+ if (path.isAbsolute(filePath)) {
3960
+ return filePath;
3961
+ }
3962
+ // 优先在项目根目录查找
3963
+ const rootPath = path.join(projectPath, filePath);
3964
+ if (fs.existsSync(rootPath)) {
3965
+ return rootPath;
3966
+ }
3967
+ // 兜底:尝试 .openclaw/workspace/
3968
+ const workspacePath = path.join(projectPath, '.openclaw', 'workspace', filePath);
3969
+ if (fs.existsSync(workspacePath)) {
3970
+ return workspacePath;
3971
+ }
3972
+ // 都找不到,返回项目根目录路径
3973
+ return rootPath;
3974
+ }
3975
+ /**
3976
+ * 判断文件路径是否为占位符/示例文本
3977
+ * 用于过滤大模型在说明文字中误写的 [SEND_FILE:...] 标记
3978
+ */
3979
+ isPlaceholderPath(filePath) {
3980
+ if (!filePath)
3981
+ return true;
3982
+ const normalized = filePath.trim().toLowerCase();
3983
+ // 精确占位符
3984
+ const exactPlaceholders = ['...', '\u2026', 'path', 'file', 'file_path', 'filepath',
3985
+ '\u8def\u5f84', '\u6587\u4ef6\u8def\u5f84', '\u6587\u4ef6', 'filename', 'xxx'];
3986
+ if (exactPlaceholders.includes(normalized))
3987
+ return true;
3988
+ // 跨通道示例占位符,如 [SEND_FILE:channel:路径] 被未知 channel 回退后会变成 channel:路径。
3989
+ const channelPlaceholders = ['channel', 'channel_name', 'channelname', 'target', 'target_channel', 'targetchannel',
3990
+ '\u6e20\u9053', '\u901a\u9053', '\u9891\u9053', '\u76ee\u6807\u6e20\u9053', '\u6e20\u9053\u540d', '\u901a\u9053\u540d'];
3991
+ const colonIndex = normalized.indexOf(':');
3992
+ if (colonIndex > 0) {
3993
+ const maybeChannel = normalized.slice(0, colonIndex).trim();
3994
+ const maybePath = normalized.slice(colonIndex + 1).trim();
3995
+ if (channelPlaceholders.includes(maybeChannel) && exactPlaceholders.includes(maybePath))
3996
+ return true;
3997
+ }
3998
+ // 示例路径前缀
3999
+ if (/^(\/path\/to\/|\.\/path\/to\/|example\/|\u793a\u4f8b|\/example)/i.test(filePath))
4000
+ return true;
4001
+ // 含模板变量
4002
+ if (/\$\{.+\}|\{\{.+\}\}|<.+>/.test(filePath))
4003
+ return true;
4004
+ // 纯标点/特殊字符(非路径字符)
4005
+ if (/^[.\s\u2026]+$/.test(filePath))
4006
+ return true;
4007
+ // 含正则/代码特殊字符(Agent 在说明中引用了代码或正则表达式)
4008
+ if (/[\[\]{}*+?|^$]/.test(filePath))
4009
+ return true;
4010
+ return false;
4011
+ }
4012
+ }
4013
+ // ── 出站协议辅助:buildEnvelope / sendInteractionPayload ──
4014
+ // Phase 3 of outbound unification: callers (permission flow, CommandHandler
4015
+ // interaction cards, claude-runner AskUserQuestion / ExitPlanMode) should
4016
+ // produce `{ kind: 'interaction', interaction, fallbackText }` and dispatch
4017
+ // via `adapter.send(envelope, payload)` instead of calling
4018
+ // `adapter.sendInteraction(...)` directly. These helpers centralise the
4019
+ // indirection and provide a backwards-compatible fallback path for adapters
4020
+ // that do not yet implement `send`.