evolcore 0.0.25 → 0.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/CHANGELOG.md +632 -565
  2. package/LICENSE +21 -21
  3. package/README.md +455 -455
  4. package/bin/codex-managed-acl.ps1 +203 -160
  5. package/bin/codex-managed-hook.mjs +208 -208
  6. package/bin/ec-safe-output.js +327 -327
  7. package/bin/ec.js +40 -29
  8. package/bin/install-codex-managed-hooks.mjs +582 -555
  9. package/dist/agents/baseagent-availability.js +68 -0
  10. package/dist/agents/baseagent.js +96 -46
  11. package/dist/agents/claude-runner.js +172 -33
  12. package/dist/agents/codex/app-server-client.js +76 -31
  13. package/dist/agents/codex/catalog.js +1 -1
  14. package/dist/agents/codex/diagnostics.js +34 -1
  15. package/dist/agents/codex-runner.js +434 -446
  16. package/dist/agents/ecagent-runner.js +174 -34
  17. package/dist/agents/gemini-runner.js +8 -7
  18. package/dist/agents/runner-types.js +75 -19
  19. package/dist/aun/aid/agentmd.js +151 -36
  20. package/dist/aun/aid/client.js +1 -1
  21. package/dist/aun/aid/control-aid.js +180 -1
  22. package/dist/aun/aid/identity.js +4 -4
  23. package/dist/aun/aid/index.js +1 -1
  24. package/dist/aun/aid/store.js +17 -13
  25. package/dist/aun/group/history.js +303 -0
  26. package/dist/aun/group/identity.js +43 -0
  27. package/dist/aun/group/index.js +5 -0
  28. package/dist/aun/{msg/group.js → group/operations.js} +162 -39
  29. package/dist/aun/{menu-client.js → menu/client.js} +85 -6
  30. package/dist/aun/menu/index.js +2 -0
  31. package/dist/aun/menu/transport.js +49 -0
  32. package/dist/aun/msg/index.js +2 -1
  33. package/dist/aun/msg/managed-operation.js +85 -19
  34. package/dist/aun/msg/mention-schema.js +51 -0
  35. package/dist/aun/msg/p2p.js +19 -9
  36. package/dist/aun/msg/upload.js +43 -80
  37. package/dist/aun/outbox.js +29 -7
  38. package/dist/aun/rpc/client.js +1 -0
  39. package/dist/aun/storage/client.js +39 -50
  40. package/dist/channels/aun.js +842 -318
  41. package/dist/channels/daemon.js +10 -6
  42. package/dist/cli/agent-command.js +212 -89
  43. package/dist/cli/agent-create.js +626 -11
  44. package/dist/cli/agent.js +125 -13
  45. package/dist/cli/aun-commands.js +487 -337
  46. package/dist/cli/bench.js +20 -20
  47. package/dist/cli/config.js +55 -33
  48. package/dist/cli/contact.js +72 -15
  49. package/dist/cli/ctl-command.js +24 -24
  50. package/dist/cli/daemon-commands.js +140 -63
  51. package/dist/cli/diag.js +31 -0
  52. package/dist/cli/fs-command.js +70 -70
  53. package/dist/cli/handoff-command.js +46 -14
  54. package/dist/cli/index.js +91 -79
  55. package/dist/cli/init.js +34 -1
  56. package/dist/cli/link-rules.js +21 -21
  57. package/dist/cli/managed-command-guard.js +5 -1
  58. package/dist/cli/menu-client.js +51 -32
  59. package/dist/cli/model.js +3 -3
  60. package/dist/cli/net-check.js +22 -22
  61. package/dist/cli/queue-command.js +31 -31
  62. package/dist/cli/response.js +32 -32
  63. package/dist/cli/restart-monitor.js +103 -42
  64. package/dist/cli/stats.js +51 -51
  65. package/dist/cli/task-command.js +59 -0
  66. package/dist/cli/task-context.js +97 -49
  67. package/dist/cli/trigger-command.js +74 -64
  68. package/dist/config/builtin-roles.js +11 -0
  69. package/dist/config/config-field-policy.js +11 -0
  70. package/dist/config/config-manager.js +32 -4
  71. package/dist/config/config-operation-service.js +160 -0
  72. package/dist/config/contact-book-store.js +1 -1
  73. package/dist/config/contact-book.js +1 -1
  74. package/dist/config/contact-operation-service.js +97 -8
  75. package/dist/config/peer-role-resolver.js +1 -1
  76. package/dist/config/resolved-config-op.js +30 -7
  77. package/dist/config-store.js +1 -1
  78. package/dist/core/agent-application-service.js +7 -1
  79. package/dist/core/audit/event-key.js +17 -2
  80. package/dist/core/audit/log-integrity.js +34 -8
  81. package/dist/core/auth/agent-delegation.js +48 -14
  82. package/dist/core/auth/auth-gateway.js +2 -0
  83. package/dist/core/auth/authorization-audit.js +53 -49
  84. package/dist/core/auth/operation-authorizer.js +90 -7
  85. package/dist/core/auth/operation-catalog.js +145 -4
  86. package/dist/core/auth/remote-access-store.js +155 -10
  87. package/dist/core/bootstrap-messages.js +66 -7
  88. package/dist/core/bootstrap-service.js +111 -20
  89. package/dist/core/command/agent-control.js +88 -7
  90. package/dist/core/command/agent-storage-menu.js +443 -0
  91. package/dist/core/command/cli-intent-parser.js +19 -0
  92. package/dist/core/command/command-handler.js +159 -65
  93. package/dist/core/command/data-root-resolver.js +273 -0
  94. package/dist/core/command/diag-service.js +118 -0
  95. package/dist/core/command/group-menu.js +7 -2
  96. package/dist/core/command/menu-catalog.js +46 -7
  97. package/dist/core/command/menu-handler.js +392 -100
  98. package/dist/core/command/menu-protocol.js +45 -6
  99. package/dist/core/command/menu-token-store.js +41 -8
  100. package/dist/core/command/role-menu.js +2 -2
  101. package/dist/core/command/slash-gate.js +34 -4
  102. package/dist/core/command/slash-handler.js +123 -43
  103. package/dist/core/event-catalog.js +4 -0
  104. package/dist/core/evolagent-registry.js +28 -2
  105. package/dist/core/group/invite-service.js +924 -0
  106. package/dist/core/handoff/runtime.js +205 -27
  107. package/dist/core/handoff/store.js +193 -4
  108. package/dist/core/interaction-router.js +22 -8
  109. package/dist/core/message/im-renderer.js +146 -34
  110. package/dist/core/message/message-bridge.js +163 -20
  111. package/dist/core/message/message-log.js +5 -3
  112. package/dist/core/message/message-queue.js +99 -41
  113. package/dist/core/message/message-utils.js +1 -0
  114. package/dist/core/message/response-engine.js +755 -144
  115. package/dist/core/model/catalog-service.js +79 -49
  116. package/dist/core/model/context-window-suffix.js +33 -0
  117. package/dist/core/model/model-catalog.js +1 -1
  118. package/dist/core/model/model-permission.js +12 -3
  119. package/dist/core/permission/approval-gateway.js +36 -3
  120. package/dist/core/permission/ec-command-parser.js +112 -41
  121. package/dist/core/permission/protected-paths.js +134 -16
  122. package/dist/core/permission/sandbox-runtime.js +108 -17
  123. package/dist/core/permission/tool-error-code.js +15 -0
  124. package/dist/core/permission/tool-policy.js +9 -8
  125. package/dist/core/relation/peer-identity.js +57 -6
  126. package/dist/core/relation/shared-data.js +106 -0
  127. package/dist/core/role/runtime-policy.js +64 -0
  128. package/dist/core/runtime-lock.js +10 -26
  129. package/dist/core/session/runner-task-history.js +170 -0
  130. package/dist/core/session/session-manager.js +34 -6
  131. package/dist/core/session/session-mapper.js +41 -0
  132. package/dist/core/session/session-turn-coordinator.js +37 -7
  133. package/dist/core/session/session-turns.js +48 -7
  134. package/dist/core/task/task-recovery-service.js +176 -0
  135. package/dist/core/task/task-resume-operation.js +32 -0
  136. package/dist/eck/group-rules-sync.js +5 -3
  137. package/dist/eck/kit-renderer.js +1 -0
  138. package/dist/index.js +588 -217
  139. package/dist/ipc.js +193 -15
  140. package/dist/paths.js +17 -4
  141. package/dist/product.js +2 -0
  142. package/dist/response-system/engines/v1/proactive-flow.js +59 -2
  143. package/dist/stats/billing.js +41 -15
  144. package/dist/stats/db.js +159 -159
  145. package/dist/stats/eck-vars.js +9 -9
  146. package/dist/stats/price-resolver.js +28 -10
  147. package/dist/stats/query.js +139 -139
  148. package/dist/stats/role-budget.js +6 -6
  149. package/dist/stats/writer.js +51 -51
  150. package/dist/trigger/event-source.js +19 -0
  151. package/dist/trigger/feedback.js +233 -40
  152. package/dist/trigger/parser.js +55 -5
  153. package/dist/trigger/patch.js +9 -0
  154. package/dist/trigger/scheduler.js +30 -4
  155. package/dist/trigger/validation.js +20 -3
  156. package/dist/utils/cross-platform.js +30 -1
  157. package/dist/utils/ecweb-utils.js +1 -0
  158. package/dist/utils/error-dict.json +160 -160
  159. package/dist/utils/error-utils.js +61 -2
  160. package/dist/utils/evolcore-version.js +129 -0
  161. package/dist/utils/logger.js +26 -14
  162. package/dist/utils/macos-autostart.js +22 -22
  163. package/dist/utils/model-prices.jsonl +23 -23
  164. package/dist/utils/npm-ops.js +176 -35
  165. package/dist/utils/tool-summary.js +59 -3
  166. package/dist/utils/welcome.js +196 -23
  167. package/ecagent/LICENSE +21 -21
  168. package/ecagent/NOTICE +11 -11
  169. package/ecagent/README.md +61 -61
  170. package/ecagent/dist/harness/agent-harness.js +3 -0
  171. package/ecagent/dist/harness/compaction/branch-summarization.js +30 -30
  172. package/ecagent/dist/harness/compaction/compaction.js +83 -83
  173. package/ecagent/dist/harness/messages.js +7 -7
  174. package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
  175. package/kits/docs/GUIDE.md +20 -20
  176. package/kits/docs/INDEX.md +60 -66
  177. package/kits/docs/aun/CHEATSHEET.md +19 -19
  178. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -15
  179. package/kits/docs/channels/aun.md +69 -56
  180. package/kits/docs/channels/feishu.md +56 -56
  181. package/kits/docs/context-assembly.md +379 -367
  182. package/kits/docs/eck_templates/GUIDE.template.md +22 -22
  183. package/kits/docs/eck_templates/INDEX.template.md +28 -28
  184. package/kits/docs/eck_templates/path-registry.template.md +33 -33
  185. package/kits/docs/eck_templates/runtime.template.md +19 -19
  186. package/kits/docs/evolcore/INDEX.md +76 -71
  187. package/kits/docs/evolcore/agent-create.md +202 -165
  188. package/kits/docs/evolcore/agent.md +99 -67
  189. package/kits/docs/evolcore/aid.md +68 -60
  190. package/kits/docs/evolcore/codex-windows-sandbox.md +146 -146
  191. package/kits/docs/evolcore/config.md +241 -195
  192. package/kits/docs/evolcore/contact.md +75 -68
  193. package/kits/docs/evolcore/ctl.md +46 -46
  194. package/kits/docs/evolcore/event.md +216 -216
  195. package/kits/docs/evolcore/fs-architecture.md +1215 -1215
  196. package/kits/docs/evolcore/fs.md +110 -110
  197. package/kits/docs/evolcore/group-collaboration.md +244 -244
  198. package/kits/docs/evolcore/group-fs.md +17 -17
  199. package/kits/docs/evolcore/group-rules.md +251 -251
  200. package/kits/docs/evolcore/group.md +267 -152
  201. package/kits/docs/evolcore/menu-client.md +58 -34
  202. package/kits/docs/evolcore/model.md +50 -50
  203. package/kits/docs/evolcore/msg.md +169 -165
  204. package/kits/docs/evolcore/response.md +75 -75
  205. package/kits/docs/evolcore/rpc.md +37 -37
  206. package/kits/docs/evolcore/self-summary.md +29 -29
  207. package/kits/docs/evolcore/stats.md +83 -83
  208. package/kits/docs/evolcore/storage.md +53 -50
  209. package/kits/docs/evolcore/task.md +14 -0
  210. package/kits/docs/evolcore/trigger.md +623 -575
  211. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -26
  212. package/kits/docs/identity/PATH_OPS.md +16 -16
  213. package/kits/docs/identity/ROLE_DETAIL.md +25 -25
  214. package/kits/docs/identity/identity-tools.md +26 -26
  215. package/kits/docs/path-registry.md +62 -62
  216. package/kits/docs/prompt-loading-architecture.md +269 -269
  217. package/kits/docs/venues/aun-group.md +45 -45
  218. package/kits/docs/venues/aun-private.md +10 -10
  219. package/kits/docs/venues/client-desktop.md +10 -10
  220. package/kits/docs/venues/client-mobile.md +10 -10
  221. package/kits/docs/venues/feishu-group.md +13 -13
  222. package/kits/docs/venues/feishu-private.md +9 -9
  223. package/kits/docs/venues/group.md +42 -25
  224. package/kits/docs/venues/private.md +10 -10
  225. package/kits/eck_manifest.auxiliary.json +43 -43
  226. package/kits/eck_manifest.json +217 -190
  227. package/kits/eck_message_manifest.json +74 -74
  228. package/kits/migrations/README-role-config-v4.md +32 -32
  229. package/kits/migrations/migrate-contact-book-v2.mjs +748 -748
  230. package/kits/migrations/migrate-role-config-v4.mjs +614 -614
  231. package/kits/migrations/migrate-role-config-v5.mjs +365 -365
  232. package/kits/migrations/rename-config-file.mjs +99 -99
  233. package/kits/rules/01-overview.md +166 -162
  234. package/kits/rules/02-navigation.md +96 -96
  235. package/kits/rules/03-identity.md +36 -36
  236. package/kits/rules/04-relation.md +76 -75
  237. package/kits/rules/05-venue.md +60 -60
  238. package/kits/rules/06-channel.md +65 -63
  239. package/kits/schemas/_meta.json +53 -53
  240. package/kits/schemas/agent-config.schema.1.json +177 -177
  241. package/kits/schemas/agent-config.schema.10.json +395 -395
  242. package/kits/schemas/agent-config.schema.11.json +421 -421
  243. package/kits/schemas/agent-config.schema.12.json +451 -427
  244. package/kits/schemas/agent-config.schema.2.json +239 -239
  245. package/kits/schemas/agent-config.schema.3.json +119 -119
  246. package/kits/schemas/agent-config.schema.4.json +208 -208
  247. package/kits/schemas/agent-config.schema.5.json +326 -326
  248. package/kits/schemas/agent-config.schema.6.json +322 -322
  249. package/kits/schemas/agent-config.schema.7.json +303 -303
  250. package/kits/schemas/agent-config.schema.8.json +304 -304
  251. package/kits/schemas/agent-config.schema.9.json +364 -364
  252. package/kits/schemas/contact-book.schema.1.json +36 -36
  253. package/kits/schemas/contact-book.schema.2.json +43 -43
  254. package/kits/schemas/contact-book.schema.3.json +67 -67
  255. package/kits/schemas/daemon.schema.1.json +91 -91
  256. package/kits/schemas/daemon.schema.2.json +101 -101
  257. package/kits/schemas/daemon.schema.3.json +123 -123
  258. package/kits/schemas/daemon.schema.4.json +136 -136
  259. package/kits/schemas/daemon.schema.5.json +135 -135
  260. package/kits/schemas/daemon.schema.6.json +131 -131
  261. package/kits/schemas/defaults.schema.1.json +81 -81
  262. package/kits/schemas/defaults.schema.2.json +85 -85
  263. package/kits/schemas/defaults.schema.3.json +73 -73
  264. package/kits/schemas/defaults.schema.4.json +107 -107
  265. package/kits/schemas/defaults.schema.5.json +119 -119
  266. package/kits/schemas/menu-exec-schema-commands.md +208 -208
  267. package/kits/schemas/migrations/README.md +30 -30
  268. package/kits/schemas/relation-config.schema.1.json +158 -158
  269. package/kits/schemas/relation-config.schema.2.json +73 -73
  270. package/kits/schemas/relation-config.schema.3.json +50 -50
  271. package/kits/schemas/relation-config.schema.4.json +47 -47
  272. package/kits/schemas/relation-config.schema.5.json +47 -47
  273. package/kits/schemas/relation-config.schema.6.json +46 -46
  274. package/kits/schemas/relation-config.schema.7.json +59 -59
  275. package/kits/schemas/relation-config.schema.8.json +103 -103
  276. package/kits/schemas/role-config.schema.1.json +215 -201
  277. package/kits/schemas/role-registry.schema.1.json +35 -35
  278. package/kits/schemas/single-session.schema.1.json +31 -31
  279. package/kits/schemas/single-session.schema.2.json +57 -57
  280. package/kits/schemas/single-session.schema.3.json +32 -32
  281. package/kits/templates/bootstrap-welcome.md +17 -17
  282. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -9
  283. package/kits/templates/message-fragments/handoff-request-to-target.md +19 -19
  284. package/kits/templates/message-fragments/handoff-response-to-origin.md +9 -9
  285. package/kits/templates/message-fragments/inject-default.md +2 -2
  286. package/kits/templates/message-fragments/item.md +2 -2
  287. package/kits/templates/roles/admin.json +92 -92
  288. package/kits/templates/roles/member.json +101 -96
  289. package/kits/templates/roles/owner.json +9 -9
  290. package/kits/templates/roles/visitor.json +90 -85
  291. package/kits/templates/system-fragments/baseagent.md +14 -14
  292. package/kits/templates/system-fragments/bootstrap.md +53 -34
  293. package/kits/templates/system-fragments/channel.md +28 -64
  294. package/kits/templates/system-fragments/commands.md +36 -28
  295. package/kits/templates/system-fragments/group-unread-history.md +7 -0
  296. package/kits/templates/system-fragments/identity.md +13 -13
  297. package/kits/templates/system-fragments/relation.md +19 -19
  298. package/kits/templates/system-fragments/reply.md +56 -0
  299. package/kits/templates/system-fragments/session.md +59 -59
  300. package/kits/templates/system-fragments/venue.md +31 -31
  301. package/package.json +104 -98
  302. package/dist/aun/group-identity.js +0 -9
  303. /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
  304. /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
