evolcore 0.0.35 → 0.0.36

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 (224) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +6 -0
  3. package/bin/install-codex-managed-hooks.mjs +20 -3
  4. package/dist/agents/baseagent-availability.js +1 -1
  5. package/dist/agents/baseagent.js +88 -0
  6. package/dist/agents/claude-runner.js +59 -12
  7. package/dist/agents/codex/app-server-client.js +18 -1
  8. package/dist/agents/codex-runner.js +262 -21
  9. package/dist/agents/ecagent-runner.js +47 -11
  10. package/dist/agents/gemini-runner.js +36 -12
  11. package/dist/aun/aid/client.js +4 -3
  12. package/dist/aun/aid/control-aid.js +270 -8
  13. package/dist/aun/aid/identity.js +6 -3
  14. package/dist/aun/aid/ownership-operation.js +58 -0
  15. package/dist/aun/aid/store.js +1 -0
  16. package/dist/aun/group/fs-download.js +67 -2
  17. package/dist/aun/group/history.js +20 -5
  18. package/dist/aun/group/operations.js +8 -2
  19. package/dist/aun/menu/client.js +39 -2
  20. package/dist/aun/menu/transport.js +1 -1
  21. package/dist/aun/msg/history.js +43 -4
  22. package/dist/aun/msg/managed-operation.js +30 -53
  23. package/dist/aun/msg/p2p.js +51 -15
  24. package/dist/aun/outbox.js +4 -0
  25. package/dist/channels/aun.js +260 -73
  26. package/dist/channels/daemon.js +6 -4
  27. package/dist/channels/feishu.js +3 -0
  28. package/dist/cli/agent-command.js +161 -223
  29. package/dist/cli/agent-create.js +190 -223
  30. package/dist/cli/agent.js +3 -1
  31. package/dist/cli/aid-bind-command.js +53 -0
  32. package/dist/cli/aun-commands.js +384 -91
  33. package/dist/cli/bench.js +1 -1
  34. package/dist/cli/cli-argv.js +74 -0
  35. package/dist/cli/daemon-commands.js +105 -74
  36. package/dist/cli/diag.js +1 -1
  37. package/dist/cli/fs-command.js +18 -19
  38. package/dist/cli/getmeta-command.js +159 -6
  39. package/dist/cli/index.js +6 -12
  40. package/dist/cli/init-channel.js +8 -55
  41. package/dist/cli/init.js +125 -218
  42. package/dist/cli/queue-command.js +11 -8
  43. package/dist/cli/restart-monitor.js +134 -48
  44. package/dist/cli/session-command.js +1 -1
  45. package/dist/cli/stats.js +7 -5
  46. package/dist/cli/thread-command.js +103 -30
  47. package/dist/cli/trigger-command.js +1 -1
  48. package/dist/cli/watch-msg-diag.js +1 -0
  49. package/dist/config/access-policy.js +2 -2
  50. package/dist/config/boot-log.js +14 -11
  51. package/dist/config/config-field-policy.js +9 -1
  52. package/dist/config/config-manager.js +176 -90
  53. package/dist/config/config-operation-service.js +7 -14
  54. package/dist/config/contact-book-store.js +142 -26
  55. package/dist/config/contact-book.js +4 -2
  56. package/dist/config/lifecycle.js +1 -1
  57. package/dist/config/owner-config.js +96 -0
  58. package/dist/config/peer-role-resolver.js +12 -18
  59. package/dist/config/providers-config.js +382 -0
  60. package/dist/config/providers-import.js +74 -0
  61. package/dist/config/providers-management.js +328 -0
  62. package/dist/config/providers.js +22 -0
  63. package/dist/config/resolved-config-op.js +17 -14
  64. package/dist/config/snapshot.js +28 -16
  65. package/dist/config-store.js +18 -14
  66. package/dist/core/agent-ipc-registration.js +21 -63
  67. package/dist/core/agent-runtime.js +16 -5
  68. package/dist/core/auth/agent-delegation.js +199 -25
  69. package/dist/core/auth/auth-gateway.js +44 -2
  70. package/dist/core/auth/authorization-denial.js +1 -1
  71. package/dist/core/auth/debug-context.js +2 -1
  72. package/dist/core/auth/delegation-authorization.js +2 -2
  73. package/dist/core/auth/delegation-diagnostics.js +4 -1
  74. package/dist/core/auth/operation-authorizer.js +10 -1
  75. package/dist/core/auth/operation-catalog.js +19 -1
  76. package/dist/core/bootstrap-service.js +43 -20
  77. package/dist/core/channel-loader.js +0 -1
  78. package/dist/core/channel-runtime.js +30 -4
  79. package/dist/core/command/agent-control.js +196 -37
  80. package/dist/core/command/cli-intent-parser.js +60 -4
  81. package/dist/core/command/command-handler.js +212 -60
  82. package/dist/core/command/connect-menu.js +1 -0
  83. package/dist/core/command/diag-service.js +5 -4
  84. package/dist/core/command/file-search.js +368 -0
  85. package/dist/core/command/menu-catalog.js +2 -1
  86. package/dist/core/command/menu-handler.js +292 -107
  87. package/dist/core/command/menu-protocol.js +23 -0
  88. package/dist/core/command/role-menu.js +7 -19
  89. package/dist/core/command/skill-slash.js +130 -25
  90. package/dist/core/command/slash-handler.js +101 -53
  91. package/dist/core/command/topic-slash.js +16 -5
  92. package/dist/core/daemon-bootstrap.js +7 -1
  93. package/dist/core/daemon-control.js +19 -1
  94. package/dist/core/daemon-lifecycle.js +2 -1
  95. package/dist/core/event-catalog.js +2 -0
  96. package/dist/core/evolagent-registry.js +13 -2
  97. package/dist/core/evolagent.js +3 -6
  98. package/dist/core/handoff/ipc-registration.js +2 -2
  99. package/dist/core/handoff/runtime.js +40 -6
  100. package/dist/core/handoff/store.js +1 -0
  101. package/dist/core/inference/text-inference.js +95 -6
  102. package/dist/core/ipc-registration.js +43 -24
  103. package/dist/core/memory/consolidation-context.js +4 -1
  104. package/dist/core/memory/consolidation.js +25 -14
  105. package/dist/core/message/aun-ipc-sender.js +296 -88
  106. package/dist/core/message/aun-request-registry.js +226 -0
  107. package/dist/core/message/create-status.js +33 -4
  108. package/dist/core/message/message-bridge.js +62 -13
  109. package/dist/core/message/message-log.js +0 -1
  110. package/dist/core/message/message-queue.js +4 -1
  111. package/dist/core/message/message-utils.js +8 -1
  112. package/dist/core/message/peer-mode.js +17 -9
  113. package/dist/core/message/response-engine.js +266 -124
  114. package/dist/core/message/retry-scheduler.js +11 -8
  115. package/dist/core/model/catalog-service.js +60 -18
  116. package/dist/core/model/model-catalog.js +112 -9
  117. package/dist/core/model/model-fallback.js +94 -26
  118. package/dist/core/ownership/owner-resolver.js +687 -0
  119. package/dist/core/permission/approval-gateway.js +196 -62
  120. package/dist/core/permission/ec-command-parser.js +138 -25
  121. package/dist/core/permission/filesystem-approval-scope.js +141 -0
  122. package/dist/core/permission/protected-paths.js +14 -1
  123. package/dist/core/permission/sandbox-runtime.js +171 -18
  124. package/dist/core/permission/tool-policy.js +13 -2
  125. package/dist/core/role/runtime-policy.js +3 -4
  126. package/dist/core/session/session-idle-monitor.js +10 -8
  127. package/dist/core/session/session-manager.js +72 -45
  128. package/dist/core/session/session-mapper.js +18 -7
  129. package/dist/core/session/session-renew.js +5 -4
  130. package/dist/core/session/session-title.js +2 -1
  131. package/dist/core/session/session-turn-coordinator.js +4 -2
  132. package/dist/core/skill/index-generator.js +2 -16
  133. package/dist/core/task/task-recovery-service.js +15 -2
  134. package/dist/core/thread/command-parser.js +28 -13
  135. package/dist/core/thread/index.js +1 -0
  136. package/dist/core/thread/ipc-operation.js +11 -8
  137. package/dist/core/thread/thread-operation.js +79 -44
  138. package/dist/core/thread/thread-route.js +54 -6
  139. package/dist/core/thread/thread-service.js +236 -45
  140. package/dist/core/thread/topic-id.js +248 -0
  141. package/dist/eck/kit-renderer.js +1 -1
  142. package/dist/eck/message-renderer.js +12 -2
  143. package/dist/index.js +50 -27
  144. package/dist/ipc.js +59 -6
  145. package/dist/paths.js +1 -0
  146. package/dist/product.js +1 -0
  147. package/dist/response-system/engines/v1/proactive-flow.js +10 -3
  148. package/dist/stats/db.js +8 -0
  149. package/dist/stats/writer.js +26 -4
  150. package/dist/trigger/authorization.js +48 -3
  151. package/dist/trigger/feedback.js +28 -10
  152. package/dist/trigger/ipc-service.js +86 -10
  153. package/dist/trigger/local-files.js +55 -23
  154. package/dist/trigger/scheduler.js +57 -11
  155. package/dist/trigger/system-definitions.js +1 -0
  156. package/dist/trigger/validation.js +44 -15
  157. package/dist/utils/aid-bind.js +17 -297
  158. package/dist/utils/avatar-upload.js +16 -4
  159. package/dist/utils/cross-platform.js +109 -34
  160. package/dist/utils/ecweb-supervisor.js +1 -1
  161. package/dist/utils/ecweb-utils.js +8 -1
  162. package/dist/utils/error-utils.js +44 -6
  163. package/dist/utils/instance-registry.js +12 -2
  164. package/dist/utils/npm-ops.js +102 -27
  165. package/dist/utils/package-version-info.js +72 -27
  166. package/dist/utils/restart-safety.js +50 -0
  167. package/dist/utils/upgrade-jobs.js +304 -0
  168. package/dist/utils/windows-autostart.js +17 -10
  169. package/kits/docs/INDEX.md +5 -0
  170. package/kits/docs/aun/ABILITY_CALLING.md +239 -0
  171. package/kits/docs/evolcore/INDEX.md +14 -3
  172. package/kits/docs/evolcore/agent-create.md +55 -141
  173. package/kits/docs/evolcore/agent.md +38 -16
  174. package/kits/docs/evolcore/aid.md +34 -0
  175. package/kits/docs/evolcore/config.md +20 -1
  176. package/kits/docs/evolcore/event.md +2 -2
  177. package/kits/docs/evolcore/fs.md +18 -2
  178. package/kits/docs/evolcore/group-collaboration.md +13 -246
  179. package/kits/docs/evolcore/group.md +2 -1
  180. package/kits/docs/evolcore/memory-consolidation-batches.md +1 -1
  181. package/kits/docs/evolcore/menu-protocol.md +65 -1
  182. package/kits/docs/evolcore/msg.md +64 -2
  183. package/kits/docs/evolcore/skill.md +86 -0
  184. package/kits/docs/evolcore/stats.md +6 -0
  185. package/kits/docs/evolcore/task.md +2 -0
  186. package/kits/docs/evolcore/thread-chain.md +145 -51
  187. package/kits/docs/evolcore/trigger.md +48 -3
  188. package/kits/eck_manifest.json +21 -0
  189. package/kits/eck_message_manifest.json +4 -4
  190. package/kits/migrations/migrate-role-config-v5.mjs +20 -7
  191. package/kits/rules/01-overview.md +2 -1
  192. package/kits/rules/03-identity.md +3 -1
  193. package/kits/rules/04-relation.md +1 -1
  194. package/kits/rules/07-memory-and-thread.md +1 -1
  195. package/kits/schemas/_meta.json +16 -1
  196. package/kits/schemas/agent-config.schema.13.json +44 -2
  197. package/kits/schemas/contact-book.schema.3.json +5 -0
  198. package/kits/schemas/daemon.schema.6.json +3 -1
  199. package/kits/schemas/daemon.schema.8.json +329 -0
  200. package/kits/schemas/defaults.schema.5.json +39 -3
  201. package/kits/schemas/providers.schema.1.json +76 -0
  202. package/kits/schemas/relation-config.schema.9.json +5 -1
  203. package/kits/skills/.switches.json +28 -0
  204. package/kits/skills/aid-ability/SKILL.md +214 -0
  205. package/kits/skills/ec-agent-create/SKILL.md +162 -0
  206. package/kits/skills/ec-group/SKILL.md +2 -1
  207. package/kits/skills/ec-group-collaboration/SKILL.md +228 -0
  208. package/kits/skills/ec-identity/SKILL.md +143 -0
  209. package/kits/skills/ec-skill/scripts/cli.mjs +36 -3
  210. package/kits/skills/ec-skill/scripts/skill.mjs +202 -15
  211. package/kits/skills/ec-trigger/SKILL.md +167 -0
  212. package/kits/skills/ec-trigger/references/examples.md +153 -0
  213. package/kits/skills/thread-ops/SKILL.md +108 -420
  214. package/kits/templates/message-fragments/thread-context.md +1 -2
  215. package/kits/templates/message-fragments/trigger-thread-check.md +1 -1
  216. package/kits/templates/roles/member.json +1 -1
  217. package/kits/templates/roles/visitor.json +1 -0
  218. package/kits/templates/system-fragments/bootstrap.md +1 -0
  219. package/kits/templates/system-fragments/channel.md +2 -1
  220. package/kits/templates/system-fragments/commands.md +5 -4
  221. package/kits/templates/system-fragments/identity.md +2 -0
  222. package/kits/templates/system-fragments/session.md +1 -1
  223. package/kits/templates/system-fragments/venue.md +1 -1
  224. package/package.json +4 -3