@@ -1,8 +1,12 @@
1
+ import { initializationBlocksTasks } from '../../cli/agent-create.js';
1
2
  import path from 'path';
2
3
  import fs from 'fs';
3
4
  import os from 'os';
4
5
  import crypto from 'crypto';
5
- import { BaseagentRunnerUnavailableError, hasCompact, autoCompactWindowForContextWindow, autoCompactWindowForModel, isClaudeContextUsageModel, numericToken } from '../../agents/runner-types.js';
6
+ import { BaseagentRunnerUnavailableError, hasCompact, autoCompactWindowForModel, isClaudeContextUsageModel, numericToken, formatAsyncQuestionAnswer, parseAsyncUserQuestions, } from '../../agents/runner-types.js';
7
+ import { loadedBaseagentIds } from '../../agents/baseagent-availability.js';
8
+ import { evaluateInboundAdmissionPreflight, evaluateOrdinaryInboundAdmission } from './message-admission.js';
9
+ import { authorizeAccess, authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
6
10
  import { buildTaskRuntimeEnv, ensureRuntimeLockDir, ensureSessionRuntimeDir } from '../../cli/task-context.js';
7
11
  import { IMRenderer } from './im-renderer.js';
8
12
  import { createSendFileMarkerPattern } from './file-markers.js';
@@ -10,10 +14,11 @@ import { createTextInferenceProvider } from '../inference/text-inference.js';
10
14
  import { StreamIdleMonitor } from './stream-idle-monitor.js';
11
15
  import { logger } from '../../utils/logger.js';
12
16
  import { isHostChinese } from '../../utils/locale.js';
13
- import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, isAbortTerminalReason, prefixErrorType, isRetryableError, isContextTooLongText } from '../../utils/error-utils.js';
14
- import { isExactEvolcoreSendCommandForSession } from '../permission/ec-command-parser.js';
15
- import { summarizeToolInput } from '../../utils/tool-summary.js';
16
- import { agentDir as resolveAgentDir, agentMdPath, getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
17
+ import { ExecutionTimeoutError, OrphanedTargetSessionError, isExecutionFailure, getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, isAbortTerminalReason, prefixErrorType, isRetryableError, isContextTooLongText } from '../../utils/error-utils.js';
18
+ import { readEvolcoreRuntimeFlavor, tagErrorTextWithEvolcoreVersion } from '../../utils/evolcore-version.js';
19
+ import { describeProactiveSendCommandFailure, isExactEvolcoreSendCommandForSession } from '../permission/ec-command-parser.js';
20
+ import { summarizeToolInputForDisplay } from '../../utils/tool-summary.js';
21
+ import { agentDataDir, agentDir as resolveAgentDir, agentMdPath, getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
17
22
  import { renderKitSections } from '../../eck/kit-renderer.js';
18
23
  import { renderMessageBody } from '../../eck/message-renderer.js';
19
24
  import { syncGroupRulesContext } from '../../eck/group-rules-sync.js';
@@ -21,8 +26,9 @@ import { consumeHints, hintsToSubMessages, composeHintFallback } from './pending
21
26
  import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
22
27
  import { recordCausationLink, recordCausationSpan } from '../causation/audit.js';
23
28
  import { normalizeBaseagent } from '../../agents/baseagent.js';
29
+ import { renderActionAsText } from '../interaction-router.js';
24
30
  import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
25
- import { buildEnvelope, replyContextFromSession, withEnvelopeReplyContext } from './message-utils.js';
31
+ import { buildEnvelope, isInteractionSendAccepted, replyContextFromSession, sendInteractionPayload, withEnvelopeReplyContext } from './message-utils.js';
26
32
  import { isSystemOrServicePeer, resolveChatMode } from './peer-mode.js';
27
33
  import { AGENT_DELEGATION_TOKEN_ENV } from '../auth/agent-delegation.js';
28
34
  import { hasTrustedPrincipal } from '../auth/authenticated-actor.js';
@@ -51,7 +57,290 @@ import { classifyToolErrorCode, normalizeToolErrorCode } from '../permission/too
51
57
  import { normalizeExecutionPermissionMode } from '../permission/mode.js';
52
58
  import { buildToolLifecycleEventKey } from '../audit/event-key.js';
53
59
  import { isFullAccessEnabled, loadDaemonConfig } from '../../config-store.js';
60
+ import { assertAgentPolicyAuthorization } from '../role/runtime-policy.js';
54
61
  import { auditFullAccessEvent } from '../auth/authorization-audit.js';
62
+ import { TaskRecoveryService, isTaskRecoveryCurrent } from '../task/task-recovery-service.js';
63
+ const QUESTION_TTL_MS = 24 * 60 * 60 * 1000;
64
+ /** Pending async questions outlive the originating turn, but never change its backend binding. */
65
+ export class AsyncQuestionService {
66
+ deps;
67
+ registered = new Map();
68
+ deliveries = new Map();
69
+ constructor(deps) {
70
+ this.deps = deps;
71
+ }
72
+ directory(selfAid) {
73
+ return this.deps.directory?.(selfAid) ?? path.join(agentDataDir(selfAid), 'async-questions');
74
+ }
75
+ file(selfAid, id) {
76
+ if (!/^aq-[a-f0-9]{40}$/.test(id))
77
+ throw new Error('Invalid async question id');
78
+ return path.join(this.directory(selfAid), `${id}.json`);
79
+ }
80
+ save(record) {
81
+ fs.mkdirSync(this.directory(record.selfAid), { recursive: true, mode: 0o700 });
82
+ const destination = this.file(record.selfAid, record.id);
83
+ const temporary = `${destination}.tmp`;
84
+ const fd = fs.openSync(temporary, 'w', 0o600);
85
+ try {
86
+ fs.writeFileSync(fd, JSON.stringify(record));
87
+ fs.fsyncSync(fd);
88
+ }
89
+ finally {
90
+ fs.closeSync(fd);
91
+ }
92
+ // Replace atomically, without unlinking the previous durable state first.
93
+ fs.renameSync(temporary, destination);
94
+ }
95
+ read(selfAid, id) {
96
+ try {
97
+ const value = JSON.parse(fs.readFileSync(this.file(selfAid, id), 'utf8'));
98
+ if (value.version !== 1 || value.selfAid !== selfAid || value.id !== id
99
+ || !value.sessionId || !value.providerThreadId || !value.itemId
100
+ || !value.origin?.peerId || !value.channelKey || !Number.isFinite(value.expiresAt))
101
+ return undefined;
102
+ parseAsyncUserQuestions([value.question]);
103
+ return value;
104
+ }
105
+ catch (error) {
106
+ if (error.code !== 'ENOENT')
107
+ throw error;
108
+ return undefined;
109
+ }
110
+ }
111
+ async present(session, request, message, adapter, envelope) {
112
+ if (!session.selfAID || !message.peerId)
113
+ throw new Error('Async question requires an authenticated route');
114
+ const questions = parseAsyncUserQuestions(request.questions);
115
+ const records = [];
116
+ for (const [index, question] of questions.entries()) {
117
+ const hash = crypto.createHash('sha256').update(JSON.stringify([
118
+ session.selfAID, session.id, request.providerThreadId, request.itemId, index,
119
+ ])).digest('hex').slice(0, 40);
120
+ const id = `aq-${hash}`;
121
+ const existing = this.read(session.selfAID, id);
122
+ if (existing && existing.status !== 'pending')
123
+ continue;
124
+ const replyContext = {
125
+ delivery: envelope.delivery ?? envelope.replyContext?.delivery,
126
+ threadId: session.threadId || undefined,
127
+ metadata: {
128
+ encrypted: message.encrypted ?? envelope.replyContext?.metadata?.encrypted,
129
+ chatmode: envelope.chatmode,
130
+ },
131
+ };
132
+ const record = existing ?? {
133
+ version: 1, id, sessionId: session.id, selfAid: session.selfAID,
134
+ providerThreadId: request.providerThreadId, providerTurnId: request.providerTurnId,
135
+ itemId: request.itemId, question,
136
+ origin: {
137
+ channel: message.channel, channelType: session.channelType,
138
+ channelId: session.channelId, peerId: message.peerId, peerType: message.peerType,
139
+ chatType: session.chatType === 'group' ? 'group' : 'private',
140
+ threadId: session.threadId || undefined, selfAID: session.selfAID,
141
+ },
142
+ envelope: { ...envelope, sessionId: session.id, replyContext, delivery: replyContext.delivery },
143
+ channelKey: adapter.channelKey, createdAt: Date.now(), expiresAt: Date.now() + QUESTION_TTL_MS,
144
+ status: 'pending',
145
+ };
146
+ if (!existing)
147
+ this.save(record);
148
+ records.push(record);
149
+ }
150
+ // Persist the complete batch before dispatch; one failed card must not lose the other questions.
151
+ const errors = [];
152
+ for (const record of records) {
153
+ try {
154
+ await this.deliver(record, adapter);
155
+ }
156
+ catch (error) {
157
+ errors.push(error);
158
+ }
159
+ }
160
+ if (errors.length)
161
+ throw new AggregateError(errors, 'Async question delivery failed; pending questions retained');
162
+ }
163
+ async restore(adapter, selfAid) {
164
+ let names;
165
+ try {
166
+ names = fs.readdirSync(this.directory(selfAid));
167
+ }
168
+ catch (error) {
169
+ if (error.code === 'ENOENT')
170
+ return;
171
+ throw error;
172
+ }
173
+ for (const name of names) {
174
+ if (!/^aq-[a-f0-9]{40}\.json$/.test(name))
175
+ continue;
176
+ try {
177
+ const record = this.read(selfAid, name.slice(0, -5));
178
+ if (!record || record.channelKey !== adapter.channelKey)
179
+ continue;
180
+ if (this.registered.get(record.id) === adapter)
181
+ continue;
182
+ if (record.expiresAt <= Date.now()) {
183
+ fs.unlinkSync(this.file(selfAid, record.id));
184
+ continue;
185
+ }
186
+ if (record.status === 'submitting') {
187
+ // A crash may have happened after Codex accepted the steer. Never replay it blindly.
188
+ record.status = 'uncertain';
189
+ record.reason = 'restart_during_submission';
190
+ this.save(record);
191
+ }
192
+ if (record.status === 'pending')
193
+ await this.deliver(record, adapter);
194
+ }
195
+ catch (error) {
196
+ logger.warn(`[AsyncQuestions] restore failed: id=${name} error=${String(error)}`);
197
+ }
198
+ }
199
+ }
200
+ async validSession(record) {
201
+ const session = await this.deps.getSession(record.sessionId);
202
+ if (!session || session.baseagent !== 'codex' || session.selfAID !== record.selfAid
203
+ || session.agentSessionId !== record.providerThreadId
204
+ || session.channel !== record.origin.channel
205
+ || session.channelType !== record.origin.channelType
206
+ || session.channelId !== record.origin.channelId
207
+ || (session.threadId || '') !== (record.origin.threadId || '')
208
+ || !(await this.deps.authorize(record, session)))
209
+ return undefined;
210
+ return session;
211
+ }
212
+ /** Recheck persisted answers after queue delays/restarts, immediately before a provider run. */
213
+ async validateQueuedAnswer(session, answer) {
214
+ if (!session.selfAID)
215
+ return false;
216
+ const record = this.read(session.selfAID, answer.requestId);
217
+ return !!record && record.sessionId === session.id
218
+ && ['submitting', 'answered', 'uncertain'].includes(record.status)
219
+ && record.providerThreadId === answer.providerThreadId
220
+ && record.itemId === answer.questionItemId && record.question.title === answer.question
221
+ && record.answer === answer.answer && !!(await this.validSession(record));
222
+ }
223
+ async deliver(record, adapter) {
224
+ if (this.registered.get(record.id) === adapter)
225
+ return;
226
+ // A channel reload replaces its callback/card registry. Rebind pending questions to the new adapter.
227
+ if (this.registered.has(record.id))
228
+ await this.deps.router.cancel(record.id, 'daemon_restart');
229
+ const previous = this.deliveries.get(record.id);
230
+ if (previous)
231
+ return previous;
232
+ const delivery = this.deliverOnce(record, adapter);
233
+ this.deliveries.set(record.id, delivery);
234
+ try {
235
+ await delivery;
236
+ }
237
+ finally {
238
+ this.deliveries.delete(record.id);
239
+ }
240
+ }
241
+ async deliverOnce(record, adapter) {
242
+ if (record.expiresAt <= Date.now() || !(await this.validSession(record))) {
243
+ record.status = 'cancelled';
244
+ record.reason = 'expired_or_binding_changed';
245
+ this.save(record);
246
+ return;
247
+ }
248
+ const options = record.question.options ?? [];
249
+ const interaction = {
250
+ type: 'interaction', id: record.id, sessionId: record.sessionId, channelId: record.origin.channelId,
251
+ initiatorId: record.origin.peerId, blocking: false, expiresAt: record.expiresAt,
252
+ kind: {
253
+ kind: 'action', title: '请补充信息', body: record.question.title, bodyFormat: 'plain',
254
+ buttons: options.map((label, index) => ({ key: `opt-${index}`, label })),
255
+ allowCustomInput: true,
256
+ },
257
+ fallback: {
258
+ command: 'ask',
259
+ buttonArgMap: Object.fromEntries(options.map((_, index) => [`opt-${index}`, `${record.id} ${index + 1}`])),
260
+ acceptFreeText: true, freeTextHint: `回复 /ask ${record.id} <回答>`,
261
+ },
262
+ };
263
+ // Register before dispatch: a fast reply must not race card delivery.
264
+ this.registered.set(record.id, adapter);
265
+ this.deps.router.register(record.id, record.sessionId, async (action, values) => {
266
+ const current = this.read(record.selfAid, record.id);
267
+ if (!current || current.status !== 'pending' || current.expiresAt <= Date.now()) {
268
+ await this.deps.router.cancel(record.id, 'expired_or_resolved');
269
+ return false;
270
+ }
271
+ const answer = this.answerText(current.question, action, values);
272
+ if (!answer)
273
+ return false;
274
+ const session = await this.validSession(current);
275
+ if (!session) {
276
+ await this.deps.router.cancel(record.id, 'binding_or_authorization_changed');
277
+ return false;
278
+ }
279
+ current.answer = answer;
280
+ current.status = 'submitting';
281
+ this.save(current);
282
+ try {
283
+ await this.deps.submit(current, session, {
284
+ requestId: current.id, providerThreadId: current.providerThreadId,
285
+ questionItemId: current.itemId, question: current.question.title, answer,
286
+ });
287
+ current.status = 'answered';
288
+ this.save(current);
289
+ this.registered.delete(record.id);
290
+ logger.info(`[AsyncQuestions] answer accepted: session=${record.sessionId} request=${record.id}`);
291
+ return true;
292
+ }
293
+ catch (error) {
294
+ current.status = 'uncertain';
295
+ current.reason = 'submission_failed';
296
+ this.save(current);
297
+ logger.warn(`[AsyncQuestions] answer submission failed: request=${record.id} error=${String(error)}`);
298
+ throw error;
299
+ }
300
+ }, {
301
+ initiatorId: record.origin.peerId, initiatorChannelKey: adapter.channelKey,
302
+ blocking: false, retainOnRejected: true, fallbackCommand: 'ask', timeoutMs: record.expiresAt - Date.now(),
303
+ onCancel: async (reason) => {
304
+ this.registered.delete(record.id);
305
+ if (reason === 'daemon_restart')
306
+ return;
307
+ const current = this.read(record.selfAid, record.id);
308
+ if (current?.status === 'pending') {
309
+ current.status = 'cancelled';
310
+ current.reason = reason;
311
+ this.save(current);
312
+ }
313
+ await adapter.invalidateInteraction?.(record.id, reason);
314
+ },
315
+ });
316
+ try {
317
+ const receipt = await sendInteractionPayload(adapter, {
318
+ ...record.envelope, operationId: `${record.id}-${Date.now()}`,
319
+ }, interaction, renderActionAsText(interaction), record.envelope.replyContext);
320
+ if (!isInteractionSendAccepted(receipt))
321
+ throw new Error('Async question card was not accepted for delivery');
322
+ }
323
+ catch (error) {
324
+ // Keep the durable pending record for a later channel reconnect/daemon restore.
325
+ await this.deps.router.cancel(record.id, 'daemon_restart');
326
+ throw error;
327
+ }
328
+ }
329
+ answerText(question, action, values) {
330
+ if (action === 'cancel' || action === 'dismiss' || values?.behavior === 'dismiss')
331
+ return undefined;
332
+ let answer;
333
+ if (action === '_custom_input')
334
+ answer = typeof values?.custom_text === 'string' ? values.custom_text.trim() : undefined;
335
+ else if (/^opt-\d+$/.test(action))
336
+ answer = question.options?.[Number(action.slice(4))];
337
+ else if (question.options?.length && /^\d+$/.test(action))
338
+ answer = question.options[Number(action) - 1];
339
+ else
340
+ answer = action.trim();
341
+ return answer && answer.length <= 65_536 ? answer : undefined;
342
+ }
343
+ }
55
344
  const INBOUND_MESSAGE_LOG_TEXT_MAX_LENGTH = 240;
56
345
  export function formatInboundMessageLogText(text) {
57
346
  const singleLine = text.replace(/\s+/gu, ' ').trim();
@@ -485,9 +774,6 @@ export async function resolveGroupRulesForTurn(opts, sync = syncGroupRulesContex
485
774
  }
486
775
  return sync({ selfAid: opts.selfAid, groupId: opts.groupId, channel: opts.channel });
487
776
  }
488
- function autoCompactTokensFromMaxTokens(maxTokens) {
489
- return autoCompactWindowForContextWindow(maxTokens);
490
- }
491
777
  /**
492
778
  * 构造 OutboundEnvelope —— 出站三件套的信封部分。
493
779
  *
@@ -521,12 +807,15 @@ export class ResponseEngine {
521
807
  primaryRunnerKey;
522
808
  interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
523
809
  timeoutErrors = new Map();
810
+ activeTimeoutHandlers = new Map();
524
811
  /** sessionId → 已成功的模型降级状态(进程重启清零) */
525
812
  modelFallbackMap = new Map();
526
813
  /** sessionId → interruptible task-level API retry delays. */
527
814
  retryDelayWaiters = new Map();
528
815
  interactionRouter;
816
+ asyncQuestions;
529
817
  messageQueue;
818
+ taskRecoveryService;
530
819
  handoffRuntime;
531
820
  /** sessionId → 活跃的空闲监控器,用于等待用户交互期间暂停/恢复计时 */
532
821
  activeMonitors = new Map();
@@ -600,10 +889,7 @@ export class ResponseEngine {
600
889
  return createTextInferenceProvider(baseagent, config);
601
890
  }
602
891
  getAvailableBaseagentsForOwner(evolName) {
603
- const prefix = `${evolName}::`;
604
- return [...this.agentMap.keys()]
605
- .filter(key => key.startsWith(prefix))
606
- .map(key => key.slice(prefix.length));
892
+ return loadedBaseagentIds(this.agentMap.keys(), evolName);
607
893
  }
608
894
  async sendBaseagentMismatch(channelKey, channelId, session, error, replyContext) {
609
895
  const channelInfo = this.resolveChannelInfo(channelKey);
@@ -744,6 +1030,12 @@ export class ResponseEngine {
744
1030
  }
745
1031
  setInteractionRouter(router) {
746
1032
  this.interactionRouter = router;
1033
+ this.asyncQuestions = new AsyncQuestionService({
1034
+ router,
1035
+ getSession: id => this.sessionManager.getSessionById(id),
1036
+ authorize: async (record, session) => this.authorizeAsyncQuestion(record, session),
1037
+ submit: (record, session, answer) => this.submitAsyncQuestionAnswer(record, session, answer),
1038
+ });
747
1039
  // 等待用户交互期间暂停 idle 监控,应答/取消/超时后恢复——
748
1040
  // 避免把「正在等用户点按钮」误判为「任务卡死」而中断任务。
749
1041
  router.setWaitHooks({
@@ -754,9 +1046,118 @@ export class ResponseEngine {
754
1046
  this.activeMonitors.get(sessionId)?.resume();
755
1047
  },
756
1048
  });
1049
+ for (const { adapter } of this.channels.values())
1050
+ this.restoreAsyncQuestions(adapter);
757
1051
  }
758
1052
  setMessageQueue(queue) {
759
1053
  this.messageQueue = queue;
1054
+ this.taskRecoveryService = new TaskRecoveryService({
1055
+ sessions: this.sessionManager,
1056
+ queue,
1057
+ getRunner: (session) => this.getAgent(session.metadata?.channelKey || session.channel, session.baseagent, session.selfAID),
1058
+ resumePause: (sessionId) => this.resumeSession(sessionId),
1059
+ hasChannel: (session) => !!this.resolveChannelInfo(session.metadata?.channelKey || session.channel),
1060
+ });
1061
+ }
1062
+ async recoverTask(request) {
1063
+ if (!this.taskRecoveryService) {
1064
+ return { ok: false, status: 'not_recoverable', sessionId: request.sessionId, message: '消息队列尚未初始化' };
1065
+ }
1066
+ return this.taskRecoveryService.resume(request);
1067
+ }
1068
+ authorizeAsyncQuestion(record, session) {
1069
+ const origin = record.origin;
1070
+ const owner = this.agentRegistry?.resolveByChannel(origin.channel);
1071
+ if (owner?.aid !== record.selfAid)
1072
+ return false;
1073
+ const chatType = origin.chatType === 'group' ? 'group' : 'private';
1074
+ const channelType = origin.channelType || record.channelKey.split('#')[0];
1075
+ const preflight = evaluateInboundAdmissionPreflight({
1076
+ selfAid: record.selfAid, channelKey: record.channelKey, channelType, chatType, actorId: origin.peerId,
1077
+ });
1078
+ if (!preflight.allow || !evaluateOrdinaryInboundAdmission(preflight.context).allow)
1079
+ return false;
1080
+ const identity = this.inferDirectSessionIdentity({ ...origin, content: '' }, owner);
1081
+ if (!identity || identity.role === 'none')
1082
+ return false;
1083
+ return authorizeAccess(buildAuthSubject({
1084
+ selfAid: record.selfAid, channel: record.channelKey, channelType, chatType,
1085
+ actorId: origin.peerId, channelId: session.channelId, conversationId: session.channelId, identity,
1086
+ })).allow;
1087
+ }
1088
+ async submitAsyncQuestionAnswer(record, session, answer) {
1089
+ const runner = this.getAgent(record.origin.channel, 'codex', record.selfAid);
1090
+ if (!runner.steerAsyncQuestionAnswer)
1091
+ throw new Error('Codex async input is unavailable');
1092
+ if (await runner.steerAsyncQuestionAnswer(session.id, answer) === 'accepted')
1093
+ return;
1094
+ if (!this.messageQueue)
1095
+ throw new Error('Async question resume queue is unavailable');
1096
+ // The queue owns idle-thread restoration and a fresh task/permission context.
1097
+ // Keep the exact Session/backend identity; never create a new topic for an old card.
1098
+ await this.messageQueue.enqueuePersisted(session.id, {
1099
+ ...record.origin, content: formatAsyncQuestionAnswer(answer),
1100
+ asyncQuestionAnswer: answer, boundSessionId: session.id, baseagent: 'codex',
1101
+ source: 'card-trigger', messageId: `async-answer-${record.id}`,
1102
+ timestamp: Date.now(), receivedAt: Date.now(),
1103
+ encrypted: record.envelope.replyContext?.metadata?.encrypted,
1104
+ replyContext: record.envelope.replyContext,
1105
+ }, session.projectPath, {
1106
+ interruptible: false, interruptSamePeer: false,
1107
+ agentName: this.agentRegistry?.resolveByChannel(record.origin.channel)?.name,
1108
+ selfAID: record.selfAid, sessionKeyField: session.sessionKey,
1109
+ });
1110
+ }
1111
+ /** Read the exact attempt's lifecycle without touching any newer task. */
1112
+ async inspectTargetExecution(sessionId, attemptId, selfAid) {
1113
+ const session = await this.sessionManager.getSessionById(sessionId);
1114
+ if (!session || session.deletedAt || session.selfAID !== selfAid)
1115
+ return { state: 'missing' };
1116
+ const state = this.turnCoordinator.current(session);
1117
+ const terminal = state.lastTerminal;
1118
+ const matches = (context) => (context?.triggerAttemptId ?? context?.recovery?.attemptId) === attemptId;
1119
+ if (terminal && matches(terminal.recoveryContext)) {
1120
+ return { state: 'terminal', taskId: terminal.taskId, generation: terminal.generation,
1121
+ outcome: terminal.status === 'committed' ? 'completed'
1122
+ : ['cancelled', 'superseded', 'interrupted'].includes(terminal.status) ? 'skipped' : 'failed',
1123
+ reason: terminal.reason };
1124
+ }
1125
+ const queued = this.messageQueue?.getMessageState(sessionId, attemptId, selfAid);
1126
+ if (queued?.state === 'queued')
1127
+ return { state: 'live' };
1128
+ const active = state.active;
1129
+ if (active && matches(active.recoveryContext)) {
1130
+ const identity = { taskId: active.taskId, generation: active.generation };
1131
+ if (active.status === 'waiting_interaction' || active.status === 'interrupting'
1132
+ || active.interactions.some(item => !item.settlement)
1133
+ || this.isPauseRequested(sessionId) || this.retryDelayWaiters.has(sessionId))
1134
+ return { state: 'live', ...identity };
1135
+ const runner = this.getAgent(session.metadata?.channelKey || session.channel, session.baseagent, selfAid);
1136
+ return { state: runner.hasActiveStream(sessionId) ? 'live' : 'missing', ...identity,
1137
+ hasTerminalOwner: this.activeTimeoutHandlers.has(sessionId) };
1138
+ }
1139
+ // Never adopt or interrupt a newer task to clean up an older attempt.
1140
+ if (active) {
1141
+ return { state: 'terminal', outcome: 'skipped', reason: 'stale_generation',
1142
+ taskId: active.taskId, generation: active.generation };
1143
+ }
1144
+ if (queued?.state === 'active') {
1145
+ return { state: 'missing', hasTerminalOwner: this.activeTimeoutHandlers.has(sessionId) };
1146
+ }
1147
+ if (queued?.state === 'missing' && (active || terminal)) {
1148
+ const current = active ?? terminal;
1149
+ return { state: 'terminal', outcome: 'skipped', reason: 'stale_generation',
1150
+ taskId: current.taskId, generation: current.generation };
1151
+ }
1152
+ return { state: 'missing' };
1153
+ }
1154
+ /** Outer execution deadlines use the same terminal owner as runner idle timeouts. */
1155
+ async timeoutSession(sessionId, error) {
1156
+ const timeout = this.activeTimeoutHandlers.get(sessionId);
1157
+ if (timeout)
1158
+ await timeout(error);
1159
+ else
1160
+ await this.interruptSession(sessionId, error instanceof OrphanedTargetSessionError ? 'execution_error' : 'timeout');
760
1161
  }
761
1162
  async interruptSession(sessionId, reason) {
762
1163
  this.clearPauseSession(sessionId);
@@ -810,7 +1211,7 @@ export class ResponseEngine {
810
1211
  // TimeoutController owns timeout delivery (including user-facing timeout
811
1212
  // details) and already guarantees a single terminal status. Sending it
812
1213
  // here as well would race that barrier and duplicate status.timeout.
813
- if (!interruptedTaskId || effectiveReason === 'timeout')
1214
+ if (!interruptedTaskId || effectiveReason === 'timeout' || effectiveReason === 'execution_error')
814
1215
  return Promise.resolve();
815
1216
  const channelInfo = this.resolveChannelInfo(channelKey);
816
1217
  if (!channelInfo)
@@ -915,9 +1316,25 @@ export class ResponseEngine {
915
1316
  causation: runtime?.causation,
916
1317
  });
917
1318
  }
1319
+ async recoverAuthorizedHandoffResult(params, selfAid) {
1320
+ if (!this.handoffRuntime) {
1321
+ return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff runtime not configured' };
1322
+ }
1323
+ const runtime = this.activeTaskRuntimeContexts.get(params.sessionId);
1324
+ return this.handoffRuntime.recoverReturnHandoff({
1325
+ selfAid,
1326
+ recoverySessionId: params.sessionId,
1327
+ handoffId: params.handoffId,
1328
+ content: params.content,
1329
+ actorId: params.actorId,
1330
+ causation: runtime?.causation,
1331
+ });
1332
+ }
918
1333
  agentRegistry;
919
1334
  setAgentRegistry(registry) {
920
1335
  this.agentRegistry = registry;
1336
+ for (const { adapter } of this.channels.values())
1337
+ this.restoreAsyncQuestions(adapter);
921
1338
  }
922
1339
  /** 更新 EvolAgent.lastActivity —— 每次发出 status.* 事件(含 progress)时调用 */
923
1340
  touchAgentActivity(channelKey) {
@@ -1014,6 +1431,15 @@ export class ResponseEngine {
1014
1431
  if (!this.channelTypeMap.has(type)) {
1015
1432
  this.channelTypeMap.set(type, adapter.channelName);
1016
1433
  }
1434
+ this.restoreAsyncQuestions(adapter);
1435
+ }
1436
+ restoreAsyncQuestions(adapter) {
1437
+ const selfAid = this.agentRegistry?.resolveByChannel(adapter.channelKey)?.aid;
1438
+ if (selfAid && this.asyncQuestions) {
1439
+ void this.asyncQuestions.restore(adapter, selfAid).catch(error => {
1440
+ logger.warn(`[AsyncQuestions] channel restore failed: ${String(error)}`);
1441
+ });
1442
+ }
1017
1443
  }
1018
1444
  /**
1019
1445
  * 注销渠道适配器(热重载断开渠道时调用,避免遗留死实例)。
@@ -1207,7 +1633,10 @@ export class ResponseEngine {
1207
1633
  && (!authorization.triggerId || !authorization.runId || !authorization.attemptId)) {
1208
1634
  throw new Error('fullaccess Trigger authorization lacks scheduler identity or approval provenance');
1209
1635
  }
1210
- if (authorization.source !== 'fullaccess-command' && authorization.source !== 'trigger') {
1636
+ if (authorization.source === 'agent-policy') {
1637
+ assertAgentPolicyAuthorization(authorization);
1638
+ }
1639
+ else if (authorization.source !== 'fullaccess-command' && authorization.source !== 'trigger') {
1211
1640
  throw new Error('fullaccess execution authorization has an unsupported source');
1212
1641
  }
1213
1642
  if (!isFullAccessEnabled()) {
@@ -1229,6 +1658,8 @@ export class ResponseEngine {
1229
1658
  const originChannelKey = message.triggerMeta.originChannelKey;
1230
1659
  if (!selfAid || !actorId || !channelType || !originChannelKey)
1231
1660
  return undefined;
1661
+ const chatType = message.triggerMeta.originChatType ?? 'private';
1662
+ const conversationId = message.triggerMeta.originConversationId ?? actorId;
1232
1663
  const control = message.triggerMeta.originControl === true;
1233
1664
  let actor;
1234
1665
  let resolvedRole = 'none';
@@ -1237,9 +1668,9 @@ export class ResponseEngine {
1237
1668
  selfAid,
1238
1669
  channelKey: originChannelKey,
1239
1670
  channelType,
1240
- chatType: 'private',
1671
+ chatType,
1241
1672
  actorId,
1242
- conversationId: actorId,
1673
+ conversationId,
1243
1674
  });
1244
1675
  actor = detail.actor;
1245
1676
  resolvedRole = detail.effectiveRole || 'none';
@@ -1259,7 +1690,7 @@ export class ResponseEngine {
1259
1690
  channelType,
1260
1691
  originChannelKey,
1261
1692
  control,
1262
- peerKey: formatPeerKey(channelType, actorId),
1693
+ peerKey: formatPeerKey(channelType, chatType === 'group' ? conversationId : actorId),
1263
1694
  };
1264
1695
  }
1265
1696
  resolveHandoffOriginExecutionIdentity(message, selfAid) {
@@ -1425,7 +1856,8 @@ export class ResponseEngine {
1425
1856
  auditFullAccessEvent({
1426
1857
  event: fallbackFullAccessAudit.authorization.source === 'trigger'
1427
1858
  ? 'fullaccess.trigger.execution.ended'
1428
- : 'fullaccess.execution.ended',
1859
+ : fallbackFullAccessAudit.authorization.source === 'agent-policy'
1860
+ ? 'fullaccess.agent-policy.execution.ended' : 'fullaccess.execution.ended',
1429
1861
  source: fallbackFullAccessAudit.authorization.source,
1430
1862
  actorId: fallbackFullAccessAudit.authorization.authorizedBy,
1431
1863
  processRole: fallbackFullAccessAudit.processRole,
@@ -1446,6 +1878,15 @@ export class ResponseEngine {
1446
1878
  async processMessageWithMonitors(message) {
1447
1879
  const idleMs = (this.globalSettings.idleMonitor?.timeout ?? 120) * 1000;
1448
1880
  const totalExecutionMs = this.totalExecutionLimitMs();
1881
+ if (message.taskRecovery) {
1882
+ const current = message.boundSessionId
1883
+ ? await this.sessionManager.getSessionById(message.boundSessionId)
1884
+ : undefined;
1885
+ if (!current || current.selfAID !== message.selfAID || !isTaskRecoveryCurrent(current, message.taskRecovery)) {
1886
+ this.publishTriggerExecutionSkipped(message, 'recovery_stale');
1887
+ return;
1888
+ }
1889
+ }
1449
1890
  if (message.handoffDelivery
1450
1891
  && this.handoffRuntime
1451
1892
  && this.handoffRuntime.isDiscardedDelivery(message)) {
@@ -1508,7 +1949,8 @@ export class ResponseEngine {
1508
1949
  const userRole = session.identity?.role || 'none';
1509
1950
  const isInternalHandoff = message.source === 'handoff';
1510
1951
  const fullAccessAuditCandidate = this.resolveFullAccessAuditCandidate(message);
1511
- const isCurrentFullAccessCommand = fullAccessAuditCandidate?.authorization.source === 'fullaccess-command'
1952
+ const isCurrentFullAccessCommand = (fullAccessAuditCandidate?.authorization.source === 'fullaccess-command'
1953
+ || fullAccessAuditCandidate?.authorization.source === 'agent-policy')
1512
1954
  && fullAccessAuditCandidate.processRole === 'fullaccess-run';
1513
1955
  const isCurrentFullAccessTrigger = fullAccessAuditCandidate?.authorization.source === 'trigger'
1514
1956
  && fullAccessAuditCandidate.processRole === 'fullaccess-run';
@@ -1597,11 +2039,17 @@ export class ResponseEngine {
1597
2039
  claimedByInternal: false,
1598
2040
  terminalDelivered: false,
1599
2041
  claim: () => {
1600
- if (!timeoutControl.error || this.interruptedSessions.get(session.id) !== 'timeout')
2042
+ if (!timeoutControl.error || !['timeout', 'execution_error'].includes(this.interruptedSessions.get(session.id) ?? ''))
1601
2043
  return undefined;
1602
2044
  timeoutControl.claimedByInternal = true;
1603
2045
  return timeoutControl.error;
1604
2046
  },
2047
+ claimTerminal: () => {
2048
+ if (timeoutControl.terminalDelivered)
2049
+ return false;
2050
+ timeoutControl.terminalDelivered = true;
2051
+ return true;
2052
+ },
1605
2053
  };
1606
2054
  const rejectAfterInterruptBarrier = (error) => {
1607
2055
  if (timeoutTriggered)
@@ -1610,9 +2058,10 @@ export class ResponseEngine {
1610
2058
  timeoutControl.error = error;
1611
2059
  this.timeoutErrors.set(session.id, error);
1612
2060
  this.turnCoordinator.hydrate(session);
1613
- this.turnCoordinator.invalidate(session.id, 'timeout');
1614
- this.interruptedSessions.set(session.id, 'timeout');
1615
- const barrier = this.interruptSession(session.id, 'timeout')
2061
+ const reason = error instanceof OrphanedTargetSessionError ? 'execution_error' : 'timeout';
2062
+ this.turnCoordinator.invalidate(session.id, reason);
2063
+ this.interruptedSessions.set(session.id, reason);
2064
+ const barrier = this.interruptSession(session.id, reason)
1616
2065
  .catch(interruptError => {
1617
2066
  logger.debug(`[ResponseEngine] Timeout interrupt barrier failed: ${interruptError instanceof Error ? interruptError.message : String(interruptError)}`);
1618
2067
  });
@@ -1623,6 +2072,11 @@ export class ResponseEngine {
1623
2072
  });
1624
2073
  };
1625
2074
  const triggerAttemptId = message.replyContext?.metadata?.triggerAttemptId;
2075
+ const externalTimeout = async (error) => {
2076
+ rejectAfterInterruptBarrier(error);
2077
+ await timeoutControl.barrier;
2078
+ };
2079
+ this.activeTimeoutHandlers.set(session.id, externalTimeout);
1626
2080
  const resetTimer = (eventType, toolName) => {
1627
2081
  monitor?.recordEvent(eventType || 'unknown', toolName);
1628
2082
  if (typeof triggerAttemptId === 'string') {
@@ -1702,11 +2156,9 @@ export class ResponseEngine {
1702
2156
  await Promise.race(processingPromises);
1703
2157
  }
1704
2158
  catch (error) {
1705
- if (error instanceof Error && (error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT')) {
2159
+ if (error instanceof Error && (isExecutionFailure(error) || error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT')) {
1706
2160
  await timeoutControl.barrier;
1707
- if (!timeoutControl.terminalDelivered) {
1708
- await timeoutControl.deliverUnhandled?.(error);
1709
- }
2161
+ await timeoutControl.deliverUnhandled?.(error);
1710
2162
  this.timeoutErrors.delete(session.id);
1711
2163
  }
1712
2164
  // 超时错误:kill 级别已发送诊断信息,无需再发
@@ -1747,6 +2199,8 @@ export class ResponseEngine {
1747
2199
  if (totalExecutionTimer)
1748
2200
  clearTimeout(totalExecutionTimer);
1749
2201
  this.activeMonitors.delete(streamKey);
2202
+ if (this.activeTimeoutHandlers.get(session.id) === externalTimeout)
2203
+ this.activeTimeoutHandlers.delete(session.id);
1750
2204
  }
1751
2205
  }
1752
2206
  resolveFullAccessAuditCandidate(message) {
@@ -1754,15 +2208,18 @@ export class ResponseEngine {
1754
2208
  if (commandAuthorization?.permissionMode === 'fullaccess'
1755
2209
  && commandAuthorization.processRole === 'fullaccess-run'
1756
2210
  && commandAuthorization.dataScope === 'daemon'
1757
- && commandAuthorization.source === 'fullaccess-command'
2211
+ && (commandAuthorization.source === 'fullaccess-command' || commandAuthorization.source === 'agent-policy')
1758
2212
  && typeof commandAuthorization.authorizedBy === 'string'
1759
2213
  && commandAuthorization.authorizedBy.length > 0) {
1760
2214
  let currentOwner = false;
1761
2215
  try {
1762
2216
  currentOwner = isFullAccessEnabled()
1763
2217
  && new Set(loadDaemonConfig().owners ?? []).has(commandAuthorization.authorizedBy);
2218
+ if (commandAuthorization.source === 'agent-policy')
2219
+ assertAgentPolicyAuthorization(commandAuthorization);
1764
2220
  }
1765
2221
  catch {
2222
+ currentOwner = false;
1766
2223
  // Runtime validation will fail closed; the audit must still retain the
1767
2224
  // attempted authorization without falsely labeling it active.
1768
2225
  }
@@ -1882,7 +2339,7 @@ export class ResponseEngine {
1882
2339
  // states. An untransitioned `created` agent or malformed lifecycle must
1883
2340
  // not fall through to a base runner's default prompt. Missing lifecycle
1884
2341
  // is normalized to active for legacy configurations.
1885
- if (owningAgentForTask && !isActive && !isBootstrapping) {
2342
+ if (owningAgentForTask && ((!isActive && !isBootstrapping) || initializationBlocksTasks(owningAgentForTask.config))) {
1886
2343
  const blockedLifecycle = lifecycle
1887
2344
  ?? `invalid (${String(owningAgentForTask.config.lifecycle)})`;
1888
2345
  logger.error(`[ResponseEngine] Agent lifecycle is not runnable: agent=${owningAgentForTask.aid} lifecycle=${blockedLifecycle}`);
@@ -1979,7 +2436,7 @@ export class ResponseEngine {
1979
2436
  ?? this.resolveHandoffOriginExecutionIdentity(message, selfAid);
1980
2437
  const configChannelType = authorizationIdentity?.channelType ?? currentChannelType;
1981
2438
  const configChatType = authorizationIdentity
1982
- ? (message.handoffDelivery?.originAuthorization?.chatType ?? 'private')
2439
+ ? (message.triggerMeta?.originChatType ?? message.handoffDelivery?.originAuthorization?.chatType ?? 'private')
1983
2440
  : (chatType === 'group' ? 'group' : 'private');
1984
2441
  // A group channel ID identifies the conversation, not the authenticated
1985
2442
  // operator. Keep the historical private-chat fallback (where channelId
@@ -2132,6 +2589,7 @@ export class ResponseEngine {
2132
2589
  modeConfig: resolvedMode.context.modeConfig,
2133
2590
  state: modeState,
2134
2591
  isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, chatType, selfAid),
2592
+ describeSendCommandFailure: describeProactiveSendCommandFailure,
2135
2593
  logger,
2136
2594
  } : null;
2137
2595
  if (resolvedMode?.mode.beforeProcess && modeProcessCtx) {
@@ -2166,25 +2624,30 @@ export class ResponseEngine {
2166
2624
  });
2167
2625
  if (timeoutControl) {
2168
2626
  timeoutControl.deliverUnhandled = async (timeoutError) => {
2169
- if (timeoutControl.terminalDelivered)
2627
+ if (!timeoutControl.claimTerminal())
2170
2628
  return;
2171
- timeoutControl.terminalDelivered = true;
2172
2629
  const isTotalExecutionTimeout = timeoutError.message === 'TOTAL_EXECUTION_TIMEOUT';
2173
- const totalExecutionMs = this.totalExecutionLimitMs();
2174
- const statusPayload = {
2630
+ const totalExecutionMs = timeoutError instanceof ExecutionTimeoutError ? timeoutError.timeoutMs : this.totalExecutionLimitMs();
2631
+ const statusPayload = timeoutError instanceof OrphanedTargetSessionError ? {
2632
+ kind: 'status.error',
2633
+ metadata: { errorType: timeoutError.reason, message: '任务执行状态丢失,已结束本次执行。' },
2634
+ } : {
2175
2635
  kind: 'status.timeout',
2176
2636
  metadata: isTotalExecutionTimeout
2177
- ? (totalExecutionMs === undefined ? {} : { totalExecutionMs })
2637
+ ? { ...(totalExecutionMs === undefined ? {} : { totalExecutionMs }),
2638
+ ...(timeoutError instanceof ExecutionTimeoutError ? { reason: timeoutError.reason } : {}) }
2178
2639
  : { idleSec: getLastIdleSec?.() || undefined },
2179
2640
  };
2180
2641
  const idleSec = getLastIdleSec?.() || 0;
2181
- const userMessage = isTotalExecutionTimeout
2182
- ? (totalExecutionMs === undefined
2183
- ? '⚠️ 任务超过总执行时限,已自动中断'
2184
- : `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`)
2185
- : idleSec > 0
2186
- ? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断`
2187
- : '⚠️ 任务超时,已自动中断';
2642
+ const userMessage = timeoutError instanceof OrphanedTargetSessionError
2643
+ ? '⚠️ 任务执行状态丢失,已结束本次执行。'
2644
+ : isTotalExecutionTimeout
2645
+ ? (totalExecutionMs === undefined
2646
+ ? '⚠️ 任务超过总执行时限,已自动中断'
2647
+ : `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`)
2648
+ : idleSec > 0
2649
+ ? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断`
2650
+ : '⚠️ 任务超时,已自动中断';
2188
2651
  agent.cleanupStream(streamKey);
2189
2652
  this.sessionManager.clearProcessingIfTask(session.id, taskId);
2190
2653
  if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
@@ -2198,13 +2661,13 @@ export class ResponseEngine {
2198
2661
  await adapter.send(envelope, {
2199
2662
  kind: 'result.error',
2200
2663
  text: userMessage,
2201
- reason: isTotalExecutionTimeout ? 'total_execution_timeout' : 'timeout',
2664
+ reason: isExecutionFailure(timeoutError) ? timeoutError.reason : isTotalExecutionTimeout ? 'total_execution_timeout' : 'timeout',
2202
2665
  }).catch(() => { });
2203
2666
  if (daemonTrigger) {
2204
2667
  await adapter.send(envelope, statusPayload).catch(() => { });
2205
2668
  }
2206
2669
  this.touchAgentActivity(channelKey);
2207
- this.publishTriggerExecutionFailure(message, timeoutError.message, {
2670
+ this.publishTriggerExecutionFailure(message, isExecutionFailure(timeoutError) ? timeoutError.reason : timeoutError.message, {
2208
2671
  messageId,
2209
2672
  causation: taskCausation,
2210
2673
  });
@@ -2212,9 +2675,14 @@ export class ResponseEngine {
2212
2675
  this.eventBus.publish({
2213
2676
  type: 'task:error',
2214
2677
  sessionId: session.id,
2678
+ agentAid: runnerSelfAid,
2679
+ peerId: configActorId,
2680
+ encrypted: message.encrypted ?? message.replyContext?.metadata?.encrypted,
2681
+ originTriggerId: message.triggerMeta?.triggerId,
2215
2682
  taskId,
2216
2683
  generation: turnLease?.generation,
2217
2684
  error: timeoutError.message,
2685
+ terminalReason: isExecutionFailure(timeoutError) ? timeoutError.reason : undefined,
2218
2686
  errorType,
2219
2687
  agentName: agentNameForStats,
2220
2688
  causation: taskCausation,
@@ -2343,14 +2811,28 @@ export class ResponseEngine {
2343
2811
  // message is authoritative; session metadata is only a fallback for
2344
2812
  // legacy paths that do not carry a per-message display name.
2345
2813
  const peerName = message.peerName ?? session.metadata?.peerName;
2346
- const peerId = session.metadata?.peerId ?? message.peerId ?? message.channelId;
2814
+ const peerId = message.peerId ?? session.metadata?.peerId ?? message.channelId;
2347
2815
  const peerShort = peerId ? peerId.split('.')[0].split(':')[0] : '?';
2348
2816
  const peerLabel = peerName && peerName !== peerShort ? `${peerShort}(${peerName})` : peerShort;
2349
2817
  logger.info(`[ResponseEngine] session=${session.id} task=${taskId} peer=${peerLabel} chatType=${session.chatType} chatMode=${effectiveChatMode} baseagent=${session.baseagent} msgChatType=${message.chatType ?? 'n/a'}`);
2350
2818
  // Establish the durable turn before exposing any processing state. This
2351
2819
  // lets an immediate /stop or replacement message close the same task and
2352
2820
  // prevents a BusinessTrip reaction from becoming orphaned during setup.
2353
- turnLease = await this.turnCoordinator.begin(session, taskId);
2821
+ if (message.taskRecovery && !isTaskRecoveryCurrent({
2822
+ ...session, metadata: { ...session.metadata, turnState: this.turnCoordinator.current(session) },
2823
+ }, message.taskRecovery)) {
2824
+ this.publishTriggerExecutionSkipped(message, 'recovery_stale');
2825
+ return;
2826
+ }
2827
+ turnLease = await this.turnCoordinator.begin(session, taskId, undefined, configActorId ? {
2828
+ peerId: configActorId,
2829
+ encrypted: message.encrypted ?? message.replyContext?.metadata?.encrypted,
2830
+ baseagent: session.baseagent,
2831
+ agentSessionId: session.agentSessionId,
2832
+ originTriggerId: message.triggerMeta?.triggerId,
2833
+ recovery: message.taskRecovery?.origin,
2834
+ triggerAttemptId: message.triggerMeta?.attemptId,
2835
+ } : undefined);
2354
2836
  // 记录开始处理
2355
2837
  const taskEncrypt = message.replyContext?.metadata?.encrypted != null ? !!(message.replyContext.metadata.encrypted) : undefined;
2356
2838
  this.eventBus.publish({
@@ -2427,7 +2909,10 @@ export class ResponseEngine {
2427
2909
  if (payload.kind === 'result.text' && payload.isFinal) {
2428
2910
  opts.title = '\u2705 \u6700\u7ec8\u56de\u590d:';
2429
2911
  }
2430
- opts.metadata = withTaskMetadata(opts.metadata);
2912
+ opts.metadata = withTaskMetadata({
2913
+ ...(opts.metadata ?? {}),
2914
+ ...(payload.kind === 'result.text' ? payload.metadata : {}),
2915
+ });
2431
2916
  // status.notice is a diagnostic observation, not execution activity.
2432
2917
  if (payload.kind.startsWith('status.') && payload.kind !== 'status.notice') {
2433
2918
  this.touchAgentActivity(channelKey);
@@ -2572,10 +3057,10 @@ export class ResponseEngine {
2572
3057
  };
2573
3058
  })();
2574
3059
  let effectivePermissionMode = 'readonly';
2575
- // Allocate/register the session-owned root before any runner preflight
2576
- // context is installed. Codex may already own a stronger directory;
2577
- // other runners receive a private child of the process TMPDIR.
2578
- const sessionRuntimeDir = agent.getManagedTempDir?.(session.id) ?? ensureSessionRuntimeDir(session.id);
3060
+ // Allocate the stable session-owned root before any runner preflight
3061
+ // context is installed. Every runner for this EvolCore session receives
3062
+ // the same directory, independent of daemon or runner lifetimes.
3063
+ const sessionRuntimeDir = ensureSessionRuntimeDir(session.id);
2579
3064
  const recordExecutionAnomaly = triggerRunId
2580
3065
  ? (anomaly) => {
2581
3066
  recordTriggerExecutionAnomaly(triggerRunId, {
@@ -2623,8 +3108,8 @@ export class ResponseEngine {
2623
3108
  pauseController: taskPauseController,
2624
3109
  preToolUsePolicyHook: pureSessionPolicyHook,
2625
3110
  armApprovedDelegationCommand: this.agentDelegationRegistry
2626
- ? (carrierToken, commandHash) => this.agentDelegationRegistry.armApprovedCommand({
2627
- carrierToken,
3111
+ ? (sessionToken, commandHash) => this.agentDelegationRegistry.armApprovedCommand({
3112
+ sessionToken,
2628
3113
  sessionId: session.id,
2629
3114
  taskId,
2630
3115
  commandHash,
@@ -2764,6 +3249,7 @@ export class ResponseEngine {
2764
3249
  let previousFallbackState;
2765
3250
  let agentModel;
2766
3251
  let v2HandoffIds = [];
3252
+ let v2HandoffResultRefs = [];
2767
3253
  let v2HandoffDirection;
2768
3254
  let handoffPromptRendered = false;
2769
3255
  let runtimeEnv;
@@ -2798,10 +3284,19 @@ export class ResponseEngine {
2798
3284
  if (commandFullAccess.permissionMode !== 'fullaccess'
2799
3285
  || commandFullAccess.processRole !== 'fullaccess-run'
2800
3286
  || commandFullAccess.dataScope !== 'daemon'
2801
- || commandFullAccess.source !== 'fullaccess-command'
3287
+ || (commandFullAccess.source !== 'fullaccess-command' && commandFullAccess.source !== 'agent-policy')
2802
3288
  || !commandFullAccess.authorizedBy) {
2803
3289
  throw new Error('fullaccess command authorization is missing or malformed');
2804
3290
  }
3291
+ if (commandFullAccess.source === 'agent-policy'
3292
+ && (commandFullAccess.agentAid !== runnerSelfAid
3293
+ || configChatType !== 'private'
3294
+ || (message.source !== undefined && message.source !== 'user')
3295
+ || !hasTrustedPrincipal(authenticatedActor)
3296
+ || authenticatedActor.principalId !== commandFullAccess.authorizedBy
3297
+ || authenticatedActor.operatorId !== commandFullAccess.policyActor?.operatorId)) {
3298
+ throw new Error('Agent fullaccess policy authorization is missing its Agent identity');
3299
+ }
2805
3300
  fullAccessAuthorization = commandFullAccess;
2806
3301
  }
2807
3302
  else {
@@ -3016,18 +3511,25 @@ export class ResponseEngine {
3016
3511
  // happen before task-start auto compact because compact is a backend
3017
3512
  // management action just like the subsequent runQuery.
3018
3513
  let runAgentSessionId = session.agentSessionId;
3019
- if (session.threadId
3514
+ if ((session.threadId || message.taskRecovery)
3020
3515
  && typeof this.sessionManager.getSessionById === 'function') {
3021
3516
  const latestBackendSession = await this.sessionManager.getSessionById(session.id);
3022
- if (!latestBackendSession || latestBackendSession.threadId !== session.threadId) {
3517
+ if (!latestBackendSession || latestBackendSession.deletedAt || latestBackendSession.threadId !== session.threadId) {
3023
3518
  throw new Error('topic session changed before backend run started');
3024
3519
  }
3520
+ if (message.taskRecovery && (latestBackendSession.agentSessionId !== message.taskRecovery.agentSessionId
3521
+ || latestBackendSession.baseagent !== message.taskRecovery.baseagent)) {
3522
+ throw new Error('recovery backend changed before execution');
3523
+ }
3025
3524
  runAgentSessionId = latestBackendSession.agentSessionId;
3026
3525
  session.agentSessionId = runAgentSessionId;
3027
3526
  session.metadata = latestBackendSession.metadata;
3028
3527
  }
3029
3528
  modelOverride = {
3030
3529
  ...(modelOverride || {}),
3530
+ ...(message.taskRecovery
3531
+ ? { recovery: { agentSessionId: message.taskRecovery.agentSessionId } }
3532
+ : {}),
3031
3533
  ...(session.threadId
3032
3534
  ? { backend: { kind: 'topic', agentSessionId: runAgentSessionId ?? null } }
3033
3535
  : {}),
@@ -3040,13 +3542,17 @@ export class ResponseEngine {
3040
3542
  },
3041
3543
  };
3042
3544
  const refreshTopicBindingForLogicalRetry = async () => {
3043
- if (!session.threadId || typeof this.sessionManager.getSessionById !== 'function')
3545
+ if ((!session.threadId && !message.taskRecovery) || typeof this.sessionManager.getSessionById !== 'function')
3044
3546
  return;
3045
3547
  const latest = await this.sessionManager.getSessionById(session.id);
3046
3548
  if (!latest || latest.threadId !== session.threadId) {
3047
3549
  throw new Error('topic session changed during backend run');
3048
3550
  }
3049
3551
  const latestAgentSessionId = latest.agentSessionId;
3552
+ if (message.taskRecovery && (latestAgentSessionId !== message.taskRecovery.agentSessionId
3553
+ || latest.baseagent !== message.taskRecovery.baseagent)) {
3554
+ throw new Error('recovery backend changed during execution');
3555
+ }
3050
3556
  session.agentSessionId = latestAgentSessionId;
3051
3557
  session.metadata = latest.metadata;
3052
3558
  if (latestAgentSessionId === runAgentSessionId)
@@ -3083,6 +3589,26 @@ export class ResponseEngine {
3083
3589
  groupId: session.metadata?.groupId || message.channelId,
3084
3590
  channel: currentChannelType || message.channel,
3085
3591
  });
3592
+ const historyGroupId = session.metadata?.groupId || message.channelId;
3593
+ const canUseGroupHistory = (operation) => {
3594
+ if (currentChannelType !== 'aun' || session.chatType !== 'group' || !selfAid || !historyGroupId)
3595
+ return false;
3596
+ const historySubject = buildAuthSubject({
3597
+ selfAid, actorId: configActorId, channel: channelKey, channelType: configChannelType,
3598
+ chatType: configChatType, channelId: message.channelId,
3599
+ conversationId: configChatType === 'group' ? historyGroupId : configActorId,
3600
+ permissionMode: effectivePermissionMode,
3601
+ });
3602
+ return authorizeOperation({
3603
+ source: 'agent-tool', subject: historySubject, audit: false,
3604
+ intent: {
3605
+ operation, scope: ['owner', 'admin'].includes(historySubject.role) ? 'agent' : 'relation',
3606
+ source: 'agent-tool',
3607
+ args: { self: selfAid, targetId: historyGroupId, peer: historyGroupId,
3608
+ peerKey: formatPeerKey('aun', historyGroupId) },
3609
+ },
3610
+ }).allow;
3611
+ };
3086
3612
  // Kit renderer: 组装上下文
3087
3613
  const pkgRoot = getPackageRoot();
3088
3614
  const kitCtx = {
@@ -3098,7 +3624,7 @@ export class ResponseEngine {
3098
3624
  KITS_FRAGMENTS: path.join(pkgRoot, 'kits', 'templates', 'system-fragments'),
3099
3625
  KITS_MESSAGE_FRAGMENTS: path.join(pkgRoot, 'kits', 'templates', 'message-fragments'),
3100
3626
  // evolcore 运行模式:dev=源码仓库 | install=全局安装包
3101
- evolcoreMode: fs.existsSync(path.join(pkgRoot, 'src', 'index.ts')) ? 'dev' : 'install',
3627
+ evolcoreMode: readEvolcoreRuntimeFlavor(),
3102
3628
  // 路径变量(用于 manifest 路径展开,resolvePath 用 ctx.vars 取真值)
3103
3629
  PERSONAL_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'personal') : undefined,
3104
3630
  RELATIONS_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'relations') : undefined,
@@ -3114,11 +3640,12 @@ export class ResponseEngine {
3114
3640
  peerKey,
3115
3641
  peerName: peerName || undefined,
3116
3642
  peerRole,
3117
- peerType: message.peerType || session.metadata?.peerType || undefined,
3643
+ // Display missing identity explicitly without changing the mode resolver's legacy fallback.
3644
+ peerType: peerType || 'unknown',
3118
3645
  sameDevice: message.sameDevice ?? false,
3119
3646
  sameNetwork: message.sameNetwork ?? false,
3120
3647
  sameEgressIp: message.sameEgressIp ?? false,
3121
- groupId: session.metadata?.groupId || undefined,
3648
+ groupId: session.chatType === 'group' ? historyGroupId : undefined,
3122
3649
  groupName: session.metadata?.groupName || undefined,
3123
3650
  // 信封展示用:有群名则「名<ID>」,否则纯 ID。规避模板引擎无 not/else 的限制。
3124
3651
  groupLabel: session.metadata?.groupId
@@ -3133,6 +3660,7 @@ export class ResponseEngine {
3133
3660
  ?? session.metadata?.mentionMode
3134
3661
  ?? message.mentionMode
3135
3662
  ?? undefined,
3663
+ groupHistoryAvailable: canUseGroupHistory('ec.group.history'),
3136
3664
  clientType: message.clientType || undefined,
3137
3665
  permissionMode: effectivePermissionMode,
3138
3666
  capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
@@ -3241,6 +3769,7 @@ export class ResponseEngine {
3241
3769
  v2HandoffIds = Array.from(new Set(items.flatMap(item => (item.handoff?.handoffIds?.length
3242
3770
  ? item.handoff.handoffIds
3243
3771
  : (item.handoff?.handoffId ? [item.handoff.handoffId] : [])))));
3772
+ v2HandoffResultRefs = items.flatMap(item => item.handoff?.resultRefs ?? []);
3244
3773
  v2HandoffDirection = message.handoffDelivery.direction;
3245
3774
  return items;
3246
3775
  }
@@ -3314,18 +3843,23 @@ export class ResponseEngine {
3314
3843
  return;
3315
3844
  }
3316
3845
  if (v2HandoffIds.length > 0 && handoffPromptRendered && this.handoffRuntime && selfAid) {
3317
- for (const handoffId of v2HandoffIds) {
3318
- if (v2HandoffDirection === 'origin')
3319
- this.handoffRuntime.completeOriginContext(selfAid, handoffId);
3320
- else if (v2HandoffDirection === 'target')
3321
- this.handoffRuntime.completeTargetContext(selfAid, handoffId);
3846
+ if (v2HandoffDirection === 'origin' && v2HandoffResultRefs.length > 0) {
3847
+ for (const ref of v2HandoffResultRefs) {
3848
+ this.handoffRuntime.completeOriginContext(selfAid, ref.handoffId, ref.resultId);
3849
+ }
3850
+ }
3851
+ else {
3852
+ for (const handoffId of v2HandoffIds) {
3853
+ if (v2HandoffDirection === 'origin')
3854
+ this.handoffRuntime.completeOriginContext(selfAid, handoffId);
3855
+ else if (v2HandoffDirection === 'target')
3856
+ this.handoffRuntime.completeTargetContext(selfAid, handoffId);
3857
+ }
3322
3858
  }
3323
3859
  }
3324
- // The runner may already own a stronger per-session directory (Codex
3325
- // registers one before constructing its thread). Other runners use a
3326
- // private child of the process-provided TMPDIR. Both paths are passed
3327
- // explicitly so sandboxed helper commands never fall back to a shared
3328
- // agent or project directory for transient state.
3860
+ // Pass the stable session directory explicitly so sandboxed helper
3861
+ // commands never fall back to a process, agent, or project directory
3862
+ // for transient state.
3329
3863
  const taskRuntimeContext = {
3330
3864
  taskId,
3331
3865
  sessionId: session.id,
@@ -3408,7 +3942,8 @@ export class ResponseEngine {
3408
3942
  auditFullAccessEvent({
3409
3943
  event: fullAccessAuthorization.source === 'trigger'
3410
3944
  ? 'fullaccess.trigger.execution.started'
3411
- : 'fullaccess.execution.started',
3945
+ : fullAccessAuthorization.source === 'agent-policy'
3946
+ ? 'fullaccess.agent-policy.execution.started' : 'fullaccess.execution.started',
3412
3947
  source: fullAccessAuthorization.source,
3413
3948
  actorId: fullAccessAuthorization.authorizedBy,
3414
3949
  processRole: fullAccessAuditProcessRole,
@@ -3432,6 +3967,23 @@ export class ResponseEngine {
3432
3967
  }
3433
3968
  fullAccessRunnerInvoked = true;
3434
3969
  }
3970
+ if (message.asyncQuestionAnswer) {
3971
+ if (session.baseagent !== 'codex' || runAgentSessionId !== message.asyncQuestionAnswer.providerThreadId) {
3972
+ throw new Error('Async question backend changed before queued resume');
3973
+ }
3974
+ if (!(await this.asyncQuestions?.validateQueuedAnswer(session, message.asyncQuestionAnswer))) {
3975
+ throw new Error('Async question authorization or persisted answer changed before queued resume');
3976
+ }
3977
+ // An active queue item restored after submission resumes the unfinished work.
3978
+ // Its native answer is already in Codex history and must not be appended twice.
3979
+ if (!message.restartResume?.submitted) {
3980
+ modelOverride = { ...modelOverride, asyncQuestionAnswer: message.asyncQuestionAnswer };
3981
+ }
3982
+ }
3983
+ if (session.baseagent === 'codex') {
3984
+ modelOverride = { ...modelOverride,
3985
+ enableRequestUserInput: effectiveAgentConfig?.baseagents?.codex?.enableRequestUserInput ?? true };
3986
+ }
3435
3987
  const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, runAgentSessionId, renderResult?.images.length ? renderResult.images : message.images, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
3436
3988
  // The turn may be superseded while the runner is still creating its
3437
3989
  // backend query. Replay the interrupt after runQuery resolves, when
@@ -3448,7 +4000,23 @@ export class ResponseEngine {
3448
4000
  const processAttempt = this.processEventStream(stream, session, agent, renderer, (eventType, toolName) => {
3449
4001
  resetTimer(eventType, toolName);
3450
4002
  attemptTimeout?.reset();
3451
- }, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease, effectivePermissionMode, selfAid);
4003
+ }, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease, effectivePermissionMode, selfAid, async (request) => {
4004
+ if (!this.asyncQuestions)
4005
+ throw new Error('Async question service is unavailable');
4006
+ if (permissionContext.approvalInteractionPolicy === 'deny') {
4007
+ logger.info(`[AsyncQuestions] no interactive recipient for unattended task: session=${session.id}`);
4008
+ return;
4009
+ }
4010
+ await renderer.flush(false);
4011
+ try {
4012
+ await this.asyncQuestions.present(session, request, message, adapter, envelope);
4013
+ }
4014
+ catch (error) {
4015
+ // Card dispatch is non-blocking business input. Keep consuming the running turn;
4016
+ // pending records can be delivered after reconnect/restart.
4017
+ logger.warn(`[AsyncQuestions] presentation failed: session=${session.id} error=${String(error)}`);
4018
+ }
4019
+ });
3452
4020
  streamResult = attemptTimeout
3453
4021
  ? await Promise.race([processAttempt, attemptTimeout.promise])
3454
4022
  : await processAttempt;
@@ -3867,7 +4435,7 @@ export class ResponseEngine {
3867
4435
  // `hasReceivedText` 是任务级计数。工具调用前的文字已经显示,并不表示最后一轮
3868
4436
  // (通常来自 complete.result)的文字也已显示;用本轮投影标记避免终态丢失。
3869
4437
  if (shouldSuppress() || !streamResult.hasProjectedCurrentReplyText) {
3870
- renderer.addText(visibleFinalReplyText);
4438
+ await renderer.addText(visibleFinalReplyText, undefined, undefined, streamResult.assistantUuid);
3871
4439
  }
3872
4440
  }
3873
4441
  // 先清理流和处理中状态(保证即使 flush 卡住,session 也不会永久处于"处理中")
@@ -3886,7 +4454,7 @@ export class ResponseEngine {
3886
4454
  const suffix = showHint
3887
4455
  ? `\n\n---\n⚠️ [降级模型: ${usedFallback.model} | 可告诉我"帮我检查可用模型"来诊断]`
3888
4456
  : `\n\n---\n⚠️ [降级模型: ${usedFallback.model}]`;
3889
- renderer.addText(suffix);
4457
+ await renderer.addText(suffix, undefined, undefined, streamResult.assistantUuid);
3890
4458
  if (showHint && curFbState) {
3891
4459
  curFbState.hintShown = true;
3892
4460
  this.modelFallbackMap.set(session.id, curFbState);
@@ -3899,7 +4467,14 @@ export class ResponseEngine {
3899
4467
  }
3900
4468
  else {
3901
4469
  // Flush 剩余内容(文件标记已在 flush 时自动移除)
3902
- await renderer.flush(true);
4470
+ // A final text flush can also contain failed output. Preserve the
4471
+ // outcome so AUN does not label that text as normal completion.
4472
+ if (streamResult.isError && !isExpectedTerminalInterrupt(streamResult.terminalReason)) {
4473
+ await renderer.flush(true, 'failed');
4474
+ }
4475
+ else {
4476
+ await renderer.flush(!isExpectedTerminalInterrupt(streamResult.terminalReason));
4477
+ }
3903
4478
  }
3904
4479
  const terminalInterrupt = isExpectedTerminalInterrupt(streamResult.terminalReason);
3905
4480
  if (!streamResult.isError && !terminalInterrupt) {
@@ -3922,7 +4497,8 @@ export class ResponseEngine {
3922
4497
  }
3923
4498
  }
3924
4499
  else {
3925
- await this.turnCoordinator.finish(session, turnLease, terminalInterrupt ? 'cancelled' : 'failed', streamResult.protocolIncompleteReason || streamResult.terminalReason || streamResult.subtype);
4500
+ await this.turnCoordinator.finish(session, turnLease, terminalInterrupt ? 'cancelled' : 'failed', permissionTerminalFailure ? 'permission_denied'
4501
+ : streamResult.protocolIncompleteReason || streamResult.terminalReason || streamResult.subtype);
3926
4502
  }
3927
4503
  // 注意:不在此处清除 interruptedSessions,由下一条消息的 prompt 包装逻辑消费
3928
4504
  const interruptReason = this.interruptedSessions.get(session.id);
@@ -3956,7 +4532,7 @@ export class ResponseEngine {
3956
4532
  // DaemonChannel settles a trigger when it receives a terminal
3957
4533
  // payload. Send the metadata-bearing status first so failed runs do
3958
4534
  // not lose their tool counts and recovery diagnostics.
3959
- if (message.source === 'trigger') {
4535
+ if (this.isTrustedDaemonTrigger(message)) {
3960
4536
  await adapter.send(envelope, { kind: 'status.error', metadata: terminalMetadata }).catch(() => { });
3961
4537
  await adapter.send(envelope, { kind: 'result.error', text: userErrorSummary, reason: rawSubtype }).catch(() => { });
3962
4538
  }
@@ -3997,6 +4573,10 @@ export class ResponseEngine {
3997
4573
  this.eventBus.publish({
3998
4574
  type: 'task:error',
3999
4575
  sessionId: session.id,
4576
+ agentAid: runnerSelfAid,
4577
+ peerId: configActorId,
4578
+ encrypted: message.encrypted ?? message.replyContext?.metadata?.encrypted,
4579
+ originTriggerId: message.triggerMeta?.triggerId,
4000
4580
  taskId,
4001
4581
  generation: turnLease?.generation,
4002
4582
  error: errorSummary,
@@ -4275,7 +4855,8 @@ export class ResponseEngine {
4275
4855
  await timeoutControl?.barrier;
4276
4856
  }
4277
4857
  const handlingTimeout = error instanceof Error
4278
- && (error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT');
4858
+ && (isExecutionFailure(error) || error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT');
4859
+ const ownsTimeoutTerminal = !handlingTimeout || !timeoutControl || timeoutControl.claimTerminal();
4279
4860
  if (handlingTimeout) {
4280
4861
  this.timeoutErrors.delete(session.id);
4281
4862
  }
@@ -4297,11 +4878,12 @@ export class ResponseEngine {
4297
4878
  const errType = classifyError(error);
4298
4879
  const interruptReason = this.interruptedSessions.get(session.id);
4299
4880
  const isTotalExecutionTimeout = error instanceof Error && error.message === 'TOTAL_EXECUTION_TIMEOUT';
4300
- const isTimeout = error instanceof Error && error.message === 'SDK_TIMEOUT';
4881
+ const isTimeout = error instanceof Error && (error.message === 'SDK_TIMEOUT' || error instanceof ExecutionTimeoutError);
4301
4882
  const canPublishTerminal = !turnLease || this.turnCoordinator.canPublish(turnLease);
4302
4883
  const staleSupersededTurn = !!turnLease
4303
4884
  && !canPublishTerminal
4304
4885
  && interruptReason !== 'timeout'
4886
+ && interruptReason !== 'execution_error'
4305
4887
  && !isTimeout
4306
4888
  && !isTotalExecutionTimeout;
4307
4889
  const isUserInterrupt = isNonErrorInterruptReason(interruptReason) || staleSupersededTurn;
@@ -4309,7 +4891,7 @@ export class ResponseEngine {
4309
4891
  if (isUserInterrupt) {
4310
4892
  await this.publishTriggerExecutionInterrupted(message, adapter, envelope, interruptReason ?? 'stale_turn', taskCausation);
4311
4893
  }
4312
- else {
4894
+ else if (ownsTimeoutTerminal) {
4313
4895
  this.publishTriggerExecutionFailure(message, errorMsg, {
4314
4896
  messageId,
4315
4897
  causation: taskCausation,
@@ -4318,24 +4900,27 @@ export class ResponseEngine {
4318
4900
  if (leaseWasCurrent && turnLease) {
4319
4901
  await this.turnCoordinator.finish(session, turnLease, 'failed', error instanceof Error ? error.message : String(error));
4320
4902
  }
4321
- const totalExecutionMs = this.totalExecutionLimitMs();
4903
+ const totalExecutionMs = error instanceof ExecutionTimeoutError ? error.timeoutMs : this.totalExecutionLimitMs();
4322
4904
  const procStatus = errType === ErrorType.SDK_TIMEOUT || isTotalExecutionTimeout ? 'timeout'
4323
4905
  : errType === ErrorType.STREAM_ERROR ? 'interrupted'
4324
4906
  : 'error';
4325
4907
  const daemonTrigger = this.isTrustedDaemonTrigger(message);
4326
- const statusPayload = procStatus === 'timeout'
4327
- ? { kind: 'status.timeout', metadata: isTotalExecutionTimeout
4328
- ? (totalExecutionMs === undefined ? {} : { totalExecutionMs })
4329
- : { idleSec: getLastIdleSec?.() || undefined } }
4330
- : procStatus === 'interrupted'
4331
- ? { kind: 'status.interrupted', metadata: { reason: 'stream_error' } }
4332
- : { kind: 'status.error' };
4908
+ const statusPayload = error instanceof OrphanedTargetSessionError
4909
+ ? { kind: 'status.error', metadata: { errorType: error.reason, message: '任务执行状态丢失,已结束本次执行。' } }
4910
+ : procStatus === 'timeout'
4911
+ ? { kind: 'status.timeout', metadata: isTotalExecutionTimeout
4912
+ ? { ...(totalExecutionMs === undefined ? {} : { totalExecutionMs }),
4913
+ ...(error instanceof ExecutionTimeoutError ? { reason: error.reason } : {}) }
4914
+ : { idleSec: getLastIdleSec?.() || undefined } }
4915
+ : procStatus === 'interrupted'
4916
+ ? { kind: 'status.interrupted', metadata: { reason: 'stream_error' } }
4917
+ : { kind: 'status.error' };
4333
4918
  // 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送中断/错误提示
4334
- if (!isUserInterrupt && !daemonTrigger) {
4919
+ if (!isUserInterrupt && ownsTimeoutTerminal && !daemonTrigger) {
4335
4920
  await adapter.send(envelope, statusPayload).catch(() => { });
4336
4921
  this.touchAgentActivity(channelKey);
4337
4922
  }
4338
- if (!isUserInterrupt && daemonTrigger)
4923
+ if (!isUserInterrupt && ownsTimeoutTerminal && daemonTrigger)
4339
4924
  this.touchAgentActivity(channelKey);
4340
4925
  // 用户主动中断时降级日志;其余仍按 error 记录
4341
4926
  if (isUserInterrupt) {
@@ -4349,13 +4934,18 @@ export class ResponseEngine {
4349
4934
  // (message-queue / slash-handler)已发过 task:interrupted(它填充了
4350
4935
  // interruptedSessions,isUserInterrupt 才会为真),stats 侧已据此收尾任务。
4351
4936
  // 此处不再发任何事件——发 task:error 会误归类,重发 task:interrupted 会重复记账。
4352
- if (!isUserInterrupt) {
4937
+ if (!isUserInterrupt && ownsTimeoutTerminal) {
4353
4938
  this.eventBus.publish({
4354
4939
  type: 'task:error',
4355
4940
  sessionId: session.id,
4941
+ agentAid: runnerSelfAid,
4942
+ peerId: configActorId,
4943
+ encrypted: message.encrypted ?? message.replyContext?.metadata?.encrypted,
4944
+ originTriggerId: message.triggerMeta?.triggerId,
4356
4945
  taskId,
4357
4946
  generation: turnLease?.generation,
4358
4947
  error: errorMsg,
4948
+ terminalReason: isExecutionFailure(error) ? error.reason : undefined,
4359
4949
  errorType,
4360
4950
  agentName: agentNameForStats,
4361
4951
  causation: taskCausation,
@@ -4363,14 +4953,19 @@ export class ResponseEngine {
4363
4953
  recordCausationSpan(taskCausation, 'task.run', { status: 'failed', refs: { taskId, sessionId: session.id, messageId }, reason: errorType });
4364
4954
  }
4365
4955
  // 记录处理失败
4366
- logger.message({
4367
- msgId: messageId,
4368
- sessionId: session.id,
4369
- dir: 'inbound',
4370
- status: isUserInterrupt ? 'interrupted' : 'failed',
4371
- error: error instanceof Error ? error.message : String(error)
4372
- });
4373
- if (error instanceof Error && !isUserInterrupt) {
4956
+ if (ownsTimeoutTerminal) {
4957
+ logger.message({
4958
+ msgId: messageId,
4959
+ sessionId: session.id,
4960
+ dir: 'inbound',
4961
+ status: isUserInterrupt ? 'interrupted' : 'failed',
4962
+ error: error instanceof Error ? error.message : String(error)
4963
+ });
4964
+ }
4965
+ else {
4966
+ logger.info(`[ResponseEngine] Duplicate timeout terminal suppressed: session=${session.id} task=${taskId}`);
4967
+ }
4968
+ if (error instanceof Error && !isUserInterrupt && ownsTimeoutTerminal) {
4374
4969
  logger.error(`[${message.channel}] Error stack:`, error.stack);
4375
4970
  }
4376
4971
  // 发送用户友好的错误消息
@@ -4380,7 +4975,11 @@ export class ResponseEngine {
4380
4975
  const retryExhaustedCount = getRetryExhaustedCount(error);
4381
4976
  const modelFallbackExhaustedMessage = getModelFallbackExhaustedMessage(error);
4382
4977
  const retryInputAlreadySubmitted = wasRetryInputAlreadySubmitted(error);
4383
- if (isUserInterrupt) {
4978
+ if (!ownsTimeoutTerminal) {
4979
+ // The monitor path already owns delivery for this timeout. Cleanup and
4980
+ // durable turn finalization above still run, but no terminal is emitted twice.
4981
+ }
4982
+ else if (isUserInterrupt) {
4384
4983
  logger.info(`[ResponseEngine] User interrupt by new_message, skip sending error message`);
4385
4984
  }
4386
4985
  else if (error?._errorAlreadySent && !daemonTrigger && !retryExhaustedCount && !isTimeout && !isTotalExecutionTimeout) {
@@ -4389,19 +4988,21 @@ export class ResponseEngine {
4389
4988
  else {
4390
4989
  // SDK_TIMEOUT:status.timeout 已发结构化状态,此处再补一条用户可见的错误文本(result.error)
4391
4990
  const idleSec = getLastIdleSec?.() || 0;
4392
- const userMessage = retryExhaustedCount
4393
- ? formatRetryableErrorFinalMessage(error, retryExhaustedCount)
4394
- : retryInputAlreadySubmitted
4395
- ? formatRetryInputAlreadySubmittedMessage(error)
4396
- : modelFallbackExhaustedMessage
4397
- ? modelFallbackExhaustedMessage
4398
- : isTotalExecutionTimeout
4399
- ? (totalExecutionMs === undefined
4400
- ? '⚠️ 任务超过总执行时限,已自动中断'
4401
- : `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`)
4402
- : isTimeout
4403
- ? (idleSec > 0 ? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断` : '⚠️ 任务超时,已自动中断')
4404
- : getErrorMessage(error, undefined);
4991
+ const userMessage = error instanceof OrphanedTargetSessionError
4992
+ ? '⚠️ 任务执行状态丢失,已结束本次执行。'
4993
+ : retryExhaustedCount
4994
+ ? formatRetryableErrorFinalMessage(error, retryExhaustedCount)
4995
+ : retryInputAlreadySubmitted
4996
+ ? formatRetryInputAlreadySubmittedMessage(error)
4997
+ : modelFallbackExhaustedMessage
4998
+ ? modelFallbackExhaustedMessage
4999
+ : isTotalExecutionTimeout
5000
+ ? (totalExecutionMs === undefined
5001
+ ? '⚠️ 任务超过总执行时限,已自动中断'
5002
+ : `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`)
5003
+ : isTimeout
5004
+ ? (idleSec > 0 ? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断` : '⚠️ 任务超时,已自动中断')
5005
+ : getErrorMessage(error, undefined);
4405
5006
  // 获取 session 用于话题回复(如果 resolveSession 已执行)
4406
5007
  let sendOpts;
4407
5008
  try {
@@ -4418,7 +5019,7 @@ export class ResponseEngine {
4418
5019
  const errorPayload = {
4419
5020
  kind: 'result.error',
4420
5021
  text: userMessage,
4421
- reason: retryExhaustedCount ? 'retry_exhausted' : retryInputAlreadySubmitted ? 'input_already_submitted' : modelFallbackExhaustedMessage ? 'model_unavailable' : isTotalExecutionTimeout ? 'total_execution_timeout' : isTimeout ? 'timeout' : errType,
5022
+ reason: isExecutionFailure(error) ? error.reason : retryExhaustedCount ? 'retry_exhausted' : retryInputAlreadySubmitted ? 'input_already_submitted' : modelFallbackExhaustedMessage ? 'model_unavailable' : isTotalExecutionTimeout ? 'total_execution_timeout' : isTimeout ? 'timeout' : errType,
4422
5023
  };
4423
5024
  if (!isUserInterrupt && daemonTrigger && procStatus === 'error') {
4424
5025
  // DaemonChannel settles a trigger on the first terminal payload. Put
@@ -4452,16 +5053,15 @@ export class ResponseEngine {
4452
5053
  kind: 'activity.batch',
4453
5054
  items: [{
4454
5055
  kind: 'notice',
4455
- text: userMessage,
5056
+ // activity.batch 载荷混合正常内容与错误,顶层出站包裹不整体打标;
5057
+ // 这里的 notice 是 proactive 下用户能看到的错误副本,单独打标。
5058
+ text: tagErrorTextWithEvolcoreVersion(userMessage),
4456
5059
  severity: 'warn',
4457
5060
  subtype: 'task-error',
4458
5061
  }],
4459
5062
  }).catch(() => { });
4460
5063
  }
4461
5064
  }
4462
- if (handlingTimeout && timeoutControl) {
4463
- timeoutControl.terminalDelivered = true;
4464
- }
4465
5065
  }
4466
5066
  finally {
4467
5067
  if (fullAccessAuthorization) {
@@ -4472,7 +5072,8 @@ export class ResponseEngine {
4472
5072
  auditFullAccessEvent({
4473
5073
  event: fullAccessAuthorization.source === 'trigger'
4474
5074
  ? 'fullaccess.trigger.execution.ended'
4475
- : 'fullaccess.execution.ended',
5075
+ : fullAccessAuthorization.source === 'agent-policy'
5076
+ ? 'fullaccess.agent-policy.execution.ended' : 'fullaccess.execution.ended',
4476
5077
  source: fullAccessAuthorization.source,
4477
5078
  actorId: fullAccessAuthorization.authorizedBy,
4478
5079
  processRole: fullAccessAuditProcessRole,
@@ -4567,12 +5168,12 @@ export class ResponseEngine {
4567
5168
  return undefined;
4568
5169
  const shouldClose = !writerDb;
4569
5170
  try {
4570
- const row = db.prepare(`SELECT model, input_tokens, cache_creation_tokens, cache_read_tokens,
4571
- context_tokens, max_tokens, auto_compact_tokens
4572
- FROM model_calls
4573
- WHERE session_id = ?
4574
- AND agent_session_id = ?
4575
- ORDER BY ts DESC, call_index DESC
5171
+ const row = db.prepare(`SELECT model, input_tokens, cache_creation_tokens, cache_read_tokens,
5172
+ context_tokens
5173
+ FROM model_calls
5174
+ WHERE session_id = ?
5175
+ AND agent_session_id = ?
5176
+ ORDER BY ts DESC, call_index DESC
4576
5177
  LIMIT 1`).get(sessionId, agentSessionId);
4577
5178
  if (!row)
4578
5179
  return undefined;
@@ -4590,16 +5191,11 @@ export class ResponseEngine {
4590
5191
  }
4591
5192
  if (totalTokens <= 0)
4592
5193
  return undefined;
4593
- const recordedAutoCompactTokens = row.auto_compact_tokens ?? undefined;
4594
- const inferredAutoCompactTokens = autoCompactTokensFromMaxTokens(row.max_tokens ?? undefined);
4595
- const candidateAutoCompactTokens = recordedAutoCompactTokens && recordedAutoCompactTokens > 0
4596
- ? recordedAutoCompactTokens
4597
- : inferredAutoCompactTokens ?? autoCompactWindowForModel(model);
4598
- // Historic rows may contain the old fixed 350k threshold. Never let a
4599
- // persisted value exceed the context window that accompanied it.
4600
- const autoCompactTokens = inferredAutoCompactTokens
4601
- ? Math.min(candidateAutoCompactTokens, inferredAutoCompactTokens)
4602
- : candidateAutoCompactTokens;
5194
+ // EvolCore 的自动压缩阈值固定为 650k,与模型上下文窗口无关:
5195
+ // 大窗口模型(1M)到 650k 由 EvolCore 压缩;小窗口模型(200k/128k)
5196
+ // 到不了该阈值,交给 base agent 自身的压缩机制处理。
5197
+ // 历史行里可能存着旧的 350k 阈值,这里一律忽略,只认当前常量。
5198
+ const autoCompactTokens = autoCompactWindowForModel(model);
4603
5199
  return { totalTokens, autoCompactTokens };
4604
5200
  }
4605
5201
  finally {
@@ -4675,7 +5271,7 @@ export class ResponseEngine {
4675
5271
  logger.warn(`[ResponseEngine] Bound session ${boundSessionId} not found, falling back to latest`);
4676
5272
  }
4677
5273
  }
4678
- 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);
5274
+ 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, message.lable);
4679
5275
  this.ensureSessionBaseagent(session, resolvedBaseagent);
4680
5276
  // 群名解析:群会话首次取群显示名(group.get),缓存到 metadata,供信封渲染。
4681
5277
  // 渠道私有方法 getGroupName 自带进程缓存 + 容错;取不到不阻塞(groupName 保持空,模板回退 groupId)。
@@ -4733,7 +5329,7 @@ export class ResponseEngine {
4733
5329
  */
4734
5330
  async processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress, proactive,
4735
5331
  /** [迁移点4/5] 响应模式插件 + 状态,用于调 onToolUse/onComplete 钩子 */
4736
- modeHooks, causation, turnLease, permissionMode, proactiveSelfAid) {
5332
+ modeHooks, causation, turnLease, permissionMode, proactiveSelfAid, onAsyncQuestion) {
4737
5333
  // Per-session agent name for stats bucketing
4738
5334
  const statsChannelKey = session.channel === 'daemon' ? session.channel : (session.metadata?.channelKey || session.channel);
4739
5335
  const agentNameForStats = this.agentRegistry?.resolveByChannel(statsChannelKey)?.name ?? '<unknown>';
@@ -4820,6 +5416,15 @@ export class ResponseEngine {
4820
5416
  // It must never be projected as progress, activity, text, or transcript.
4821
5417
  if (event.type === 'heartbeat')
4822
5418
  continue;
5419
+ if (event.type === 'user_question') {
5420
+ if (!onAsyncQuestion)
5421
+ throw new Error('Async question has no delivery context');
5422
+ await onAsyncQuestion(event.request);
5423
+ madeProgress = true;
5424
+ // A structured question is user-visible progress without being a text reply.
5425
+ textCount++;
5426
+ continue;
5427
+ }
4823
5428
  if (event.type === 'input_accepted') {
4824
5429
  inputAccepted = !turnLease || event.inputId === turnLease.inputId;
4825
5430
  logger.debug(`[ResponseEngine] Input accepted: session=${session.id} input=${event.inputId}`);
@@ -4915,7 +5520,7 @@ export class ResponseEngine {
4915
5520
  || (typeof input?.prompt === 'string' ? input.prompt.slice(0, 80) : '')
4916
5521
  || (typeof input?.query === 'string' ? input.query.slice(0, 80) : '')
4917
5522
  || '';
4918
- eventDetail = ` tool=${event.name}${desc ? ` desc="${desc}"` : ''}`;
5523
+ eventDetail = ` tool=${event.name}${desc ? ` desc="${String(desc).replace(/\s+/g, ' ')}"` : ''}`;
4919
5524
  }
4920
5525
  else if (event.type === 'tool_result') {
4921
5526
  const decision = event.decision ?? (event.isError ? 'error' : 'allow');
@@ -4935,6 +5540,10 @@ export class ResponseEngine {
4935
5540
  }
4936
5541
  // IMRenderer 旁路:proactive 模式逐事件投影为 thought(fire-and-forget)
4937
5542
  renderer.emit(event);
5543
+ if (event.type === 'text_start') {
5544
+ await renderer.sendTextStart(event.contentIndex, event.turn);
5545
+ continue;
5546
+ }
4938
5547
  // session_id 已在 AgentRunner.transformStream 中处理,此处仅记录
4939
5548
  if (event.type === 'session_id') {
4940
5549
  logger.debug(`[ResponseEngine] Session ID updated: ${event.sessionId} for session: ${session.id}`);
@@ -5096,7 +5705,7 @@ export class ResponseEngine {
5096
5705
  this.eventBus.publish({ type: 'message:text', sessionId: session.id, text: visibleText, isFinal: false });
5097
5706
  }
5098
5707
  if (visibleText && !shouldSuppress()) {
5099
- renderer.addText(visibleText, event.outputTokens, event.turn);
5708
+ await renderer.addText(visibleText, event.outputTokens, event.turn, event.assistantUuid);
5100
5709
  hasProjectedCurrentReplyText = true;
5101
5710
  }
5102
5711
  }
@@ -5156,7 +5765,7 @@ export class ResponseEngine {
5156
5765
  timestamp: Date.now(),
5157
5766
  causation,
5158
5767
  });
5159
- const desc = summarizeToolInput(event.name, event.input || {});
5768
+ const desc = summarizeToolInputForDisplay(event.name, event.input || {});
5160
5769
  const pendingInput = { name: event.name, input: event.input || {} };
5161
5770
  if (event.callId)
5162
5771
  toolInputByCallId.set(event.callId, pendingInput);
@@ -5332,7 +5941,7 @@ export class ResponseEngine {
5332
5941
  let errorMsg = event.error || (typeof event.result === 'string' ? event.result : JSON.stringify(event.result)) || '\u6267\u884c\u5931\u8d25';
5333
5942
  // 移除 XML 风格的错误标签
5334
5943
  errorMsg = errorMsg.replace(/<tool_use_error>(.*?)<\/tool_use_error>/gs, '$1');
5335
- renderer.addToolResult(event.name || '\u5de5\u5177', false, undefined, errorMsg, event.callId, event.durationMs, cachedDesc);
5944
+ renderer.addToolResult(event.name || '\u5de5\u5177', false, undefined, errorMsg, event.callId, event.durationMs, cachedDesc, event);
5336
5945
  }
5337
5946
  else if (!event.isError && !shouldSuppress()) {
5338
5947
  renderer.addToolResult(event.name || '\u5de5\u5177', true, event.result, undefined, event.callId, event.durationMs, cachedDesc);
@@ -5433,7 +6042,7 @@ export class ResponseEngine {
5433
6042
  // User interrupt (AbortError) is expected, log at info level
5434
6043
  const catchInterruptReason = this.interruptedSessions.get(session.id);
5435
6044
  const catchIsUserInterrupt = isNonErrorInterruptReason(catchInterruptReason);
5436
- if (catchInterruptReason === 'timeout') {
6045
+ if (catchInterruptReason === 'timeout' || catchInterruptReason === 'execution_error') {
5437
6046
  logger.info('[ResponseEngine] Stream closed by timeout barrier');
5438
6047
  throw this.timeoutErrors.get(session.id) ?? new Error('SDK_TIMEOUT');
5439
6048
  }
@@ -5471,7 +6080,9 @@ export class ResponseEngine {
5471
6080
  if (hasErrorSuppressingContent) {
5472
6081
  let errorOutputFlushed = false;
5473
6082
  try {
5474
- await renderer.flush(true);
6083
+ // The outer retry/terminal handler has not decided the outcome yet.
6084
+ // Pending stream output must not announce successful completion.
6085
+ await renderer.flush(false);
5475
6086
  errorOutputFlushed = true;
5476
6087
  }
5477
6088
  catch (flushError) {