@@ -1,4 +1,5 @@
1
1
  import { initializationBlocksTasks } from '../../cli/agent-create.js';
2
+ import { assertOwnershipAuthorization, getDaemonOwners, refreshOwnershipForAuthorization, OWNERSHIP_CACHE_MAX_AGE_MS } from '../ownership/owner-resolver.js';
2
3
  import path from 'path';
3
4
  import fs from 'fs';
4
5
  import os from 'os';
@@ -24,7 +25,8 @@ import { ExecutionTimeoutError, OrphanedTargetSessionError, isExecutionFailure,
24
25
  import { readEvolcoreRuntimeFlavor, tagErrorTextWithEvolcoreVersion } from '../../utils/evolcore-version.js';
25
26
  import { describeProactiveSendCommandFailure, isExactEvolcoreSendCommandForSession, isThreadRoutedEvolcoreSendCommand, parseCodexToolCommand, resolveCodexShellCarrierArgv, resolveCodexShellCarrierString, } from '../permission/ec-command-parser.js';
26
27
  import { resolveThreadRouteTarget } from '../thread/thread-route.js';
27
- import { summarizeToolInputForDisplay } from '../../utils/tool-summary.js';
28
+ import { isMainSession, isTopicSession, isTopicThreadId, sessionLocalTopicId, sessionTopicThreadId } from '../thread/topic-id.js';
29
+ import { summarizeToolInputForDisplay, toolInputForDisplay } from '../../utils/tool-summary.js';
28
30
  import { agentDataDir, agentDir as resolveAgentDir, agentMdPath, getPackageRoot, resolveRoot, resolvePaths, debugSessionsDir } from '../../paths.js';
29
31
  import { renderKitSections, resolveSessionManifest, DEFAULT_MANIFEST_FILE } from '../../eck/kit-renderer.js';
30
32
  import { renderMessageBody } from '../../eck/message-renderer.js';
@@ -65,10 +67,13 @@ import { normalizeToolErrorClassification } from '../permission/tool-error-code.
65
67
  import { normalizeExecutionPermissionMode } from '../permission/mode.js';
66
68
  import { buildToolLifecycleEventKey } from '../audit/event-key.js';
67
69
  import { aunMessageTranscriptUuid } from '../session/session-transcript.js';
68
- import { loadDaemonConfig } from '../../config-store.js';
69
70
  import { assertAgentPolicyAuthorization } from '../role/runtime-policy.js';
70
71
  import { auditFullAccessEvent } from '../auth/authorization-audit.js';
71
- import { TaskRecoveryService, isTaskRecoveryCurrent } from '../task/task-recovery-service.js';
72
+ import { REPLY_DELIVERY_FAILED_REASON, TaskRecoveryService, isTaskRecoveryCurrent } from '../task/task-recovery-service.js';
73
+ /** Resolve the channel type separately from a channel instance name. */
74
+ export function resolveMessageChannelType(options, message) {
75
+ return options?.channelType || message.channelType || message.channel;
76
+ }
72
77
  const QUESTION_TTL_MS = 24 * 60 * 60 * 1000;
73
78
  /** Pending async questions outlive the originating turn, but never change its backend binding. */
74
79
  /** 带 --thread 的跨话题发送:话题存在于本端索引且目标一致时视为合法发送。 */
@@ -136,7 +141,7 @@ export class AsyncQuestionService {
136
141
  continue;
137
142
  const replyContext = {
138
143
  delivery: envelope.delivery ?? envelope.replyContext?.delivery,
139
- threadId: session.threadId || undefined,
144
+ threadId: sessionTopicThreadId(session),
140
145
  metadata: {
141
146
  encrypted: message.encrypted ?? envelope.replyContext?.metadata?.encrypted,
142
147
  chatmode: envelope.chatmode,
@@ -150,7 +155,7 @@ export class AsyncQuestionService {
150
155
  channel: message.channel, channelType: session.channelType,
151
156
  channelId: session.channelId, peerId: message.peerId, peerType: message.peerType,
152
157
  chatType: session.chatType === 'group' ? 'group' : 'private',
153
- threadId: session.threadId || undefined, selfAID: session.selfAID,
158
+ threadId: sessionTopicThreadId(session), selfAID: session.selfAID,
154
159
  },
155
160
  envelope: { ...envelope, sessionId: session.id, replyContext, delivery: replyContext.delivery },
156
161
  channelKey: adapter.channelKey, createdAt: Date.now(), expiresAt: Date.now() + QUESTION_TTL_MS,
@@ -217,7 +222,7 @@ export class AsyncQuestionService {
217
222
  || session.channel !== record.origin.channel
218
223
  || session.channelType !== record.origin.channelType
219
224
  || session.channelId !== record.origin.channelId
220
- || (session.threadId || '') !== (record.origin.threadId || '')
225
+ || (sessionTopicThreadId(session) ?? '') !== (record.origin.threadId || '')
221
226
  || !(await this.deps.authorize(record, session)))
222
227
  return undefined;
223
228
  return session;
@@ -327,11 +332,18 @@ export class AsyncQuestionService {
327
332
  },
328
333
  });
329
334
  try {
330
- const receipt = await sendInteractionPayload(adapter, {
331
- ...record.envelope, operationId: `${record.id}-${Date.now()}`,
332
- }, interaction, renderActionAsText(interaction), record.envelope.replyContext);
335
+ const envelope = { ...record.envelope, operationId: record.id };
336
+ const receipt = await adapter.restoreInteraction?.(envelope, interaction, record.deliveryReceipt)
337
+ ?? await sendInteractionPayload(adapter, envelope, interaction, renderActionAsText(interaction), record.envelope.replyContext);
333
338
  if (!isInteractionSendAccepted(receipt))
334
339
  throw new Error('Async question card was not accepted for delivery');
340
+ if (typeof receipt === 'object' && (receipt.status === 'sent' || receipt.status === 'queued')) {
341
+ const current = this.read(record.selfAid, record.id);
342
+ if (current) {
343
+ current.deliveryReceipt = receipt;
344
+ this.save(current);
345
+ }
346
+ }
335
347
  }
336
348
  catch (error) {
337
349
  // Keep the durable pending record for a later channel reconnect/daemon restore.
@@ -418,6 +430,7 @@ function isShowActivitiesMode(value) {
418
430
  }
419
431
  const TOOL_INPUT_DIAGNOSTIC_MAX_LENGTH = 96_000;
420
432
  const TOOL_INPUT_DIAGNOSTIC_MAX_FINDINGS = 3;
433
+ const TOOL_INPUT_DIAGNOSTIC_MAX_SHELL_DEPTH = 4;
421
434
  const TOOL_INPUT_DIAGNOSTIC_TIMEOUT_MS = 1_000;
422
435
  function isToolResultFailure(event) {
423
436
  return event.isError === true
@@ -595,49 +608,59 @@ function diagnosticLocation(source, startOffset, endOffset = startOffset) {
595
608
  function diagnosticFinding(ruleId, title, advice, confidence, source, startOffset, endOffset) {
596
609
  return { ruleId, title, advice, confidence, location: diagnosticLocation(source, startOffset, endOffset) };
597
610
  }
598
- function diagnosticCandidates(input) {
611
+ function diagnosticCandidate(source, command = source.value, shellDepth = 0) {
612
+ while (shellDepth < TOOL_INPUT_DIAGNOSTIC_MAX_SHELL_DEPTH) {
613
+ const carrier = resolveCodexShellCarrierString(command);
614
+ if (!carrier)
615
+ break;
616
+ command = carrier.command;
617
+ shellDepth++;
618
+ }
619
+ const commandOffset = source.value.lastIndexOf(command);
620
+ if (commandOffset < 0)
621
+ return undefined;
622
+ return { source, command, commandOffset, carrier: shellDepth > 0, nestedShell: shellDepth > 1 };
623
+ }
624
+ function diagnosticCandidates(input, displayInput) {
599
625
  const candidates = [];
600
- if (typeof input.command === 'string' && boundedDiagnosticInput(input.command)) {
601
- const source = { field: 'command', value: input.command };
602
- const carrier = resolveCodexShellCarrierString(input.command);
603
- if (carrier) {
604
- const commandOffset = input.command.indexOf(carrier.command);
605
- candidates.push({
606
- source,
607
- command: carrier.command,
608
- commandOffset: commandOffset >= 0 ? commandOffset : 0,
609
- carrier: true,
610
- });
611
- }
612
- else {
613
- candidates.push({ source, command: input.command, commandOffset: 0, carrier: false });
614
- }
626
+ if (typeof displayInput.command === 'string' && boundedDiagnosticInput(displayInput.command)) {
627
+ const source = { field: 'command', value: displayInput.command };
628
+ const rawCarrier = typeof input.command === 'string'
629
+ ? resolveCodexShellCarrierString(input.command)
630
+ : undefined;
631
+ const candidate = diagnosticCandidate(source, rawCarrier?.command ?? source.value, rawCarrier ? 1 : 0);
632
+ if (candidate)
633
+ candidates.push(candidate);
615
634
  }
616
635
  if (isStringArgv(input.commandArgv) && input.commandArgv.every(boundedDiagnosticInput)) {
617
636
  const carrier = resolveCodexShellCarrierArgv(input.commandArgv);
618
637
  if (carrier) {
619
638
  const argumentIndex = input.commandArgv.length - 1;
620
- candidates.push({
621
- source: { field: 'commandArgv', value: input.commandArgv[argumentIndex], argumentIndex },
622
- command: carrier.command,
623
- commandOffset: 0,
624
- carrier: true,
625
- });
639
+ const candidate = diagnosticCandidate({ field: 'commandArgv', value: input.commandArgv[argumentIndex], argumentIndex }, carrier.command, 1);
640
+ if (candidate)
641
+ candidates.push(candidate);
626
642
  }
627
643
  else {
628
644
  const argumentIndex = 0;
629
- if (findApplyPatchArgumentsOffset(input.commandArgv[argumentIndex] ?? '') >= 0) {
630
- candidates.push({
631
- source: { field: 'commandArgv', value: input.commandArgv[argumentIndex], argumentIndex },
632
- command: input.commandArgv[argumentIndex],
633
- commandOffset: 0,
634
- carrier: false,
645
+ if (findApplyPatchArgumentsOffset(input.commandArgv[argumentIndex] ?? '') >= 0
646
+ && input.commandArgv.slice(1).every(value => value.trim() === '')) {
647
+ const candidate = diagnosticCandidate({
648
+ field: 'commandArgv', value: input.commandArgv[argumentIndex], argumentIndex,
635
649
  });
650
+ if (candidate)
651
+ candidates.push(candidate);
636
652
  }
637
653
  }
638
654
  }
639
655
  return candidates;
640
656
  }
657
+ function addApplyPatchMissingDocumentFinding(findings, candidate) {
658
+ const argumentsOffset = findApplyPatchArgumentsOffset(candidate.command);
659
+ if (argumentsOffset < 0 || candidate.command.slice(argumentsOffset).trim().length > 0)
660
+ return;
661
+ const executableStart = candidate.command.length - candidate.command.trimStart().length;
662
+ findings.push(diagnosticFinding('apply-patch-missing-document', 'apply_patch 缺少完整补丁文档', '提交完整补丁文档,并以 *** Begin Patch 开头、以 *** End Patch 结尾。', '高', candidate.source, candidate.commandOffset + executableStart, candidate.commandOffset + argumentsOffset));
663
+ }
641
664
  function addHeredocDiagnosticFindings(findings, candidate) {
642
665
  const argumentsOffset = findApplyPatchArgumentsOffset(candidate.command);
643
666
  if (argumentsOffset < 0)
@@ -673,11 +696,11 @@ function addApplyPatchCompositionFinding(findings, candidate) {
673
696
  findings.push(diagnosticFinding('apply-patch-composed-command', 'apply_patch 与其它 Shell 命令组合提交', '一次工具调用只提交完整的 apply_patch;测试、检查或其它命令另行调用。', '高', candidate.source, candidate.commandOffset + separator, candidate.commandOffset + separator + 1));
674
697
  }
675
698
  function addNestedShellDiagnosticFinding(findings, candidate) {
676
- if (!candidate.carrier || findApplyPatchArgumentsOffset(candidate.command) < 0)
699
+ if (!candidate.nestedShell || findApplyPatchArgumentsOffset(candidate.command) < 0)
677
700
  return;
678
701
  findings.push(diagnosticFinding('nested-shell-escaping-risk', '命令包含嵌套 Shell 包装层', '尽量直接提交单层 Shell 命令;需要 heredoc 或 JSON 时,减少外层转义和包装。', '中', candidate.source, candidate.commandOffset, candidate.commandOffset + candidate.command.length));
679
702
  }
680
- function addJsonArrayDiagnosticFinding(findings, input) {
703
+ function addJsonArrayDiagnosticFinding(findings, input, displayInput) {
681
704
  const rawArgv = isStringArgv(input.commandArgv) ? input.commandArgv : undefined;
682
705
  const parsedArgv = rawArgv ?? (typeof input.command === 'string'
683
706
  ? (() => {
@@ -694,7 +717,7 @@ function addJsonArrayDiagnosticFinding(findings, input) {
694
717
  const value = parsedArgv[6];
695
718
  const source = rawArgv
696
719
  ? { field: 'commandArgv', value: rawArgv[6] ?? '', argumentIndex: 6 }
697
- : { field: 'command', value: typeof input.command === 'string' ? input.command : '' };
720
+ : { field: 'command', value: typeof displayInput.command === 'string' ? displayInput.command : '' };
698
721
  if (value === undefined) {
699
722
  findings.push(diagnosticFinding('json-array-argument-missing', 'tags 参数缺少完整 JSON 数组值', '将 tags 作为一个完整参数传入,例如 PowerShell 使用单引号包住 ["DSH","AUN"]。', '高', source, source.value.length));
700
723
  return;
@@ -713,7 +736,7 @@ function addJsonArrayDiagnosticFinding(findings, input) {
713
736
  && (value.startsWith('[') || value.endsWith(','));
714
737
  findings.push(diagnosticFinding(split ? 'json-array-argument-splitting' : 'json-array-argument-invalid', split ? 'tags 的 JSON 数组疑似被拆成多个参数' : 'tags 参数不是可解析的 JSON 字符串数组', '保证整个 JSON 数组位于同一个参数中;PowerShell 普通单引号字符串内不要再添加反斜杠转义。', split ? '中' : '高', source, split ? source.value.length : 0, source.value.length));
715
738
  }
716
- function addCommandArgvMismatchFinding(findings, input) {
739
+ function addCommandArgvMismatchFinding(findings, input, displayInput) {
717
740
  if (typeof input.command !== 'string' || !isStringArgv(input.commandArgv))
718
741
  return;
719
742
  const commandArgv = input.commandArgv;
@@ -725,7 +748,8 @@ function addCommandArgvMismatchFinding(findings, input) {
725
748
  if (!parsed.ok || parsed.argv.length === commandArgv.length
726
749
  && parsed.argv.every((value, index) => value === commandArgv[index]))
727
750
  return;
728
- findings.push(diagnosticFinding('command-argv-mismatch', 'command 与 commandArgv 的解析结果不一致', '以执行器提供的结构化 commandArgv 为准,避免在外层重新拼接或改写命令文本。', '中', { field: 'command', value: input.command }, 0, input.command.length));
751
+ const displayCommand = typeof displayInput.command === 'string' ? displayInput.command : input.command;
752
+ findings.push(diagnosticFinding('command-argv-mismatch', 'command 与 commandArgv 的解析结果不一致', '以执行器提供的结构化 commandArgv 为准,避免在外层重新拼接或改写命令文本。', '中', { field: 'command', value: displayCommand }, 0, displayCommand.length));
729
753
  }
730
754
  function deduplicateDiagnosticFindings(findings) {
731
755
  const seen = new Set();
@@ -749,21 +773,23 @@ function diagnoseToolInput(original, event, deadline) {
749
773
  return undefined;
750
774
  }
751
775
  }
752
- const candidates = diagnosticCandidates(input);
776
+ const displayInput = toolInputForDisplay(original.name, input) ?? input;
777
+ const candidates = diagnosticCandidates(input, displayInput);
753
778
  if (Date.now() > deadline)
754
779
  return undefined;
755
780
  const findings = [];
756
781
  for (const candidate of candidates) {
757
782
  if (Date.now() > deadline)
758
783
  return undefined;
784
+ addApplyPatchMissingDocumentFinding(findings, candidate);
759
785
  addHeredocDiagnosticFindings(findings, candidate);
760
786
  addApplyPatchCompositionFinding(findings, candidate);
761
787
  addNestedShellDiagnosticFinding(findings, candidate);
762
788
  }
763
789
  if (Date.now() <= deadline)
764
- addJsonArrayDiagnosticFinding(findings, input);
790
+ addJsonArrayDiagnosticFinding(findings, input, displayInput);
765
791
  if (Date.now() <= deadline)
766
- addCommandArgvMismatchFinding(findings, input);
792
+ addCommandArgvMismatchFinding(findings, input, displayInput);
767
793
  if (Date.now() > deadline)
768
794
  return undefined;
769
795
  const deduplicated = deduplicateDiagnosticFindings(findings);
@@ -944,7 +970,10 @@ function formatRetryInputAlreadySubmittedMessage(error) {
944
970
  if (retryCount !== undefined) {
945
971
  return `⚠️ Codex 已重连 ${retryCount} 次模型服务后仍失败。本次执行已停止。`;
946
972
  }
947
- return '⚠️ Codex 已接收本次消息,但模型服务请求失败。本次执行已停止。';
973
+ // 无重试进度可报时,说明输入已提交但终态未确认:明确告知未自动重试,
974
+ // 避免用户误以为系统会在后台继续。
975
+ const reason = getErrorMessage(error, undefined, false) || 'Codex 暂时不可用';
976
+ return `⚠️ ${reason}\n当前输入已经提交,但本轮未能确认完成。系统未自动重试,请检查会话状态后再决定是否重试。`;
948
977
  }
949
978
  function isNonErrorInterruptReason(reason) {
950
979
  return reason === 'new_message'
@@ -1007,9 +1036,10 @@ function formatProtocolReplayExhaustedMessage(reason, replayAttempts) {
1007
1036
  return `${detail}(${reason}),已立即重试 ${replayAttempts} 次仍失败,任务未提交。\n请稍后重试;若持续出现,请切换模型或向上游服务商提供错误代码和发生时间。`;
1008
1037
  }
1009
1038
  function withFallbackModel(override, candidate) {
1010
- const { model: _model, effort: _effort, effortMode: _effortMode, ...preserved } = override || {};
1039
+ const { model: _model, provider: _provider, effort: _effort, effortMode: _effortMode, ...preserved } = override || {};
1011
1040
  return {
1012
1041
  ...preserved,
1042
+ ...(candidate.provider ? { provider: candidate.provider } : {}),
1013
1043
  model: candidate.model,
1014
1044
  ...(candidate.effort ? { effort: candidate.effort } : { effortMode: 'model_default' }),
1015
1045
  };
@@ -1109,6 +1139,9 @@ function mergeContextRecoveryResult(previous, next) {
1109
1139
  function isStandaloneContextLimitErrorLine(line) {
1110
1140
  return /^(?:(?:api )?error\s*:\s*)?(?:prompt is too long|input is too long|context too long|context_length_exceeded|context_window_exceeded|context window limit|maximum context length|上下文过长)\s*[.!。]?$/i.test(line.trim());
1111
1141
  }
1142
+ function stripThinkingBlocks(text) {
1143
+ return text.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '');
1144
+ }
1112
1145
  /**
1113
1146
  * 网关有时把上下文超限错误包装进 text event。只丢弃独立错误行,保留同一 event
1114
1147
  * 中其它行及包含错误字面量的正常输出;原文本仍由调用方保留作恢复判定。
@@ -1211,6 +1244,7 @@ export class ResponseEngine {
1211
1244
  agentMap;
1212
1245
  legacySingleRunnerMode;
1213
1246
  primaryRunnerKey;
1247
+ daemonTriggerOwnerAid;
1214
1248
  interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
1215
1249
  timeoutErrors = new Map();
1216
1250
  activeTimeoutHandlers = new Map();
@@ -1262,6 +1296,17 @@ export class ResponseEngine {
1262
1296
  && this.agentMap.values().next().value?.name === requestedBaseagent) {
1263
1297
  return this.agentMap.values().next().value;
1264
1298
  }
1299
+ // The daemon scheduler has no Agent registry entry. Its runner owner is
1300
+ // explicitly bound at startup, so multiple Agents using the same backend
1301
+ // cannot make host-owned Trigger sessions ambiguous.
1302
+ if (channel === 'daemon' && this.daemonTriggerOwnerAid
1303
+ && selfAID === this.daemonTriggerOwnerAid && requestedBaseagent) {
1304
+ const ownerName = this.primaryRunnerKey.slice(0, this.primaryRunnerKey.lastIndexOf('::'));
1305
+ const runner = this.agentMap.get(`${ownerName}::${requestedBaseagent}`);
1306
+ if (runner)
1307
+ return runner;
1308
+ throw new BaseagentRunnerUnavailableError(ownerName, requestedBaseagent, this.getAvailableBaseagentsForOwner(ownerName));
1309
+ }
1265
1310
  if (selfAID && requestedBaseagent) {
1266
1311
  const key = `${selfAID}::${requestedBaseagent}`;
1267
1312
  if (this.agentMap.has(key))
@@ -1301,7 +1346,7 @@ export class ResponseEngine {
1301
1346
  if (!aid)
1302
1347
  return undefined;
1303
1348
  const config = resolveEffective({ self: aid }, { cache: true, expand: true });
1304
- return createDecisionInferenceProvider(config.decision_model);
1349
+ return createDecisionInferenceProvider(config.decision_model, config.Services);
1305
1350
  }
1306
1351
  getAvailableBaseagentsForOwner(evolName) {
1307
1352
  return loadedBaseagentIds(this.agentMap.keys(), evolName);
@@ -1350,7 +1395,8 @@ export class ResponseEngine {
1350
1395
  }
1351
1396
  /** 判断是否为后台会话(仅主会话参与判断,话题会话独立) */
1352
1397
  isBackgroundSession(session, _channel, _channelId) {
1353
- if (session.threadId)
1398
+ // 只有话题会话是独立的;主话题(含规范 id)仍是这条关系的主会话。
1399
+ if (isTopicSession(session))
1354
1400
  return false;
1355
1401
  // 使用 session 自身的 channelType 精确定位 active.json,避免扫描误匹配
1356
1402
  const active = this.sessionManager.getActiveSessionSync(session.channel, session.channelId, session.channelType, session.selfAID);
@@ -1578,12 +1624,12 @@ export class ResponseEngine {
1578
1624
  }
1579
1625
  }
1580
1626
  async observerTargetSession(target) {
1581
- const session = target.threadId
1582
- ? await this.sessionManager.getThreadSession(target.channelKey, target.channelId, target.threadId)
1627
+ const session = isTopicThreadId(target.threadId)
1628
+ ? await this.sessionManager.getThreadSession(target.channelKey, target.channelId, String(target.threadId))
1583
1629
  : this.sessionManager.getActiveSessionSync(target.channelKey, target.channelId, 'aun', target.selfAid);
1584
1630
  if (!session || session.deletedAt || session.selfAID !== target.selfAid || session.channel !== target.channelKey
1585
1631
  || session.channelId !== target.channelId || session.chatType !== target.chatType
1586
- || (session.threadId || '') !== (target.threadId || '')
1632
+ || (sessionTopicThreadId(session) ?? '') !== (target.threadId || '')
1587
1633
  || !this.sessionManager.hasBoundAgentSession(session)
1588
1634
  || (target.chatType === 'group' && (session.metadata?.groupId !== target.channelId
1589
1635
  || !session.metadata?.observerLastActorId)))
@@ -1640,7 +1686,7 @@ export class ResponseEngine {
1640
1686
  try {
1641
1687
  await this.messageQueue.enqueuePersisted(session.id, {
1642
1688
  channel: session.channel, channelType: 'aun', channelId: session.channelId,
1643
- selfAID: session.selfAID, baseagent: session.baseagent, threadId: session.threadId,
1689
+ selfAID: session.selfAID, baseagent: session.baseagent, threadId: sessionTopicThreadId(session) ?? '',
1644
1690
  chatType: target.chatType, groupId: target.chatType === 'group' ? target.channelId : undefined,
1645
1691
  peerId: actorId, peerName: session.metadata?.observerLastPeerName || session.metadata?.peerName,
1646
1692
  peerType: session.metadata?.observerLastPeerType || session.metadata?.peerType,
@@ -1727,7 +1773,7 @@ export class ResponseEngine {
1727
1773
  return { state: 'terminal', taskId: terminal.taskId, generation: terminal.generation,
1728
1774
  outcome: terminal.status === 'committed' ? 'completed'
1729
1775
  : ['cancelled', 'superseded', 'interrupted'].includes(terminal.status) ? 'skipped' : 'failed',
1730
- reason: terminal.reason };
1776
+ reason: terminal.reason, recoverable: terminal.recoverable };
1731
1777
  }
1732
1778
  const queued = this.messageQueue?.getMessageState(sessionId, attemptId, selfAid);
1733
1779
  if (queued?.state === 'queued')
@@ -1765,9 +1811,9 @@ export class ResponseEngine {
1765
1811
  if (timeout)
1766
1812
  await timeout(error);
1767
1813
  else
1768
- await this.interruptSession(sessionId, error instanceof OrphanedTargetSessionError ? 'execution_error' : 'timeout');
1814
+ await this.interruptSession(sessionId, error instanceof OrphanedTargetSessionError ? 'execution_error' : 'timeout', false, isRetryableError(error));
1769
1815
  }
1770
- async interruptSession(sessionId, reason, strict = false) {
1816
+ async interruptSession(sessionId, reason, strict = false, recoverable) {
1771
1817
  this.clearPauseSession(sessionId);
1772
1818
  // A retry delay has no active runner stream to abort. Wake it before any
1773
1819
  // session-store I/O so /stop and a newer message take effect immediately.
@@ -1796,7 +1842,7 @@ export class ResponseEngine {
1796
1842
  // shutdown 超时退出会再次留下 started/interrupting 的悬空任务。
1797
1843
  const runtimeContext = this.activeTaskRuntimeContexts.get(sessionId);
1798
1844
  const taskSpan = this.activeTaskSpans.get(sessionId);
1799
- const taskId = (await this.turnCoordinator.completeInterrupt(session, effectiveReason))
1845
+ const taskId = (await this.turnCoordinator.completeInterrupt(session, effectiveReason, recoverable))
1800
1846
  ?? interruptedTaskId
1801
1847
  ?? taskSpan?.taskId;
1802
1848
  if (taskId) {
@@ -1841,7 +1887,7 @@ export class ResponseEngine {
1841
1887
  replyContext: {
1842
1888
  ...replyContextFromSession(session),
1843
1889
  sessionId: session.id,
1844
- threadId: session.threadId || session.metadata?.replyContext?.threadId,
1890
+ threadId: sessionTopicThreadId(session) ?? session.metadata?.replyContext?.threadId,
1845
1891
  },
1846
1892
  });
1847
1893
  const payload = {
@@ -2013,6 +2059,13 @@ export class ResponseEngine {
2013
2059
  for (const { adapter } of this.channels.values())
2014
2060
  this.restoreAsyncQuestions(adapter);
2015
2061
  }
2062
+ /** Bind daemon-owned Trigger sessions to the configured primary runner owner. */
2063
+ setDaemonTriggerOwner(aid) {
2064
+ const normalized = aid.trim();
2065
+ if (!normalized)
2066
+ throw new Error('daemon trigger owner AID is required');
2067
+ this.daemonTriggerOwnerAid = normalized;
2068
+ }
2016
2069
  /** 更新 EvolAgent.lastActivity —— 每次发出 status.* 事件(含 progress)时调用 */
2017
2070
  touchAgentActivity(channelKey) {
2018
2071
  const owning = this.agentRegistry?.resolveByChannel(channelKey);
@@ -2085,10 +2138,10 @@ export class ResponseEngine {
2085
2138
  if (!peerChannelId)
2086
2139
  return { items: [] };
2087
2140
  try {
2088
- const claim = claimHints(resolvePaths().sessionsDir, 'aun', peerChannelId, selfAID, session.threadId, (message.chatType || session.chatType || 'private'), message.source === 'owner-inject' ? 'interrupt' : 'next', message.observerHintId, session.channel);
2141
+ const claim = claimHints(resolvePaths().sessionsDir, 'aun', peerChannelId, selfAID, (sessionTopicThreadId(session) ?? ''), (message.chatType || session.chatType || 'private'), message.source === 'owner-inject' ? 'interrupt' : 'next', message.observerHintId, session.channel);
2089
2142
  if (claim.hints.length === 0)
2090
2143
  return { items: [] };
2091
- logger.info(`[ResponseEngine] claimed ${claim.hints.length} owner-hint(s) for ${peerChannelId} thread=${session.threadId || 'main'}`);
2144
+ logger.info(`[ResponseEngine] claimed ${claim.hints.length} owner-hint(s) for ${peerChannelId} thread=${sessionTopicThreadId(session) ?? 'main'}`);
2092
2145
  return { items: hintsToSubMessages(claim.hints), claim };
2093
2146
  }
2094
2147
  catch (e) {
@@ -2193,7 +2246,7 @@ export class ResponseEngine {
2193
2246
  // while the original override can still describe the pre-run UNBOUND
2194
2247
  // snapshot. Compact and retry must address the accepted backend under the
2195
2248
  // same complete TurnLease or a runner could create a second backend.
2196
- const recoveryModelOverride = session.threadId
2249
+ const recoveryModelOverride = isTopicSession(session)
2197
2250
  ? {
2198
2251
  ...(modelOverride || {}),
2199
2252
  backend: { kind: 'topic', agentSessionId: session.agentSessionId },
@@ -2213,6 +2266,7 @@ export class ResponseEngine {
2213
2266
  this.announcedCompactStarts.add(session.id);
2214
2267
  let compacted;
2215
2268
  try {
2269
+ await beforeRunQuery?.();
2216
2270
  compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath, recoveryModelOverride);
2217
2271
  }
2218
2272
  finally {
@@ -2222,7 +2276,7 @@ export class ResponseEngine {
2222
2276
  if (compactResult === true || compactResult?.ok === true) {
2223
2277
  await this.emitOperationalNotice(renderer, '✅ 压缩完成,继续处理...', 'info', 'compact-retry');
2224
2278
  await renderer.flush();
2225
- beforeRunQuery?.();
2279
+ await beforeRunQuery?.();
2226
2280
  const retryStream = await agent.runQuery(session.id, insertMessagePromptContext(prompt, messagePromptContext), absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, recoveryModelOverride, runtimeEnv);
2227
2281
  if (await this.cancelStaleRunnerStart(agent, session.id, streamKey, turnLease, turnLease.taskId)) {
2228
2282
  throw new Error('RUNNER_START_SUPERSEDED');
@@ -2355,18 +2409,27 @@ export class ResponseEngine {
2355
2409
  return false;
2356
2410
  }
2357
2411
  try {
2358
- return new Set(loadDaemonConfig().owners ?? []).has(trigger.authorizedBy);
2412
+ return new Set(getDaemonOwners()).has(trigger.authorizedBy);
2359
2413
  }
2360
2414
  catch {
2361
2415
  return false;
2362
2416
  }
2363
2417
  }
2418
+ async assertCurrentExecutionAuthorization(context, agent) {
2419
+ if (context.executionPermission) {
2420
+ await this.assertCurrentFullAccessExecutionAuthorization(context.executionPermission, agent);
2421
+ }
2422
+ else {
2423
+ await assertOwnershipAuthorization(context);
2424
+ }
2425
+ }
2364
2426
  /**
2365
2427
  * The authorization envelope is immutable for one logical run, but the
2366
2428
  * approving owner and runner profile are live policy. Check
2367
2429
  * them immediately before every Runner invocation, including retries.
2368
2430
  */
2369
- assertCurrentFullAccessExecutionAuthorization(authorization, agent) {
2431
+ async assertCurrentFullAccessExecutionAuthorization(authorization, agent) {
2432
+ await refreshOwnershipForAuthorization(authorization.agentAid);
2370
2433
  if (authorization.permissionMode !== 'fullaccess'
2371
2434
  || authorization.processRole !== 'fullaccess-run'
2372
2435
  || authorization.dataScope !== 'daemon'
@@ -2384,7 +2447,7 @@ export class ResponseEngine {
2384
2447
  else if (authorization.source !== 'fullaccess-command' && authorization.source !== 'trigger') {
2385
2448
  throw new Error('fullaccess execution authorization has an unsupported source');
2386
2449
  }
2387
- const currentOwners = new Set(loadDaemonConfig().owners ?? []);
2450
+ const currentOwners = new Set(getDaemonOwners());
2388
2451
  if (!currentOwners.has(authorization.authorizedBy)) {
2389
2452
  throw new Error('fullaccess execution authorization is no longer owned');
2390
2453
  }
@@ -2395,6 +2458,13 @@ export class ResponseEngine {
2395
2458
  resolveTriggerExecutionIdentity(message, selfAid) {
2396
2459
  if (message.source !== 'trigger' || !message.triggerMeta?.triggerId)
2397
2460
  return undefined;
2461
+ // 跨话题复查(target_session 投递):本轮是"T2 侧 agent 被自己的复查通知唤醒",
2462
+ // 身份就是 T2 会话自身的对端,与普通入站消息一致。
2463
+ // 若沿用 originPeerId(来源话题 T1 的对端),整个轮次的角色与权限都会错位。
2464
+ if (message.triggerMeta.reviewKind === 'cross-thread-review'
2465
+ || message.triggerMeta.labels?.kind === 'cross-thread-review') {
2466
+ return undefined;
2467
+ }
2398
2468
  const actorId = message.triggerMeta.originPeerId;
2399
2469
  const channelType = message.triggerMeta.originChannelType;
2400
2470
  const originChannelKey = message.triggerMeta.originChannelKey;
@@ -2518,6 +2588,7 @@ export class ResponseEngine {
2518
2588
  originTriggerId: trigger.triggerId,
2519
2589
  messageId: opts.messageId ?? message.messageId ?? runId,
2520
2590
  error,
2591
+ recoverable: opts.recoverable ?? isRetryableError(new Error(error)),
2521
2592
  targetChannel: message.channel,
2522
2593
  targetChannelId: message.channelId,
2523
2594
  fireTime: trigger.fireTime ?? 0,
@@ -2691,7 +2762,7 @@ export class ResponseEngine {
2691
2762
  // 先解析会话,再优先用 session.metadata.channelKey 精确定位实例级 adapter
2692
2763
  // message.channel 现在存实例名(channelName),可直接用于精确路由
2693
2764
  const { session, absoluteProjectPath } = await this.resolveSession(message);
2694
- if (message.source === 'owner-inject' && (!message.observerHintId || !peekHints(resolvePaths().sessionsDir, 'aun', session.channelId, session.selfAID || '', session.threadId, session.chatType, 'interrupt', session.channel).some(item => item.id === message.observerHintId))) {
2765
+ if (message.source === 'owner-inject' && (!message.observerHintId || !peekHints(resolvePaths().sessionsDir, 'aun', session.channelId, session.selfAID || '', (sessionTopicThreadId(session) ?? ''), session.chatType, 'interrupt', session.channel).some(item => item.id === message.observerHintId))) {
2695
2766
  this.reportObserverHintFailureForMessage(message, 'HINT_UNAVAILABLE');
2696
2767
  return;
2697
2768
  }
@@ -2703,6 +2774,7 @@ export class ResponseEngine {
2703
2774
  const accessAdapter = accessChannelInfo?.adapter;
2704
2775
  const accessAdapterSelfAid = typeof accessAdapter?._selfAid === 'function' ? accessAdapter._selfAid() : undefined;
2705
2776
  const selfAidForAccess = accessAdapterSelfAid || message.selfAID || session.selfAID || undefined;
2777
+ await refreshOwnershipForAuthorization(selfAidForAccess, { maxAgeMs: OWNERSHIP_CACHE_MAX_AGE_MS });
2706
2778
  if (message.handoffDelivery?.direction === 'target'
2707
2779
  && this.handoffRuntime
2708
2780
  && selfAidForAccess) {
@@ -2719,7 +2791,10 @@ export class ResponseEngine {
2719
2791
  const triggerExecutionIdentity = this.resolveTriggerExecutionIdentity(message, selfAidForAccess);
2720
2792
  const handoffExecutionIdentity = this.resolveHandoffOriginExecutionIdentity(message, selfAidForAccess);
2721
2793
  const executionIdentity = triggerExecutionIdentity ?? handoffExecutionIdentity;
2722
- if (message.source === 'trigger' && !triggerExecutionIdentity) {
2794
+ // 跨话题复查走会话自身身份,本来就解析不出 Trigger 的 origin 身份,不属于"身份缺失"。
2795
+ const isCrossThreadReview = message.triggerMeta?.reviewKind === 'cross-thread-review'
2796
+ || message.triggerMeta?.labels?.kind === 'cross-thread-review';
2797
+ if (message.source === 'trigger' && !triggerExecutionIdentity && !isCrossThreadReview) {
2723
2798
  logger.warn(`[ResponseEngine] Trigger execution identity missing: trigger=${message.triggerMeta?.triggerId ?? '<none>'}`);
2724
2799
  const channelInfo = this.resolveChannelInfo(accessChannelKey);
2725
2800
  if (channelInfo) {
@@ -2872,12 +2947,13 @@ export class ResponseEngine {
2872
2947
  return;
2873
2948
  timeoutTriggered = true;
2874
2949
  timeoutControl.error = error;
2950
+ timeoutControl.recoverable = isRetryableError(error);
2875
2951
  this.timeoutErrors.set(session.id, error);
2876
2952
  this.turnCoordinator.hydrate(session);
2877
2953
  const reason = error instanceof OrphanedTargetSessionError ? 'execution_error' : 'timeout';
2878
2954
  this.turnCoordinator.invalidate(session.id, reason);
2879
2955
  this.interruptedSessions.set(session.id, reason);
2880
- const barrier = this.interruptSession(session.id, reason)
2956
+ const barrier = this.interruptSession(session.id, reason, false, timeoutControl.recoverable)
2881
2957
  .catch(interruptError => {
2882
2958
  logger.debug(`[ResponseEngine] Timeout interrupt barrier failed: ${interruptError instanceof Error ? interruptError.message : String(interruptError)}`);
2883
2959
  });
@@ -3029,7 +3105,7 @@ export class ResponseEngine {
3029
3105
  && commandAuthorization.authorizedBy.length > 0) {
3030
3106
  let currentOwner = false;
3031
3107
  try {
3032
- currentOwner = new Set(loadDaemonConfig().owners ?? []).has(commandAuthorization.authorizedBy);
3108
+ currentOwner = new Set(getDaemonOwners()).has(commandAuthorization.authorizedBy);
3033
3109
  if (commandAuthorization.source === 'agent-policy')
3034
3110
  assertAgentPolicyAuthorization(commandAuthorization);
3035
3111
  }
@@ -3153,9 +3229,9 @@ export class ResponseEngine {
3153
3229
  return;
3154
3230
  }
3155
3231
  // Agent-owned turns are runnable only in the two explicit execution
3156
- // states. An untransitioned `created` agent or malformed lifecycle must
3157
- // not fall through to a base runner's default prompt. Missing lifecycle
3158
- // is normalized to active for legacy configurations.
3232
+ // states. Setup and owner-pending agents must not fall through to a base
3233
+ // runner's default prompt. Missing lifecycle is normalized to active for
3234
+ // legacy configurations.
3159
3235
  if (owningAgentForTask && ((!isActive && !isBootstrapping) || initializationBlocksTasks(owningAgentForTask.config))) {
3160
3236
  const blockedLifecycle = lifecycle
3161
3237
  ?? `invalid (${String(owningAgentForTask.config.lifecycle)})`;
@@ -3257,7 +3333,7 @@ export class ResponseEngine {
3257
3333
  causation: taskCausation,
3258
3334
  });
3259
3335
  // ─── 解析 self/peer/config(响应模式解析的输入)───
3260
- const currentChannelType = options?.channelType || message.channel;
3336
+ const currentChannelType = resolveMessageChannelType(options, message);
3261
3337
  const adapterAny = channelInfo.adapter;
3262
3338
  const adapterSelfAid = typeof adapterAny._selfAid === 'function' ? adapterAny._selfAid() : undefined;
3263
3339
  // During bootstrap the lifecycle owner is the authoritative identity. Do
@@ -3283,11 +3359,20 @@ export class ResponseEngine {
3283
3359
  // operator. Keep the historical private-chat fallback (where channelId
3284
3360
  // is the peer identity for AUN), but fail closed for group messages that
3285
3361
  // arrive without a sender/operator ID.
3362
+ // 跨话题复查投递的 message.peerId 是占位的 `trigger:<id>`,不是真实对端。这一轮身份
3363
+ // 按 T2 会话自身的对端算(resolveTriggerExecutionIdentity 对这类投递返回 undefined),
3364
+ // 私聊即 channelId;否则 actor 落成伪身份,在唤醒话题里连本条复查都管不了。
3365
+ const isCrossThreadReviewTurn = message.source === 'trigger'
3366
+ && (message.triggerMeta?.reviewKind === 'cross-thread-review'
3367
+ || message.triggerMeta?.labels?.kind === 'cross-thread-review');
3368
+ const turnPeerId = isCrossThreadReviewTurn && configChatType === 'private'
3369
+ ? message.channelId
3370
+ : peerIdRaw;
3286
3371
  const configActorId = authorizationIdentity?.actorId
3287
- ?? (message.peerId || (configChatType === 'private' ? message.channelId : undefined));
3372
+ ?? (turnPeerId || (configChatType === 'private' ? message.channelId : undefined));
3288
3373
  const peerKeyId = authorizationIdentity?.actorId ?? (session.chatType === 'group'
3289
3374
  ? (session.metadata?.groupId || message.channelId)
3290
- : peerIdRaw);
3375
+ : turnPeerId);
3291
3376
  const peerKey = authorizationIdentity?.peerKey ?? ((configChannelType && peerKeyId)
3292
3377
  ? formatPeerKey(configChannelType, peerKeyId)
3293
3378
  : undefined);
@@ -3417,7 +3502,7 @@ export class ResponseEngine {
3417
3502
  return {
3418
3503
  ...replyContextFromSession(session, base),
3419
3504
  sessionId: session.id,
3420
- threadId: session.threadId || base?.threadId,
3505
+ threadId: sessionTopicThreadId(session) ?? base?.threadId,
3421
3506
  metadata: withTaskMetadata(base?.metadata),
3422
3507
  };
3423
3508
  };
@@ -3489,6 +3574,7 @@ export class ResponseEngine {
3489
3574
  timeoutControl.deliverUnhandled = async (timeoutError) => {
3490
3575
  if (!timeoutControl.claimTerminal())
3491
3576
  return;
3577
+ const recoverable = timeoutControl.recoverable === true;
3492
3578
  const isTotalExecutionTimeout = timeoutError.message === 'TOTAL_EXECUTION_TIMEOUT';
3493
3579
  const totalExecutionMs = timeoutError instanceof ExecutionTimeoutError ? timeoutError.timeoutMs : this.totalExecutionLimitMs();
3494
3580
  const statusPayload = timeoutError instanceof OrphanedTargetSessionError ? {
@@ -3525,6 +3611,7 @@ export class ResponseEngine {
3525
3611
  await adapter.send(envelope, {
3526
3612
  kind: 'result.error',
3527
3613
  text: userMessage,
3614
+ recoverable,
3528
3615
  ...(getErrorRawText(timeoutError) ? { raw: getErrorRawText(timeoutError) } : {}),
3529
3616
  subtype: normalizeCoreErrorSubtype(timeoutError, undefined, isTotalExecutionTimeout ? 'timeout' : 'timeout'),
3530
3617
  reason: isExecutionFailure(timeoutError)
@@ -3538,6 +3625,7 @@ export class ResponseEngine {
3538
3625
  this.publishTriggerExecutionFailure(message, isExecutionFailure(timeoutError) ? timeoutError.reason : timeoutError.message, {
3539
3626
  messageId,
3540
3627
  causation: taskCausation,
3628
+ recoverable,
3541
3629
  });
3542
3630
  const errorType = prefixErrorType(ERROR_PREFIX.INFRA, classifyError(timeoutError));
3543
3631
  this.eventBus.publish({
@@ -3550,6 +3638,7 @@ export class ResponseEngine {
3550
3638
  taskId,
3551
3639
  generation: turnLease?.generation,
3552
3640
  error: timeoutError.message,
3641
+ recoverable,
3553
3642
  terminalReason: isExecutionFailure(timeoutError) ? timeoutError.reason : undefined,
3554
3643
  errorType,
3555
3644
  agentName: agentNameForStats,
@@ -3817,6 +3906,12 @@ export class ResponseEngine {
3817
3906
  }
3818
3907
  }
3819
3908
  opts.sessionId = session.id;
3909
+ // 本地日志用的完整话题身份(含主话题规范 id)。上面只合并了入站
3910
+ // message.replyContext,它不含主话题 id;线上字段由 threadId 单独控制,
3911
+ // 这里补的 localTopicId 不会进入报文。
3912
+ const localTopicId = sessionLocalTopicId(session);
3913
+ if (localTopicId)
3914
+ opts.localTopicId = localTopicId;
3820
3915
  if (payload.kind === 'result.text' && payload.isFinal) {
3821
3916
  opts.ext = { ...opts.ext, isFinal: true };
3822
3917
  }
@@ -3911,6 +4006,7 @@ export class ResponseEngine {
3911
4006
  approverPolicy: actorIsManager ? 'agent_manager' : 'requester',
3912
4007
  owners,
3913
4008
  admins,
4009
+ hostOwners: Array.from(new Set(getDaemonOwners())),
3914
4010
  resolveApproverCandidates,
3915
4011
  ownerAdapter: aunAdapter,
3916
4012
  forceHandoff: !!authorizationIdentity,
@@ -3927,7 +4023,7 @@ export class ResponseEngine {
3927
4023
  : undefined,
3928
4024
  originAuthRevision,
3929
4025
  originRole: peerRole,
3930
- originThreadId: session.threadId || message.threadId || undefined,
4026
+ originThreadId: sessionTopicThreadId(session) ?? (isTopicThreadId(message.threadId) ? message.threadId : undefined),
3931
4027
  originChatDir: this.sessionManager.getChatDir(session),
3932
4028
  approvalTtlMs: 20 * 60 * 1000,
3933
4029
  reauthorizeApproval: (request) => {
@@ -3959,6 +4055,7 @@ export class ResponseEngine {
3959
4055
  actor: currentApprover,
3960
4056
  owners: currentOwners,
3961
4057
  admins: currentAdmins,
4058
+ hostOwners: Array.from(new Set(getDaemonOwners())),
3962
4059
  originPrincipalId: hasTrustedPrincipal(currentOrigin)
3963
4060
  ? currentOrigin.principalId
3964
4061
  : undefined,
@@ -4080,13 +4177,17 @@ export class ResponseEngine {
4080
4177
  }
4081
4178
  : {}),
4082
4179
  armApprovedDelegationCommand: this.agentDelegationRegistry
4083
- ? (sessionToken, commandHash) => this.agentDelegationRegistry.armApprovedCommand({
4180
+ ? (sessionToken, commandHash, approvalId) => this.agentDelegationRegistry.armApprovedCommand({
4084
4181
  sessionToken,
4085
4182
  sessionId: session.id,
4086
4183
  taskId,
4087
4184
  commandHash,
4185
+ approvalId,
4088
4186
  })
4089
4187
  : undefined,
4188
+ revokeApprovedDelegationCommand: this.agentDelegationRegistry
4189
+ ? (approvalId) => this.agentDelegationRegistry.revokeApprovedCommand(session.id, taskId, approvalId)
4190
+ : undefined,
4090
4191
  bindSessionCredential: this.agentDelegationRegistry
4091
4192
  ? (sessionToken, threadId) => this.agentDelegationRegistry.registerSessionCredential({
4092
4193
  sessionToken, threadId, sessionId: session.id, taskId,
@@ -4240,6 +4341,7 @@ export class ResponseEngine {
4240
4341
  let skipEvolcoreModel = false;
4241
4342
  let probingPrimaryModel = false;
4242
4343
  let primaryModel;
4344
+ let primaryProvider;
4243
4345
  let fallbackCandidates = [];
4244
4346
  let previousFallbackState;
4245
4347
  let agentModel;
@@ -4311,7 +4413,8 @@ export class ResponseEngine {
4311
4413
  || !message.triggerMeta.authorizedBy) {
4312
4414
  throw new Error('fullaccess Trigger override lacks trusted scheduler identity or approval provenance');
4313
4415
  }
4314
- const currentOwners = new Set(loadDaemonConfig().owners ?? []);
4416
+ await refreshOwnershipForAuthorization(selfAid);
4417
+ const currentOwners = new Set(getDaemonOwners());
4315
4418
  if (!currentOwners.has(message.triggerMeta.authorizedBy)) {
4316
4419
  throw new Error('fullaccess Trigger approval is no longer owned');
4317
4420
  }
@@ -4326,7 +4429,7 @@ export class ResponseEngine {
4326
4429
  authorizedBy: message.triggerMeta.authorizedBy,
4327
4430
  };
4328
4431
  }
4329
- this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent);
4432
+ await this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent);
4330
4433
  effectivePermissionMode = 'fullaccess';
4331
4434
  }
4332
4435
  else {
@@ -4394,7 +4497,10 @@ export class ResponseEngine {
4394
4497
  ? `${effortResolution.source.target}:${effortResolution.source.file}`
4395
4498
  : undefined;
4396
4499
  if (model || effort) {
4397
- evolcoreModelOverride = { model, effort };
4500
+ evolcoreModelOverride = {
4501
+ model,
4502
+ effort,
4503
+ };
4398
4504
  effectiveModel = model;
4399
4505
  }
4400
4506
  effectiveEffort = effectiveEffortValue;
@@ -4403,6 +4509,11 @@ export class ResponseEngine {
4403
4509
  logger.warn(`[ResponseEngine] effective model config resolution failed: ${e instanceof Error ? e.message : String(e)}`);
4404
4510
  }
4405
4511
  modelOverride = evolcoreModelOverride;
4512
+ primaryProvider = normalizedBaseagent.canonical === 'unknown'
4513
+ ? undefined
4514
+ : effectiveAgentConfig?.baseagents?.[normalizedBaseagent.canonical]?.provider;
4515
+ if (primaryProvider)
4516
+ modelOverride = { ...(modelOverride || {}), provider: primaryProvider };
4406
4517
  if (triggerModelOverride || triggerEffortOverride) {
4407
4518
  const constrainedOverride = { ...(modelOverride || {}) };
4408
4519
  if (triggerModelOverride) {
@@ -4448,6 +4559,7 @@ export class ResponseEngine {
4448
4559
  selfAid: selfAid || undefined,
4449
4560
  catalog: agent.getModelCatalog?.bind(agent),
4450
4561
  baseagent: normalizedBaseagent.canonical,
4562
+ currentProvider: primaryProvider,
4451
4563
  currentModel: primaryModel,
4452
4564
  resolveModelId: typeof agent.resolveModelId === 'function'
4453
4565
  ? agent.resolveModelId.bind(agent)
@@ -4464,7 +4576,9 @@ export class ResponseEngine {
4464
4576
  if (existingFallback
4465
4577
  && (existingFallback.baseagent !== normalizedBaseagent.canonical
4466
4578
  || existingFallback.primaryModel !== primaryModel
4467
- || !fallbackCandidates.some(candidate => candidate.model === existingFallback.activeFallback.model))) {
4579
+ || existingFallback.primaryProvider !== primaryProvider
4580
+ || !fallbackCandidates.some(candidate => candidate.model === existingFallback.activeFallback.model
4581
+ && candidate.provider === existingFallback.activeFallback.provider))) {
4468
4582
  this.modelFallbackMap.delete(session.id);
4469
4583
  }
4470
4584
  else if (existingFallback) {
@@ -4509,7 +4623,7 @@ export class ResponseEngine {
4509
4623
  if (normalizedBaseagent.canonical === 'claude') {
4510
4624
  modelOverride = {
4511
4625
  ...(modelOverride || {}),
4512
- sessionTitle: deriveSessionTitle(session.name, message.content, session.threadId),
4626
+ sessionTitle: deriveSessionTitle(session.name, message.content, sessionTopicThreadId(session)),
4513
4627
  };
4514
4628
  }
4515
4629
  // Capture the authoritative topic binding after begin() has durably
@@ -4518,10 +4632,10 @@ export class ResponseEngine {
4518
4632
  // happen before task-start auto compact because compact is a backend
4519
4633
  // management action just like the subsequent runQuery.
4520
4634
  let runAgentSessionId = session.agentSessionId;
4521
- if ((session.threadId || message.taskRecovery)
4635
+ if ((isTopicSession(session) || message.taskRecovery)
4522
4636
  && typeof this.sessionManager.getSessionById === 'function') {
4523
4637
  const latestBackendSession = await this.sessionManager.getSessionById(session.id);
4524
- if (!latestBackendSession || latestBackendSession.deletedAt || latestBackendSession.threadId !== session.threadId) {
4638
+ if (!latestBackendSession || latestBackendSession.deletedAt || sessionLocalTopicId(latestBackendSession) !== sessionLocalTopicId(session)) {
4525
4639
  throw new Error('topic session changed before backend run started');
4526
4640
  }
4527
4641
  if (message.taskRecovery && (latestBackendSession.agentSessionId !== message.taskRecovery.agentSessionId
@@ -4537,7 +4651,7 @@ export class ResponseEngine {
4537
4651
  ...(message.taskRecovery
4538
4652
  ? { recovery: { agentSessionId: message.taskRecovery.agentSessionId } }
4539
4653
  : {}),
4540
- ...(session.threadId
4654
+ ...(isTopicSession(session)
4541
4655
  ? { backend: { kind: 'topic', agentSessionId: runAgentSessionId ?? null } }
4542
4656
  : {}),
4543
4657
  turn: {
@@ -4549,10 +4663,10 @@ export class ResponseEngine {
4549
4663
  },
4550
4664
  };
4551
4665
  const refreshTopicBindingForLogicalRetry = async () => {
4552
- if ((!session.threadId && !message.taskRecovery) || typeof this.sessionManager.getSessionById !== 'function')
4666
+ if ((isMainSession(session) && !message.taskRecovery) || typeof this.sessionManager.getSessionById !== 'function')
4553
4667
  return;
4554
4668
  const latest = await this.sessionManager.getSessionById(session.id);
4555
- if (!latest || latest.threadId !== session.threadId) {
4669
+ if (!latest || sessionLocalTopicId(latest) !== sessionLocalTopicId(session)) {
4556
4670
  throw new Error('topic session changed during backend run');
4557
4671
  }
4558
4672
  const latestAgentSessionId = latest.agentSessionId;
@@ -4574,9 +4688,7 @@ export class ResponseEngine {
4574
4688
  };
4575
4689
  // 预压缩必须使用与本轮 runQuery 相同的有效模型/强度覆盖。
4576
4690
  // 模型解析完成后再执行,避免关系级模型存在时回落到 runner 默认模型。
4577
- await this.runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, renderer, modelOverride, fullAccessAuthorization
4578
- ? () => this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent)
4579
- : undefined);
4691
+ await this.runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, renderer, modelOverride, () => this.assertCurrentExecutionAuthorization(permissionContext, agent));
4580
4692
  startTime = Date.now();
4581
4693
  const causationPath = inputCausation.trigger?.path ?? [];
4582
4694
  const originNode = causationPath[0];
@@ -4644,8 +4756,6 @@ export class ResponseEngine {
4644
4756
  RELATIONS_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'relations') : undefined,
4645
4757
  VENUES_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'venues') : undefined,
4646
4758
  selfAid: selfAid || undefined,
4647
- // ownerAid:当前 agent 的第一个 owner AID,用于 owner 作用域记忆路径展开。
4648
- // 来源:getFirstStaticAgentOwner 读 config.json owners[0];AID 格式不合法时置 undefined。
4649
4759
  ownerAid: (() => {
4650
4760
  const raw = selfAid ? getFirstStaticAgentOwner(selfAid) : undefined;
4651
4761
  if (!raw)
@@ -4709,7 +4819,7 @@ export class ResponseEngine {
4709
4819
  localDate: currentLocalDate(),
4710
4820
  weekday: currentWeekday(),
4711
4821
  osInfo: OS_INFO,
4712
- threadId: session.threadId || undefined,
4822
+ threadId: sessionTopicThreadId(session),
4713
4823
  // Stage 3: sessionKey 持久化字段
4714
4824
  sessionKey: session.sessionKey,
4715
4825
  chatMode: isProactive ? 'proactive' : 'interactive',
@@ -5006,7 +5116,7 @@ export class ResponseEngine {
5006
5116
  executionSource: fullAccessAuthorization.source,
5007
5117
  } : {}),
5008
5118
  daemonRuntimeEpoch: this.agentDelegationRegistry?.getRuntimeEpoch(),
5009
- threadId: session.threadId || undefined,
5119
+ threadId: sessionTopicThreadId(session),
5010
5120
  sessionRuntimeDir,
5011
5121
  runtimeLockDir: ensureRuntimeLockDir(sessionRuntimeDir),
5012
5122
  handoffIds: v2HandoffIds.length > 0 && v2HandoffDirection === 'target' ? v2HandoffIds : undefined,
@@ -5038,7 +5148,7 @@ export class ResponseEngine {
5038
5148
  }
5039
5149
  const retryScheduler = new RetryScheduler(fallbackCandidates, modelOverride?.model || agentModel, model => (typeof agent.resolveModelId === 'function'
5040
5150
  ? (agent.resolveModelId(model) ?? model)
5041
- : model));
5151
+ : model), modelOverride?.provider ?? primaryProvider);
5042
5152
  const apiRetryTimeoutMs = this.apiRetryTimeoutMs();
5043
5153
  const retryInputIsAtMostOnce = agent.retryInputSemantics === 'at_most_once';
5044
5154
  let runAttempt = 1;
@@ -5060,10 +5170,10 @@ export class ResponseEngine {
5060
5170
  const shouldTimeoutRetryAttempt = retryScheduler.shouldTimeoutAttempt() && apiRetryTimeoutMs !== undefined;
5061
5171
  let attemptTimeout;
5062
5172
  try {
5063
- logger.info(`[ResponseEngine] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${runAttempt} apiRetries=${retryScheduler.apiRetryAttemptsMade} agentSessionId=${runAgentSessionId ?? 'none'} topicBinding=${session.threadId ? 'authoritative' : 'legacy'}`);
5173
+ logger.info(`[ResponseEngine] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${runAttempt} apiRetries=${retryScheduler.apiRetryAttemptsMade} agentSessionId=${runAgentSessionId ?? 'none'} topicBinding=${isTopicSession(session) ? 'authoritative' : 'legacy'}`);
5064
5174
  import('../../utils/msg-diag.js').then(m => m.diagLog('task_start', { aid: session.selfAID ?? '', msgId: message.messageId ?? '', taskId, sessionId: runAgentSessionId ?? undefined, text: message.content ?? '' })).catch(() => { });
5175
+ await this.assertCurrentExecutionAuthorization(permissionContext, agent);
5065
5176
  if (fullAccessAuthorization) {
5066
- this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent);
5067
5177
  if (fullAccessAuditStartedAt === undefined) {
5068
5178
  fullAccessAuditProcessRole = 'fullaccess-run';
5069
5179
  fullAccessAuditStartedAt = Date.now();
@@ -5093,7 +5203,7 @@ export class ResponseEngine {
5093
5203
  // Audit is synchronous today, but it is an observable boundary.
5094
5204
  // Revalidate once more so an administrative revocation observed
5095
5205
  // there cannot be followed by a privileged Runner call.
5096
- this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent);
5206
+ await this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent);
5097
5207
  }
5098
5208
  fullAccessRunnerInvoked = true;
5099
5209
  }
@@ -5255,7 +5365,7 @@ export class ResponseEngine {
5255
5365
  if (classifyError(retryError) === ErrorType.MODEL_UNAVAILABLE && !hasExplicitTriggerModel) {
5256
5366
  if (probingPrimaryModel && previousFallbackState) {
5257
5367
  const resumedFallback = previousFallbackState.activeFallback;
5258
- retryScheduler.markModelAttempted(resumedFallback.model);
5368
+ retryScheduler.markModelAttempted(resumedFallback.model, resumedFallback.provider ?? primaryProvider);
5259
5369
  modelOverride = withFallbackModel(modelOverride, resumedFallback);
5260
5370
  usedFallback = resumedFallback;
5261
5371
  skipEvolcoreModel = true;
@@ -5277,6 +5387,7 @@ export class ResponseEngine {
5277
5387
  this.modelFallbackMap.set(session.id, {
5278
5388
  baseagent: normalizedBaseagent.canonical,
5279
5389
  primaryModel,
5390
+ primaryProvider,
5280
5391
  activeFallback: modelFallback.candidate,
5281
5392
  messagesSinceFallback: 0,
5282
5393
  nextProbeAt: 2,
@@ -5293,7 +5404,7 @@ export class ResponseEngine {
5293
5404
  const candidateHint = fallbackCandidates.length === 0
5294
5405
  ? '未配置可用备用模型'
5295
5406
  : '备用模型均不可用';
5296
- throw markModelFallbackExhausted(new Error(`model_not_found: ${primaryModel ?? 'runner_default'}`), `当前模型 ${primaryModel ?? '默认模型'} 不可用,${candidateHint}。请稍后重试;管理员可在 agents/defaults.json 的 baseagents.${normalizedBaseagent.canonical}.fallbackModels 配置同类备用模型。`, retryError);
5407
+ throw markModelFallbackExhausted(new Error(`model_not_found: ${primaryModel ?? 'runner_default'}`), `当前模型 ${primaryModel ?? '默认模型'} 不可用,${candidateHint}。请稍后重试;管理员可在 agents/defaults.json 的 baseagents.${normalizedBaseagent.canonical}.fallback 配置同类备用模型。`, retryError);
5297
5408
  }
5298
5409
  if (retryAttemptTimedOut || isRetryableError(retryError)) {
5299
5410
  await recordRetryHealthError(retryError);
@@ -5350,9 +5461,7 @@ export class ResponseEngine {
5350
5461
  permissionMode: effectivePermissionMode,
5351
5462
  turnLease,
5352
5463
  messagePromptContext: fullAccessMessagePromptContext,
5353
- beforeRunQuery: fullAccessAuthorization
5354
- ? () => this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent)
5355
- : undefined,
5464
+ beforeRunQuery: () => this.assertCurrentExecutionAuthorization(permissionContext, agent),
5356
5465
  });
5357
5466
  }
5358
5467
  else {
@@ -5397,9 +5506,7 @@ export class ResponseEngine {
5397
5506
  permissionMode: effectivePermissionMode,
5398
5507
  turnLease,
5399
5508
  messagePromptContext: fullAccessMessagePromptContext,
5400
- beforeRunQuery: fullAccessAuthorization
5401
- ? () => this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent)
5402
- : undefined,
5509
+ beforeRunQuery: () => this.assertCurrentExecutionAuthorization(permissionContext, agent),
5403
5510
  });
5404
5511
  // 重试后仍然 prompt_too_long:显示友好提示
5405
5512
  const retryStillTooLong = streamResult.isError && streamHitContextLimit(streamResult);
@@ -5608,6 +5715,9 @@ export class ResponseEngine {
5608
5715
  await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_turn', taskCausation);
5609
5716
  return;
5610
5717
  }
5718
+ const streamRecoverable = !permissionTerminalFailure
5719
+ && !isExpectedTerminalInterrupt(streamResult.terminalReason)
5720
+ && isRetryableError(new Error(getStreamErrorText(streamResult)));
5611
5721
  if ((visibleFinalReplyText || streamResult.syntheticDiagnosticText) && !isTriggerNoop) {
5612
5722
  // `hasReceivedText` 是任务级计数。工具调用前的文字已经显示,并不表示最后一轮
5613
5723
  // (通常来自 complete.result)的文字也已显示;用本轮投影标记避免终态丢失。
@@ -5623,7 +5733,6 @@ export class ResponseEngine {
5623
5733
  const errorText = visibleFinalReplyText || streamResult.syntheticDiagnosticText || getStreamErrorMessage(streamResult, false);
5624
5734
  if (streamResult.isError) {
5625
5735
  // 用原始诊断文本(含错误码)判断可重试性,不用已过 stripContextLimitErrorLines 的展示文本
5626
- const recoverable = isRetryableError(new Error(getStreamErrorText(streamResult)));
5627
5736
  const subtype = normalizeCoreErrorSubtype(new Error(streamRawErrorText || getStreamErrorText(streamResult)), streamResult.subtype, streamResult.terminalReason);
5628
5737
  const reason = normalizeCoreErrorReason(streamResult.terminalReason);
5629
5738
  await adapter.send(envelope, {
@@ -5632,7 +5741,7 @@ export class ResponseEngine {
5632
5741
  ...(streamRawErrorText ? { raw: streamRawErrorText } : {}),
5633
5742
  subtype,
5634
5743
  ...(reason ? { reason } : {}),
5635
- recoverable,
5744
+ recoverable: streamRecoverable,
5636
5745
  }).catch(() => { });
5637
5746
  sentErrorAsSystemError = true;
5638
5747
  }
@@ -5673,11 +5782,22 @@ export class ResponseEngine {
5673
5782
  // Flush 剩余内容(文件标记已在 flush 时自动移除)
5674
5783
  // A final text flush can also contain failed output. Preserve the
5675
5784
  // outcome so AUN does not label that text as normal completion.
5676
- if (streamResult.isError && !isExpectedTerminalInterrupt(streamResult.terminalReason)) {
5677
- await renderer.flush(true, 'failed');
5785
+ try {
5786
+ if (streamResult.isError && !isExpectedTerminalInterrupt(streamResult.terminalReason)) {
5787
+ await renderer.flush(true, 'failed');
5788
+ }
5789
+ else {
5790
+ await renderer.flush(!isExpectedTerminalInterrupt(streamResult.terminalReason));
5791
+ }
5678
5792
  }
5679
- else {
5680
- await renderer.flush(!isExpectedTerminalInterrupt(streamResult.terminalReason));
5793
+ catch (flushError) {
5794
+ // Agent 已正常跑完,只是最终回复投递失败:打标记,让 catch 记录
5795
+ // reply_delivery_failed,__task-recovery 不再为投递问题重跑 agent。
5796
+ if (streamResult.isError)
5797
+ throw flushError;
5798
+ const tagged = flushError instanceof Error ? flushError : new Error(String(flushError));
5799
+ tagged._replyDeliveryFailed = true;
5800
+ throw tagged;
5681
5801
  }
5682
5802
  }
5683
5803
  const terminalInterrupt = isExpectedTerminalInterrupt(streamResult.terminalReason);
@@ -5702,7 +5822,7 @@ export class ResponseEngine {
5702
5822
  }
5703
5823
  else {
5704
5824
  await this.turnCoordinator.finish(session, turnLease, terminalInterrupt ? 'cancelled' : 'failed', permissionTerminalFailure ? 'permission_denied'
5705
- : streamResult.protocolIncompleteReason || streamResult.terminalReason || streamResult.subtype);
5825
+ : streamResult.protocolIncompleteReason || streamResult.terminalReason || streamResult.subtype, streamRecoverable);
5706
5826
  }
5707
5827
  // 注意:不在此处清除 interruptedSessions,由下一条消息的 prompt 包装逻辑消费
5708
5828
  const interruptReason = this.interruptedSessions.get(session.id);
@@ -5730,6 +5850,7 @@ export class ResponseEngine {
5730
5850
  if (!isExpectedInterrupt && !permissionTerminalFailure) {
5731
5851
  const terminalMetadata = {
5732
5852
  errorType: rawSubtype,
5853
+ recoverable: streamRecoverable,
5733
5854
  message: userErrorSummary,
5734
5855
  durationMs: Date.now() - startTime,
5735
5856
  numTurns: streamResult.numTurns,
@@ -5748,6 +5869,7 @@ export class ResponseEngine {
5748
5869
  await adapter.send(envelope, {
5749
5870
  kind: 'result.error',
5750
5871
  text: userErrorSummary,
5872
+ recoverable: streamRecoverable,
5751
5873
  ...(streamRawErrorText ? { raw: streamRawErrorText } : {}),
5752
5874
  subtype: coreSubtype,
5753
5875
  ...(coreReason ? { reason: coreReason } : {}),
@@ -5759,6 +5881,7 @@ export class ResponseEngine {
5759
5881
  await adapter.send(envelope, {
5760
5882
  kind: 'result.error',
5761
5883
  text: userErrorSummary,
5884
+ recoverable: streamRecoverable,
5762
5885
  ...(streamRawErrorText ? { raw: streamRawErrorText } : {}),
5763
5886
  subtype: coreSubtype,
5764
5887
  ...(coreReason ? { reason: coreReason } : {}),
@@ -5795,7 +5918,7 @@ export class ResponseEngine {
5795
5918
  else {
5796
5919
  logger.error(`[ResponseEngine] Task failed: session=${session.id} task=${taskId} ` +
5797
5920
  `type=${errorType} terminalReason=${streamResult.terminalReason ?? 'none'} error=${errorSummary}`);
5798
- this.publishTriggerExecutionFailure(message, errorSummary, { messageId, causation: taskCausation });
5921
+ this.publishTriggerExecutionFailure(message, errorSummary, { messageId, causation: taskCausation, recoverable: streamRecoverable });
5799
5922
  this.eventBus.publish({
5800
5923
  type: 'task:error',
5801
5924
  sessionId: session.id,
@@ -5806,6 +5929,7 @@ export class ResponseEngine {
5806
5929
  taskId,
5807
5930
  generation: turnLease?.generation,
5808
5931
  error: errorSummary,
5932
+ recoverable: streamRecoverable,
5809
5933
  errorType,
5810
5934
  agentName: agentNameForStats,
5811
5935
  terminalReason: streamResult.terminalReason,
@@ -5874,7 +5998,7 @@ export class ResponseEngine {
5874
5998
  // 写库即得价格对(原价 + 网关价),无需再调 calcCost 重复计算。
5875
5999
  // 网关价格缓存(/v1/models 的 pricing/effective_pricing,1h TTL)由当前 runner 提供。
5876
6000
  const gwPricing = agent.getGatewayPricing?.();
5877
- const prices = insertUsageEvent(resolveRoot(), event, gwPricing);
6001
+ const prices = insertUsageEvent(resolveRoot(), event, gwPricing, streamResult.modelCalls);
5878
6002
  turnCost = { official: prices.official, gateway: prices.gateway };
5879
6003
  // 逐次大模型调用明细落库(model_calls 表)
5880
6004
  if (streamResult.modelCalls?.length) {
@@ -5887,6 +6011,8 @@ export class ResponseEngine {
5887
6011
  peer_key: statsPeerKey,
5888
6012
  call_index: mc.call_index,
5889
6013
  model: mc.model || statsModel,
6014
+ source: mc.source,
6015
+ provider_thread_id: mc.providerThreadId,
5890
6016
  request_id: mc.request_id,
5891
6017
  message_id: mc.message_id,
5892
6018
  input_tokens: mc.tokenUsage.input_tokens ?? 0,
@@ -6127,6 +6253,8 @@ export class ResponseEngine {
6127
6253
  && !isTotalExecutionTimeout;
6128
6254
  const isUserInterrupt = isNonErrorInterruptReason(interruptReason) || staleSupersededTurn;
6129
6255
  const errorMsg = error instanceof Error ? error.message : String(error);
6256
+ const recoverable = !isUserInterrupt && !error?._replyDeliveryFailed
6257
+ && (error === timeoutControl?.error ? timeoutControl?.recoverable === true : isRetryableError(error));
6130
6258
  if (isUserInterrupt) {
6131
6259
  await this.publishTriggerExecutionInterrupted(message, adapter, envelope, interruptReason ?? 'stale_turn', taskCausation);
6132
6260
  }
@@ -6134,10 +6262,14 @@ export class ResponseEngine {
6134
6262
  this.publishTriggerExecutionFailure(message, errorMsg, {
6135
6263
  messageId,
6136
6264
  causation: taskCausation,
6265
+ recoverable,
6137
6266
  });
6138
6267
  }
6139
6268
  if (leaseWasCurrent && turnLease) {
6140
- await this.turnCoordinator.finish(session, turnLease, 'failed', error instanceof Error ? error.message : String(error));
6269
+ const failureReason = error instanceof Error ? error.message : String(error);
6270
+ await this.turnCoordinator.finish(session, turnLease, 'failed', error?._replyDeliveryFailed
6271
+ ? `${REPLY_DELIVERY_FAILED_REASON}: ${failureReason}`
6272
+ : failureReason, recoverable);
6141
6273
  }
6142
6274
  const totalExecutionMs = error instanceof ExecutionTimeoutError ? error.timeoutMs : this.totalExecutionLimitMs();
6143
6275
  const procStatus = errType === ErrorType.SDK_TIMEOUT || isTotalExecutionTimeout ? 'timeout'
@@ -6184,6 +6316,7 @@ export class ResponseEngine {
6184
6316
  taskId,
6185
6317
  generation: turnLease?.generation,
6186
6318
  error: errorMsg,
6319
+ recoverable,
6187
6320
  terminalReason: isExecutionFailure(error) ? error.reason : undefined,
6188
6321
  errorType,
6189
6322
  agentName: agentNameForStats,
@@ -6275,6 +6408,7 @@ export class ResponseEngine {
6275
6408
  const errorPayload = {
6276
6409
  kind: 'result.error',
6277
6410
  text: userMessage,
6411
+ recoverable,
6278
6412
  subtype: errorSubtype,
6279
6413
  ...(rawErrorText ? { raw: rawErrorText } : {}),
6280
6414
  ...(errorReason ? { reason: errorReason } : {}),
@@ -6289,6 +6423,7 @@ export class ResponseEngine {
6289
6423
  metadata: {
6290
6424
  errorType: errType,
6291
6425
  message: userMessage,
6426
+ recoverable,
6292
6427
  durationMs: Date.now() - startTime,
6293
6428
  numTurns: streamResult.numTurns,
6294
6429
  toolCallCount: streamResult.toolUseCount ?? 0,
@@ -6428,7 +6563,7 @@ export class ResponseEngine {
6428
6563
  // fullaccess run. Revalidate immediately before invoking the runner so a
6429
6564
  // daemon gate/owner revocation observed after task setup cannot still
6430
6565
  // cause host-level runner activity.
6431
- beforeCompact?.();
6566
+ await beforeCompact?.();
6432
6567
  const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath, modelOverride);
6433
6568
  const compactResult = compacted;
6434
6569
  if (compactResult === true || compactResult?.ok === true) {
@@ -6507,7 +6642,7 @@ export class ResponseEngine {
6507
6642
  if (!message.observerHintId || !bound || bound.deletedAt || !bound.agentSessionId
6508
6643
  || bound.channel !== message.channel || bound.channelId !== message.channelId
6509
6644
  || bound.selfAID !== message.selfAID || bound.chatType !== message.chatType
6510
- || (bound.threadId || '') !== (message.threadId || '')) {
6645
+ || sessionTopicThreadId(bound) !== (isTopicThreadId(message.threadId) ? message.threadId : undefined)) {
6511
6646
  throw new Error('Observer hint target session is no longer available');
6512
6647
  }
6513
6648
  // An enqueued priority hint is bound to exactly the session selected at
@@ -6515,7 +6650,7 @@ export class ResponseEngine {
6515
6650
  // file must never silently resume a stale/different backend session.
6516
6651
  const current = await this.observerTargetSession({
6517
6652
  channelKey: bound.channel, channelId: bound.channelId,
6518
- chatType: bound.chatType, threadId: bound.threadId,
6653
+ chatType: bound.chatType, threadId: sessionTopicThreadId(bound),
6519
6654
  selfAid: bound.selfAID,
6520
6655
  });
6521
6656
  if (current?.id !== bound.id || current.agentSessionId !== bound.agentSessionId) {
@@ -6526,7 +6661,7 @@ export class ResponseEngine {
6526
6661
  ? bound.projectPath : path.resolve(process.cwd(), bound.projectPath) };
6527
6662
  }
6528
6663
  // 话题会话创建时写入创建者和 replyContext(threadId 路由);主会话不写(避免群聊覆盖)
6529
- const metadata = message.threadId
6664
+ const metadata = isTopicThreadId(message.threadId)
6530
6665
  ? {
6531
6666
  ...(message.replyContext ? { replyContext: message.replyContext } : {}),
6532
6667
  ...(message.peerId ? { peerId: message.peerId } : {}),
@@ -6558,7 +6693,7 @@ export class ResponseEngine {
6558
6693
  // valid mapped owner/admin to role=none at the execution boundary.
6559
6694
  if (resolvedIdentity)
6560
6695
  bound.identity = resolvedIdentity;
6561
- if (bound.threadId || bound.metadata?.diagnostic) {
6696
+ if (isTopicSession(bound) || bound.metadata?.diagnostic) {
6562
6697
  const absoluteProjectPath = path.isAbsolute(bound.projectPath)
6563
6698
  ? bound.projectPath : path.resolve(process.cwd(), bound.projectPath);
6564
6699
  return { session: bound, absoluteProjectPath };
@@ -6756,6 +6891,7 @@ export class ResponseEngine {
6756
6891
  if (hasText)
6757
6892
  textCount++;
6758
6893
  assistantUuid = event.assistantUuid ?? assistantUuid;
6894
+ event = { ...event, text: stripThinkingBlocks(event.text) };
6759
6895
  }
6760
6896
  else if (event.type === 'tool_use') {
6761
6897
  // 产生工具调用同样证明输入已被消费,且此时重放会重复发起副作用。
@@ -6827,6 +6963,12 @@ export class ResponseEngine {
6827
6963
  else if (event.type === 'complete') {
6828
6964
  assistantUuid = event.assistantUuid ?? assistantUuid;
6829
6965
  protocolIncompleteReason = event.protocolIncompleteReason ?? protocolIncompleteReason;
6966
+ if (event.result) {
6967
+ const result = stripThinkingBlocks(event.result);
6968
+ if (textCount === 0 && event.result.trim() && !result.trim())
6969
+ textCount++;
6970
+ event = { ...event, result };
6971
+ }
6830
6972
  }
6831
6973
  // 记录事件类型:高价值事件(含子任务生命周期)INFO,
6832
6974
  // 框架事件(session_id/state_changed/status)DEBUG
@@ -7341,7 +7483,7 @@ export class ResponseEngine {
7341
7483
  const isAbort = isExpectedTerminalInterrupt(event.terminalReason);
7342
7484
  logger.info(`[ResponseEngine] ${isAbort ? 'task interrupted' : 'complete event'}: queryFinal=${event.queryFinal !== false} isError=${event.isError} terminalReason=${event.terminalReason ?? 'none'} subtype=${event.subtype ?? 'none'} hasReceivedText=${hasReceivedText}`);
7343
7485
  // 自动回填会话名称
7344
- if (event.sessionTitle && shouldAutoFillSessionTitle(session.name, session.threadId)) {
7486
+ if (event.sessionTitle && shouldAutoFillSessionTitle(session.name, sessionTopicThreadId(session))) {
7345
7487
  await this.sessionManager.renameSession(session.id, event.sessionTitle);
7346
7488
  logger.info(`[ResponseEngine] Auto-filled session name: ${event.sessionTitle}`);
7347
7489
  }
@@ -7392,7 +7534,7 @@ export class ResponseEngine {
7392
7534
  continue;
7393
7535
  }
7394
7536
  // 自动回填会话名称
7395
- if (event.sessionTitle && shouldAutoFillSessionTitle(session.name, session.threadId)) {
7537
+ if (event.sessionTitle && shouldAutoFillSessionTitle(session.name, sessionTopicThreadId(session))) {
7396
7538
  await this.sessionManager.renameSession(session.id, event.sessionTitle);
7397
7539
  logger.info(`[ResponseEngine] Auto-filled session name: ${event.sessionTitle}`);
7398
7540
  }