evolcore 0.0.26 → 0.0.28

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 (177) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/bin/codex-managed-acl.ps1 +43 -0
  3. package/dist/agents/baseagent-availability.js +68 -0
  4. package/dist/agents/baseagent.js +10 -5
  5. package/dist/agents/claude-runner.js +123 -22
  6. package/dist/agents/codex/app-server-client.js +70 -26
  7. package/dist/agents/codex/catalog.js +7 -1
  8. package/dist/agents/codex-runner.js +363 -421
  9. package/dist/agents/ecagent-runner.js +170 -25
  10. package/dist/agents/gemini-runner.js +6 -0
  11. package/dist/agents/runner-types.js +50 -2
  12. package/dist/aun/aid/agentmd.js +73 -31
  13. package/dist/aun/aid/client.js +1 -1
  14. package/dist/aun/aid/control-aid.js +180 -1
  15. package/dist/aun/aid/identity.js +3 -3
  16. package/dist/aun/group/history.js +303 -0
  17. package/dist/aun/group/identity.js +43 -0
  18. package/dist/aun/group/index.js +5 -0
  19. package/dist/aun/{msg/group.js → group/operations.js} +161 -38
  20. package/dist/aun/{menu-client.js → menu/client.js} +13 -7
  21. package/dist/aun/menu/index.js +2 -0
  22. package/dist/aun/menu/transport.js +49 -0
  23. package/dist/aun/msg/index.js +2 -1
  24. package/dist/aun/msg/managed-operation.js +84 -18
  25. package/dist/aun/msg/mention-schema.js +51 -0
  26. package/dist/aun/msg/p2p.js +13 -8
  27. package/dist/aun/msg/upload.js +43 -88
  28. package/dist/aun/outbox.js +29 -7
  29. package/dist/aun/rpc/client.js +1 -0
  30. package/dist/aun/storage/client.js +39 -50
  31. package/dist/channels/aun.js +544 -239
  32. package/dist/channels/daemon.js +10 -6
  33. package/dist/cli/agent-command.js +108 -6
  34. package/dist/cli/agent-create.js +515 -2
  35. package/dist/cli/agent.js +109 -8
  36. package/dist/cli/aun-commands.js +191 -42
  37. package/dist/cli/config.js +8 -5
  38. package/dist/cli/contact.js +2 -2
  39. package/dist/cli/ctl-command.js +2 -2
  40. package/dist/cli/daemon-commands.js +111 -162
  41. package/dist/cli/diag.js +3 -3
  42. package/dist/cli/fs-command.js +3 -3
  43. package/dist/cli/handoff-command.js +6 -6
  44. package/dist/cli/index.js +8 -2
  45. package/dist/cli/init.js +34 -1
  46. package/dist/cli/managed-command-guard.js +2 -1
  47. package/dist/cli/menu-client.js +13 -5
  48. package/dist/cli/model.js +3 -3
  49. package/dist/cli/queue-command.js +2 -2
  50. package/dist/cli/response.js +2 -2
  51. package/dist/cli/restart-monitor.js +81 -19
  52. package/dist/cli/task-command.js +59 -0
  53. package/dist/cli/task-context.js +97 -49
  54. package/dist/cli/trigger-command.js +20 -10
  55. package/dist/config/builtin-roles.js +10 -0
  56. package/dist/config/config-field-policy.js +11 -0
  57. package/dist/config/config-manager.js +32 -4
  58. package/dist/config/config-operation-service.js +20 -5
  59. package/dist/config/contact-book-store.js +1 -1
  60. package/dist/config/contact-book.js +1 -1
  61. package/dist/config/contact-operation-service.js +1 -1
  62. package/dist/config/peer-role-resolver.js +1 -1
  63. package/dist/config/resolved-config-op.js +3 -0
  64. package/dist/config-store.js +1 -1
  65. package/dist/core/agent-application-service.js +5 -0
  66. package/dist/core/audit/event-key.js +17 -2
  67. package/dist/core/audit/log-integrity.js +34 -8
  68. package/dist/core/auth/agent-delegation.js +48 -14
  69. package/dist/core/auth/auth-gateway.js +2 -0
  70. package/dist/core/auth/authorization-audit.js +34 -38
  71. package/dist/core/auth/operation-authorizer.js +67 -5
  72. package/dist/core/auth/operation-catalog.js +110 -0
  73. package/dist/core/auth/remote-access-store.js +115 -9
  74. package/dist/core/bootstrap-messages.js +66 -7
  75. package/dist/core/bootstrap-service.js +110 -19
  76. package/dist/core/command/agent-control.js +88 -7
  77. package/dist/core/command/agent-storage-menu.js +443 -0
  78. package/dist/core/command/cli-intent-parser.js +19 -0
  79. package/dist/core/command/command-handler.js +105 -40
  80. package/dist/core/command/data-root-resolver.js +273 -0
  81. package/dist/core/command/diag-service.js +292 -27
  82. package/dist/core/command/group-menu.js +7 -2
  83. package/dist/core/command/menu-catalog.js +34 -4
  84. package/dist/core/command/menu-handler.js +364 -96
  85. package/dist/core/command/menu-protocol.js +43 -6
  86. package/dist/core/command/role-menu.js +2 -2
  87. package/dist/core/command/slash-handler.js +61 -37
  88. package/dist/core/event-catalog.js +4 -0
  89. package/dist/core/evolagent-registry.js +28 -2
  90. package/dist/core/group/invite-service.js +924 -0
  91. package/dist/core/interaction-router.js +22 -8
  92. package/dist/core/message/im-renderer.js +94 -16
  93. package/dist/core/message/message-bridge.js +140 -13
  94. package/dist/core/message/message-log.js +5 -3
  95. package/dist/core/message/message-queue.js +73 -41
  96. package/dist/core/message/response-engine.js +729 -118
  97. package/dist/core/permission/approval-gateway.js +36 -3
  98. package/dist/core/permission/ec-command-parser.js +76 -36
  99. package/dist/core/permission/protected-paths.js +134 -16
  100. package/dist/core/permission/sandbox-runtime.js +72 -3
  101. package/dist/core/permission/tool-error-code.js +15 -0
  102. package/dist/core/permission/tool-policy.js +8 -8
  103. package/dist/core/relation/peer-identity.js +57 -6
  104. package/dist/core/relation/shared-data.js +106 -0
  105. package/dist/core/role/runtime-policy.js +64 -0
  106. package/dist/core/runtime-lock.js +10 -26
  107. package/dist/core/session/runner-task-history.js +170 -0
  108. package/dist/core/session/session-manager.js +38 -6
  109. package/dist/core/session/session-mapper.js +41 -0
  110. package/dist/core/session/session-turn-coordinator.js +37 -7
  111. package/dist/core/session/session-turns.js +48 -7
  112. package/dist/core/task/task-recovery-service.js +176 -0
  113. package/dist/core/task/task-resume-operation.js +32 -0
  114. package/dist/eck/group-rules-sync.js +5 -3
  115. package/dist/eck/kit-renderer.js +1 -0
  116. package/dist/index.js +322 -207
  117. package/dist/ipc.js +141 -15
  118. package/dist/paths.js +17 -4
  119. package/dist/product.js +3 -0
  120. package/dist/trigger/event-source.js +19 -0
  121. package/dist/trigger/feedback.js +233 -40
  122. package/dist/trigger/parser.js +55 -5
  123. package/dist/trigger/patch.js +9 -0
  124. package/dist/trigger/scheduler.js +30 -4
  125. package/dist/trigger/validation.js +20 -3
  126. package/dist/utils/atomic-write.js +98 -0
  127. package/dist/utils/cross-platform.js +87 -16
  128. package/dist/utils/ecweb-utils.js +1 -0
  129. package/dist/utils/error-utils.js +40 -2
  130. package/dist/utils/logger.js +23 -13
  131. package/dist/utils/npm-ops.js +119 -4
  132. package/dist/utils/package-version-info.js +244 -0
  133. package/dist/utils/tool-summary.js +59 -3
  134. package/dist/utils/welcome.js +174 -1
  135. package/dist/utils/windows-shell-trust.js +37 -0
  136. package/ecagent/dist/harness/agent-harness.js +3 -0
  137. package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
  138. package/kits/docs/INDEX.md +0 -6
  139. package/kits/docs/channels/aun.md +23 -2
  140. package/kits/docs/channels/feishu.md +1 -1
  141. package/kits/docs/context-assembly.md +29 -17
  142. package/kits/docs/evolcore/INDEX.md +8 -3
  143. package/kits/docs/evolcore/agent-create.md +39 -7
  144. package/kits/docs/evolcore/agent.md +33 -1
  145. package/kits/docs/evolcore/aid.md +9 -1
  146. package/kits/docs/evolcore/config.md +40 -1
  147. package/kits/docs/evolcore/group-collaboration.md +1 -1
  148. package/kits/docs/evolcore/group.md +117 -2
  149. package/kits/docs/evolcore/menu-client.md +16 -0
  150. package/kits/docs/evolcore/msg.md +3 -0
  151. package/kits/docs/evolcore/storage.md +3 -0
  152. package/kits/docs/evolcore/task.md +14 -0
  153. package/kits/docs/evolcore/trigger.md +51 -3
  154. package/kits/docs/venues/group.md +5 -0
  155. package/kits/eck_manifest.json +27 -0
  156. package/kits/rules/01-overview.md +11 -7
  157. package/kits/rules/04-relation.md +2 -1
  158. package/kits/rules/05-venue.md +1 -1
  159. package/kits/rules/06-channel.md +6 -4
  160. package/kits/schemas/agent-config.schema.12.json +25 -1
  161. package/kits/schemas/daemon.schema.6.json +2 -2
  162. package/kits/schemas/defaults.schema.5.json +1 -1
  163. package/kits/schemas/relation-config.schema.1.json +1 -1
  164. package/kits/schemas/relation-config.schema.5.json +1 -1
  165. package/kits/schemas/role-config.schema.1.json +14 -0
  166. package/kits/templates/roles/member.json +5 -0
  167. package/kits/templates/roles/visitor.json +5 -0
  168. package/kits/templates/system-fragments/baseagent.md +1 -1
  169. package/kits/templates/system-fragments/bootstrap.md +26 -9
  170. package/kits/templates/system-fragments/channel.md +8 -44
  171. package/kits/templates/system-fragments/commands.md +10 -2
  172. package/kits/templates/system-fragments/group-unread-history.md +7 -0
  173. package/kits/templates/system-fragments/reply.md +56 -0
  174. package/package.json +7 -3
  175. package/dist/aun/group-identity.js +0 -9
  176. /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
  177. /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
@@ -1,9 +1,13 @@
1
+ import { loadAgent } from '../../config-store.js';
2
+ import { initializationBlocksTasks } from '../../cli/agent-create.js';
1
3
  import { createHash, randomBytes } from 'crypto';
4
+ import { agentStorageMenuService, isAgentStorageMenuRequest } from '../command/agent-storage-menu.js';
2
5
  import { logger } from '../../utils/logger.js';
3
6
  import { StreamDebouncer } from './stream-debouncer.js';
4
7
  import { appendMessageLog, appendMessageLogStrict, buildInboundEntry, isTransientProtocolMessage } from './message-log.js';
5
8
  import { buildEnvelope, isInteractionSendAccepted, sendInteractionPayload } from './message-utils.js';
6
9
  import { chatDirPath } from '../session/session-fs-store.js';
10
+ import { readTopicLable } from '../session/session-mapper.js';
7
11
  import { formatChannelKey, tryParseChannelKey } from '../channel-loader.js';
8
12
  import { agentDir, resolvePaths } from '../../paths.js';
9
13
  import { listStaticAgentAdmins, listStaticAgentOwners, resolvePeerRoleDetail, } from '../../config/peer-role-resolver.js';
@@ -27,7 +31,9 @@ import { renderActionAsText } from '../interaction-router.js';
27
31
  import { planApprovalRoute } from '../permission/approval-gateway.js';
28
32
  import { guardGroupSlashCommand, parseFullAccessCommand } from '../command/slash-gate.js';
29
33
  import { isFullAccessEnabled } from '../../config-store.js';
34
+ import { createAgentPolicyAuthorization } from '../role/runtime-policy.js';
30
35
  import { auditFullAccessEvent } from '../auth/authorization-audit.js';
36
+ import { DIAGNOSTIC_AGENT_AID, DiagnosticSessionRouter, parseDiagnosticEvent, isDiagnosticSessionActive } from '../command/diag-service.js';
31
37
  import { consumeAdmissionReplyBudget, clearAdmissionReplyBudgets, evaluateInboundAdmissionPreflight, evaluateOrdinaryInboundAdmission, } from './message-admission.js';
32
38
  const CONTACT_REJECTION_NOTICE = '[rejection] Not accepting messages from you.';
33
39
  const REJECTABLE_PEER_TYPES = new Set(['ai', 'bot', 'agent', 'service']);
@@ -110,9 +116,11 @@ export class MessageBridge {
110
116
  menuTokenStore = new MenuTokenStore();
111
117
  handoffRuntime;
112
118
  sessionRenewService;
119
+ diagnosticRouter;
113
120
  aidStatsCollector;
114
121
  contactBindRuntimeChecker;
115
122
  interactionRouter;
123
+ groupInviteReviewService;
116
124
  constructor(defaultProjectPath, sessionManager, processor, messageQueue, cmdHandler, eventBus, defaultDebounce, processOwnersProvider) {
117
125
  this.defaultProjectPath = defaultProjectPath;
118
126
  this.sessionManager = sessionManager;
@@ -123,6 +131,13 @@ export class MessageBridge {
123
131
  this.processOwnersProvider = processOwnersProvider;
124
132
  this.defaultDebounce = defaultDebounce ?? 0;
125
133
  this.sessionRenewService = new SessionRenewService(sessionManager, processor, { eventBus });
134
+ this.diagnosticRouter = new DiagnosticSessionRouter(sessionManager, async (sessionId) => {
135
+ // Revoke in the router first, then clear pending work before awaiting the
136
+ // runner interruption. Delayed debounce and recovery are fenced below.
137
+ this.messageQueue.clearBySession(sessionId);
138
+ this.messageQueue.markActiveTerminal(sessionId, 'stop');
139
+ await this.processor.interruptSession(sessionId, 'stop');
140
+ });
126
141
  }
127
142
  /** Inject EvolAgentRegistry so owner lookups/writes route to agent.json for agent-owned channels. */
128
143
  setAgentRegistry(registry) {
@@ -144,6 +159,12 @@ export class MessageBridge {
144
159
  setInteractionRouter(router) {
145
160
  this.interactionRouter = router;
146
161
  }
162
+ setGroupInviteReviewService(service) {
163
+ this.groupInviteReviewService = service;
164
+ }
165
+ registerGroupInviteReviewChannel(binding) {
166
+ this.groupInviteReviewService?.attach(binding);
167
+ }
147
168
  /**
148
169
  * AUN supplies an authenticated sender AID, while the IM-side code proves
149
170
  * possession of the external account. This command only creates that
@@ -483,7 +504,7 @@ export class MessageBridge {
483
504
  ...(msg.replyContext ?? {}),
484
505
  metadata: {
485
506
  ...(msg.replyContext?.metadata ?? {}),
486
- ...evolMenuResponseTransportMetadata(),
507
+ ...evolMenuResponseTransportMetadata(msg.protectedHeaders?.sdk_version),
487
508
  },
488
509
  },
489
510
  }), {
@@ -795,6 +816,20 @@ export class MessageBridge {
795
816
  }
796
817
  return;
797
818
  }
819
+ if (!trustedHandoffEcho && selfAid && actorId && this.groupInviteReviewService) {
820
+ const inviteCommand = await this.groupInviteReviewService.handleCommand({
821
+ selfAid,
822
+ ownerAid: actorId,
823
+ channelKey,
824
+ channelType: resolvedChannelType,
825
+ chatType,
826
+ content,
827
+ });
828
+ if (inviteCommand.handled) {
829
+ await this.sendAdmissionControlReply(admissionPreflight.context, msg, sendReply, inviteCommand.text ?? '群邀请命令已处理。');
830
+ return;
831
+ }
832
+ }
798
833
  if (!trustedHandoffEcho && await this.handleReservedContactControl({
799
834
  context: admissionPreflight.context,
800
835
  msg,
@@ -945,6 +980,37 @@ export class MessageBridge {
945
980
  await this.sendAccessDenied(adapter, channelName, msg, sendReply);
946
981
  return;
947
982
  }
983
+ // Diagnostic invitations carry their own execution boundary. Resolve
984
+ // that boundary before ordinary command/session routing so a new case
985
+ // cannot inherit an older Codex thread.
986
+ let diagnosticSession;
987
+ if (selfAid === DIAGNOSTIC_AGENT_AID && resolvedChannelType === 'aun'
988
+ && chatType === 'private' && msg.source !== 'handoff') {
989
+ try {
990
+ const diagnosticEvent = parseDiagnosticEvent(content);
991
+ if (diagnosticEvent) {
992
+ releaseAdmission = await this.acquireAdmission(admissionRoute);
993
+ const routed = await this.diagnosticRouter.route({
994
+ event: diagnosticEvent, message: msg, channel: channelName,
995
+ channelType: resolvedChannelType, selfAid,
996
+ chatDir: chatDirPath(resolvePaths().sessionsDir, resolvedChannelType, msg.channelId, selfAid, channelName),
997
+ projectPath: owningAgent?.projectPath ?? this.defaultProjectPath,
998
+ baseagent: owningAgent?.baseagent,
999
+ });
1000
+ if ('notice' in routed) {
1001
+ await sendReply(msg.channelId, routed.notice, msg.replyContext);
1002
+ return;
1003
+ }
1004
+ diagnosticSession = routed.session;
1005
+ diagnosticSession.identity = identity;
1006
+ }
1007
+ }
1008
+ catch (error) {
1009
+ logger.warn(`[MessageBridge] Diagnostic admission failed: ${error instanceof Error ? error.message : 'unknown error'}`);
1010
+ await sendReply(msg.channelId, '诊断会话隔离未完成,本次未执行诊断。请检查诊断端日志后重试。', msg.replyContext);
1011
+ return;
1012
+ }
1013
+ }
948
1014
  // /fa is an execution prefix, not a session-mode switch or a normal
949
1015
  // quick command. Only this trusted bridge path can construct the
950
1016
  // internal per-call authorization carried into ResponseEngine.
@@ -1027,6 +1093,11 @@ export class MessageBridge {
1027
1093
  ...(chatType === 'private' ? { routeId: msg.channelId } : {}),
1028
1094
  });
1029
1095
  }
1096
+ const initializationConfig = selfAid ? loadAgent(selfAid) : null;
1097
+ if (initializationConfig && initializationBlocksTasks(initializationConfig)) {
1098
+ await sendReply(msg.channelId, 'Agent 初始化尚未完成,请由创建者查询 ec agent show 或恢复初始化。');
1099
+ return;
1100
+ }
1030
1101
  // 1. owner 绑定(按实例名绑定)
1031
1102
  if (adapter && actorId && roleDetail.effectiveRole === 'owner') {
1032
1103
  await this.bootstrapService?.tryStartBootstrap({
@@ -1106,6 +1177,14 @@ export class MessageBridge {
1106
1177
  return sendReply(msg.channelId, text, msg.replyContext);
1107
1178
  }, msg.peerId, msg.threadId, msg.chatType, msg.source, msg.replyContext, msg.messageId, msg.selfAID, authSubject))
1108
1179
  return;
1180
+ // Commands retain their normal route. Only authenticated direct model
1181
+ // input can obtain the Agent policy's per-call authorization.
1182
+ if (!executionPermissionOverride && selfAid && owningAgent?.aid === selfAid) {
1183
+ executionPermissionOverride = createAgentPolicyAuthorization({
1184
+ agentAid: selfAid, actor: roleDetail.actor, chatType,
1185
+ source: msg.source, peerType: msg.peerType,
1186
+ });
1187
+ }
1109
1188
  // 3. session 解析(使用 Channel 层填充的 chatType)
1110
1189
  if (!(await this.canCreateThreadSession(channelName, msg, chatType))) {
1111
1190
  // 静默丢弃:绝不向群里注入回复。
@@ -1147,9 +1226,10 @@ export class MessageBridge {
1147
1226
  // transaction. Without this per-route single-flight, a second message
1148
1227
  // can observe the just-renewed empty session before the first message
1149
1228
  // has been appended and create another renewed session.
1150
- releaseAdmission = await this.acquireAdmission(admissionRoute);
1229
+ if (!releaseAdmission)
1230
+ releaseAdmission = await this.acquireAdmission(admissionRoute);
1151
1231
  const hadMainSession = msg.threadId ? true : this.sessionManager.hasMainSession(channelName, msg.channelId, msg.channelType || effectiveChannelType, msg.selfAID || selfAid);
1152
- let session = await this.sessionManager.getOrCreateSession(channelName, msg.channelId, effectiveProjectPath, msg.threadId, Object.keys(metadata).length ? metadata : undefined, this.extractTopicName(msg), msg.peerId, chatType, owningAgent?.baseagent, msg.selfAID, msg.channelType || effectiveChannelType, msg.peerType, identity);
1232
+ let session = diagnosticSession ?? await this.sessionManager.getOrCreateSession(channelName, msg.channelId, effectiveProjectPath, msg.threadId, Object.keys(metadata).length ? metadata : undefined, this.extractTopicName(msg), msg.peerId, chatType, owningAgent?.baseagent, msg.selfAID, msg.channelType || effectiveChannelType, msg.peerType, identity, readTopicLable(msg) ?? readTopicLable(msg.replyContext?.metadata));
1153
1233
  if (session.threadId && typeof this.messageQueue.reserveSessionIngress === 'function') {
1154
1234
  releaseSessionIngress = this.messageQueue.reserveSessionIngress(session.id);
1155
1235
  }
@@ -1162,7 +1242,7 @@ export class MessageBridge {
1162
1242
  const inboundCausation = restoredCausation
1163
1243
  ? deriveCausation(restoredCausation)
1164
1244
  : createRootCausation();
1165
- const renewResult = await this.sessionRenewService.resolve({
1245
+ const renewResult = session.metadata?.diagnostic ? { session } : await this.sessionRenewService.resolve({
1166
1246
  session,
1167
1247
  channelName,
1168
1248
  channelType: msg.channelType || effectiveChannelType,
@@ -1205,6 +1285,7 @@ export class MessageBridge {
1205
1285
  mentions: msg.mentions, mentionAids: msg.mentionAids, threadId: msg.threadId,
1206
1286
  isMentioned: msg.isMentioned,
1207
1287
  topicName: this.extractTopicName(msg),
1288
+ lable: session.lable,
1208
1289
  replyContext: msg.replyContext,
1209
1290
  source: msg.source,
1210
1291
  ...(executionPermissionOverride ? { executionPermissionOverride } : {}),
@@ -1298,6 +1379,9 @@ export class MessageBridge {
1298
1379
  const enqueueAgentName = owningAgent?.name ?? '<unknown>';
1299
1380
  const selfAID = session.selfAID;
1300
1381
  const doEnqueue = async (m) => {
1382
+ // A cancellation can arrive while this message is being debounced.
1383
+ if (!isDiagnosticSessionActive(session, this.sessionManager.getChatDir(session)))
1384
+ return;
1301
1385
  const enqueue = handoffCandidate ? this.messageQueue.enqueuePersisted.bind(this.messageQueue) : this.messageQueue.enqueue.bind(this.messageQueue);
1302
1386
  try {
1303
1387
  return await enqueue(session.id, m, session.projectPath, {
@@ -1427,6 +1511,8 @@ export class MessageBridge {
1427
1511
  const menuAuthSubject = {
1428
1512
  ...authSubject,
1429
1513
  requestId: parsed.id,
1514
+ menuEncrypted: msg.encrypted === true,
1515
+ fromControlChannel: authSubject.fromControlChannel || msg.isControlChannel === true,
1430
1516
  ...(activeSession?.id ? { sessionId: activeSession.id } : {}),
1431
1517
  };
1432
1518
  const isTokenRequest = parsed.type === 'menu.token.request';
@@ -1457,6 +1543,17 @@ export class MessageBridge {
1457
1543
  return;
1458
1544
  }
1459
1545
  }
1546
+ // Reauthorize even cached responses: roots expire and static owners can be revoked.
1547
+ const isStorageRequest = isAgentStorageMenuRequest(parsed.request);
1548
+ if (isStorageRequest) {
1549
+ try {
1550
+ agentStorageMenuService.preflight(parsed.request, menuAuthSubject);
1551
+ }
1552
+ catch (error) {
1553
+ await this.sendMenuResponse(adapter, channel, msg, menuFailure(header, normalizeMenuError(error)));
1554
+ return;
1555
+ }
1556
+ }
1460
1557
  const scope = msg.chatType === 'group' ? (msg.groupId || msg.channelId) : msg.peerId;
1461
1558
  // Separate installations can generate identical request IDs. Token
1462
1559
  // issuance and replay must remain scoped to the authenticated requester.
@@ -1474,6 +1571,16 @@ export class MessageBridge {
1474
1571
  }));
1475
1572
  return;
1476
1573
  }
1574
+ // A replay may have awaited an in-flight request; check again before release.
1575
+ if (isStorageRequest) {
1576
+ try {
1577
+ agentStorageMenuService.preflight(parsed.request, menuAuthSubject, false);
1578
+ }
1579
+ catch (error) {
1580
+ await this.sendMenuResponse(adapter, channel, msg, menuFailure(header, normalizeMenuError(error)));
1581
+ return;
1582
+ }
1583
+ }
1477
1584
  if (deduped.replayed)
1478
1585
  this.logMenuDiagnostic('request-replay', channel, msg, parsed);
1479
1586
  if (deduped.retry)
@@ -1488,7 +1595,20 @@ export class MessageBridge {
1488
1595
  await deferredEffect();
1489
1596
  }
1490
1597
  catch (error) {
1491
- logger.warn(`[MenuControl] deferred effect failed: request=${parsed.id} error=${error instanceof Error ? error.message : String(error)}`);
1598
+ const detail = error instanceof Error ? error.message : String(error);
1599
+ const code = error && typeof error === 'object' && 'code' in error
1600
+ ? String(error.code ?? 'EXEC_FAILED')
1601
+ : 'EXEC_FAILED';
1602
+ logger.warn(`[MenuControl] deferred effect failed: request=${parsed.id} error=${detail}`);
1603
+ // File fetch is acknowledged before the upload completes. Send a
1604
+ // second response with the same request id so the client can finish
1605
+ // the pending fetch with an actionable error instead of timing out.
1606
+ const failedResponse = menuFailure(header, {
1607
+ code,
1608
+ message: detail.startsWith('文件发送失败') ? detail : `文件发送失败: ${detail}`,
1609
+ });
1610
+ await this.menuDeduper.replaceResult(dedupKey, menuPayloadFingerprint(parsed.raw), deduped.value, failedResponse);
1611
+ await this.sendMenuResponse(adapter, channel, msg, failedResponse);
1492
1612
  }
1493
1613
  }
1494
1614
  }
@@ -1498,6 +1618,13 @@ export class MessageBridge {
1498
1618
  const name = header.name ?? '';
1499
1619
  const cmd = typeof request.cmd === 'string' ? request.cmd : undefined;
1500
1620
  const args = request.args;
1621
+ const transportReplyContext = {
1622
+ ...(msg.replyContext ?? {}),
1623
+ ...(msg.threadId ? { threadId: msg.threadId } : {}),
1624
+ };
1625
+ // Explicit group context can target a different conversation. Its
1626
+ // sessions must not inherit the thread of the source conversation.
1627
+ const replyContextFor = (channelId) => channelId === msg.channelId ? transportReplyContext : undefined;
1501
1628
  switch (request.type) {
1502
1629
  case 'menu.list':
1503
1630
  if (args?.view === 'catalog') {
@@ -1514,7 +1641,7 @@ export class MessageBridge {
1514
1641
  case 'menu.query': {
1515
1642
  if (name === 'menu' && args?.view === 'values') {
1516
1643
  validateMenuValueBatchArgs(args);
1517
- const data = await this.cmdHandler.execMenuValueBatch(channel, msg.channelId, authSubject, args, args.context, msg.isControlChannel ? 'control' : 'agent');
1644
+ const data = await this.cmdHandler.execMenuValueBatch(channel, msg.channelId, authSubject, args, args.context, msg.isControlChannel ? 'control' : 'agent', transportReplyContext);
1518
1645
  return menuSuccess(header, data);
1519
1646
  }
1520
1647
  if (name === 'menu') {
@@ -1527,7 +1654,7 @@ export class MessageBridge {
1527
1654
  channelId: msg.channelId,
1528
1655
  args,
1529
1656
  });
1530
- const result = await this.cmdHandler.execMenuQuery(this.resolveCmd(name, cmd), channel, execution.channelId, execution.subject.actorId, execution.args, execution.subject.chatType, execution.subject.fromControlChannel, execution.subject.identity, execution.subject);
1657
+ const result = await this.cmdHandler.execMenuQuery(this.resolveCmd(name, cmd), channel, execution.channelId, execution.subject.actorId, execution.args, execution.subject.chatType, execution.subject.fromControlChannel, execution.subject.identity, execution.subject, 'menu', replyContextFor(execution.channelId));
1531
1658
  if ('error' in result)
1532
1659
  throw result;
1533
1660
  return menuSuccess(header, result.data);
@@ -1544,7 +1671,7 @@ export class MessageBridge {
1544
1671
  if ((name === 'topic' || cmd === '/topic') && args?.mode === 'fork-turns' && msg.threadId && !args?.sourceThreadId) {
1545
1672
  optionArgs = { ...(execution.args ?? {}), sourceThreadId: msg.threadId };
1546
1673
  }
1547
- const data = await this.cmdHandler.getSubMenuItems(this.resolveCmd(name, cmd), channel, execution.channelId, execution.subject.actorId, optionArgs, execution.subject.identity, execution.subject.chatType, execution.subject.fromControlChannel, execution.subject) ?? [];
1674
+ const data = await this.cmdHandler.getSubMenuItems(this.resolveCmd(name, cmd), channel, execution.channelId, execution.subject.actorId, optionArgs, execution.subject.identity, execution.subject.chatType, execution.subject.fromControlChannel, execution.subject, 'menu', replyContextFor(execution.channelId)) ?? [];
1548
1675
  return menuSuccess(header, data);
1549
1676
  }
1550
1677
  case 'menu.update': {
@@ -1555,10 +1682,7 @@ export class MessageBridge {
1555
1682
  channelId: msg.channelId,
1556
1683
  args,
1557
1684
  });
1558
- const result = await this.cmdHandler.execMenuUpdate(this.resolveCmd(name, cmd), request.value, channel, execution.channelId, execution.subject.actorId, execution.subject.identity, execution.subject.fromControlChannel, execution.args, execution.subject, 'menu', {
1559
- ...(msg.replyContext ?? {}),
1560
- ...(msg.threadId ? { threadId: msg.threadId } : {}),
1561
- });
1685
+ const result = await this.cmdHandler.execMenuUpdate(this.resolveCmd(name, cmd), request.value, channel, execution.channelId, execution.subject.actorId, execution.subject.identity, execution.subject.fromControlChannel, execution.args, execution.subject, 'menu', replyContextFor(execution.channelId));
1562
1686
  if ('error' in result)
1563
1687
  throw result;
1564
1688
  return menuSuccess(header, result.data);
@@ -1621,9 +1745,11 @@ export class MessageBridge {
1621
1745
  const agentName = this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>';
1622
1746
  const replyContext = {
1623
1747
  ...(msg.replyContext ?? {}),
1748
+ ...(msg.peerDeviceId ? { toDeviceId: msg.peerDeviceId } : {}),
1624
1749
  metadata: {
1625
1750
  ...(msg.replyContext?.metadata ?? {}),
1626
- ...evolMenuResponseTransportMetadata(),
1751
+ ...evolMenuResponseTransportMetadata(msg.protectedHeaders?.sdk_version),
1752
+ ...(isAgentStorageMenuRequest(request) ? { encrypted: true } : {}),
1627
1753
  },
1628
1754
  };
1629
1755
  if (msg.channelType === 'aun' && !replyContext.delivery) {
@@ -1890,6 +2016,7 @@ export class MessageBridge {
1890
2016
  }
1891
2017
  /** 注销单个渠道的 debouncer(热重载断开渠道时调用) */
1892
2018
  removeChannel(channelName) {
2019
+ this.groupInviteReviewService?.detach(channelName);
1893
2020
  const d = this.debouncers.get(channelName);
1894
2021
  if (d) {
1895
2022
  d.dispose();
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { appendJsonl, chatDirPath } from '../session/session-fs-store.js';
3
+ import { appendJsonl, appendJsonlDurable, chatDirPath } from '../session/session-fs-store.js';
4
4
  import { logger } from '../../utils/logger.js';
5
5
  const TRANSIENT_PROTOCOL_TYPE_PREFIXES = [
6
6
  'contact.request.',
@@ -313,13 +313,13 @@ export function appendMessageLog(chatDir, entry) {
313
313
  }
314
314
  }
315
315
  /** Strict conversation-history append used by handoff reply binding. */
316
- export function appendMessageLogStrict(chatDir, entry) {
316
+ export function appendMessageLogStrict(chatDir, entry, durable = false) {
317
317
  if (isTransientProtocolMessage(entry))
318
318
  return;
319
319
  if (entry.dir === 'in' && hasDuplicate(chatDir, entry.msgId))
320
320
  return;
321
321
  fs.mkdirSync(chatDir, { recursive: true });
322
- appendJsonl(messageLogPath(chatDir), entry);
322
+ (durable ? appendJsonlDurable : appendJsonl)(messageLogPath(chatDir), entry);
323
323
  if (entry.dir === 'in')
324
324
  rememberMessageId(chatDir, entry.msgId);
325
325
  }
@@ -343,6 +343,8 @@ export function buildInboundEntry(opts) {
343
343
  msgType: isCommand ? 'command' : (opts.msgType ?? 'text'),
344
344
  payloadType: opts.payloadType,
345
345
  payloadSummary: opts.payloadSummary,
346
+ groupAttention: opts.groupAttention,
347
+ groupDelivery: opts.groupDelivery,
346
348
  content: opts.content,
347
349
  replyTo: opts.replyTo ?? null,
348
350
  agent: null,
@@ -4,6 +4,7 @@ import { logger } from '../../utils/logger.js';
4
4
  import { LogicalQueueBridge } from './logical-queue-bridge.js';
5
5
  import { normalizeCausation } from '../causation/context.js';
6
6
  import { KeyedFairMutex } from '../handoff/mutex.js';
7
+ import { ExecutionTimeoutError, OrphanedTargetSessionError, isExecutionFailure } from '../../utils/error-utils.js';
7
8
  const DEFAULT_AGENT_NAME = '<unknown>';
8
9
  const RESTART_RESUME_CONTENT = 'evolcore 服务已重启,请继续之前未完成的任务。';
9
10
  export class MessageQueue {
@@ -52,7 +53,7 @@ export class MessageQueue {
52
53
  setInterruptCallback(callback) {
53
54
  this.interruptCallback = callback;
54
55
  }
55
- triggerInterrupt(queueKey, sessionKey, baseagent, evolagentName, reason) {
56
+ triggerInterrupt(queueKey, sessionKey, baseagent, evolagentName, reason, timeout) {
56
57
  const existing = this.interruptBarriers.get(queueKey);
57
58
  if (existing)
58
59
  return existing;
@@ -61,7 +62,9 @@ export class MessageQueue {
61
62
  this.activeTerminalReasons.set(queueKey, reason);
62
63
  let interruption;
63
64
  try {
64
- interruption = Promise.resolve(this.interruptCallback(sessionKey, baseagent, evolagentName, reason));
65
+ interruption = Promise.resolve(timeout
66
+ ? this.interruptCallback(sessionKey, baseagent, evolagentName, reason, timeout)
67
+ : this.interruptCallback(sessionKey, baseagent, evolagentName, reason));
65
68
  }
66
69
  catch (error) {
67
70
  interruption = Promise.reject(error);
@@ -980,7 +983,8 @@ export class MessageQueue {
980
983
  this.persistQueuesImmediate();
981
984
  }
982
985
  else if (message.messageId) {
983
- void this.cancelActiveAndWait(message.messageId, message.selfAID).finally(() => {
986
+ const timeout = isExecutionFailure(options?.signal?.reason) ? options.signal.reason : undefined;
987
+ void this.cancelActiveAndWait(message.messageId, message.selfAID, timeout).finally(() => {
984
988
  this.forgetProcessed(sessionKey, message);
985
989
  settleReject(error);
986
990
  });
@@ -1262,7 +1266,9 @@ export class MessageQueue {
1262
1266
  return result;
1263
1267
  }
1264
1268
  isMergeBarrier(message) {
1265
- return !!message.executionPermissionOverride
1269
+ return !!message.taskRecovery
1270
+ || !!message.asyncQuestionAnswer
1271
+ || !!message.executionPermissionOverride
1266
1272
  || !!message.handoffDelivery
1267
1273
  || !!message.triggerMeta
1268
1274
  || !!message.causation?.trigger?.path.length;
@@ -1525,12 +1531,14 @@ export class MessageQueue {
1525
1531
  return cancellation.found;
1526
1532
  }
1527
1533
  /** Cancel an active message and wait until the runner interrupt barrier settles. */
1528
- async cancelActiveAndWait(messageId, selfAID) {
1529
- const cancellation = this.beginActiveCancellation(messageId, selfAID);
1534
+ async cancelActiveAndWait(messageId, selfAID, cause) {
1535
+ const cancellation = this.beginActiveCancellation(messageId, selfAID, cause);
1530
1536
  await cancellation.barrier;
1531
1537
  return cancellation.found;
1532
1538
  }
1533
- beginActiveCancellation(messageId, selfAID) {
1539
+ beginActiveCancellation(messageId, selfAID, cause) {
1540
+ const reason = cause instanceof ExecutionTimeoutError ? 'timeout'
1541
+ : cause instanceof OrphanedTargetSessionError ? 'execution_error' : cause ?? 'recalled';
1534
1542
  for (const [queueKey, activeState] of this.activeStates.entries()) {
1535
1543
  if (!activeState.messageIds.has(messageId))
1536
1544
  continue;
@@ -1558,18 +1566,20 @@ export class MessageQueue {
1558
1566
  }
1559
1567
  const sessionKey = this.sessionKeyFromQueueKey(queueKey);
1560
1568
  const agentName = this.processingAgent.get(queueKey);
1561
- logger.info(`[Queue] Recalled active message ${messageId}, interrupting session ${sessionKey}`);
1562
- this.eventBus?.publish({
1563
- type: 'task:interrupted',
1564
- sessionId: sessionKey,
1565
- reason: 'recalled',
1566
- agentName,
1567
- taskId: activeState?.taskId,
1568
- generation: activeState?.generation,
1569
- causation: active?.message.causation,
1570
- });
1569
+ logger.info(`[Queue] Cancelled active message ${messageId}, interrupting session ${sessionKey}: ${reason}`);
1570
+ // TimeoutController owns the failure event and outbound timeout terminal.
1571
+ if (reason !== 'timeout' && reason !== 'execution_error')
1572
+ this.eventBus?.publish({
1573
+ type: 'task:interrupted',
1574
+ sessionId: sessionKey,
1575
+ reason,
1576
+ agentName,
1577
+ taskId: activeState?.taskId,
1578
+ generation: activeState?.generation,
1579
+ causation: active?.message.causation,
1580
+ });
1571
1581
  const barrier = this.interruptCallback
1572
- ? this.triggerInterrupt(queueKey, sessionKey, activeState.baseagent, agentName, 'recalled')
1582
+ ? this.triggerInterrupt(queueKey, sessionKey, activeState.baseagent, agentName, reason, isExecutionFailure(cause) ? cause : undefined)
1573
1583
  : Promise.resolve();
1574
1584
  return { found: true, barrier };
1575
1585
  }
@@ -1603,33 +1613,11 @@ export class MessageQueue {
1603
1613
  }
1604
1614
  let releasePromise;
1605
1615
  const promise = new Promise(resolve => { releasePromise = resolve; });
1606
- let queued = 0;
1607
- let active = false;
1608
- let dispatching = false;
1609
- for (const [queueKey, queue] of this.queues.entries()) {
1610
- if (!this.matchesSession(queueKey, sessionKey))
1611
- continue;
1612
- queued += this.queueRawMessageCount(queue);
1613
- if (this.processing.has(queueKey) || this.activeBatches.has(queueKey))
1614
- active = true;
1615
- if (this.processing.has(queueKey) && !this.activeBatches.has(queueKey))
1616
- dispatching = true;
1617
- }
1618
- for (const queueKey of this.activeBatches.keys()) {
1619
- if (this.matchesSession(queueKey, sessionKey))
1620
- active = true;
1621
- }
1622
1616
  const state = {
1623
1617
  promise,
1624
1618
  releasePromise,
1625
1619
  holders: 1,
1626
- snapshot: {
1627
- prequeue: this.prequeueReservations.get(sessionKey) ?? 0,
1628
- active,
1629
- queued,
1630
- dispatching,
1631
- cutoff: this.enqueueSequence,
1632
- },
1620
+ snapshot: this.getSessionSnapshot(sessionKey),
1633
1621
  };
1634
1622
  this.externalLocks.set(sessionKey, state);
1635
1623
  let released = false;
@@ -1648,6 +1636,50 @@ export class MessageQueue {
1648
1636
  },
1649
1637
  };
1650
1638
  }
1639
+ /** Live admission state, including work that arrived after an older gate. */
1640
+ getMessageState(sessionId, messageId, selfAid) {
1641
+ for (const [key, queue] of this.queues) {
1642
+ if (this.matchesSession(key, sessionId) && queue.some(item => this.partsOf(item)
1643
+ .some(part => this.partMatchesMessage(part, messageId, selfAid, key))))
1644
+ return { state: 'queued' };
1645
+ }
1646
+ for (const [key, item] of this.activeBatches) {
1647
+ if (this.matchesSession(key, sessionId) && this.partsOf(item)
1648
+ .some(part => this.partMatchesMessage(part, messageId, selfAid, key))) {
1649
+ return { state: 'active' };
1650
+ }
1651
+ }
1652
+ return { state: 'missing' };
1653
+ }
1654
+ /** Live admission state, including work that arrived after an older gate. */
1655
+ getSessionSnapshot(sessionKey) {
1656
+ let queued = 0;
1657
+ let active = false;
1658
+ let dispatching = false;
1659
+ const activeTaskIds = new Set();
1660
+ for (const [queueKey, queue] of this.queues.entries()) {
1661
+ if (!this.matchesSession(queueKey, sessionKey))
1662
+ continue;
1663
+ queued += this.queueRawMessageCount(queue);
1664
+ if (this.processing.has(queueKey) || this.activeBatches.has(queueKey))
1665
+ active = true;
1666
+ if (this.processing.has(queueKey) && !this.activeBatches.has(queueKey))
1667
+ dispatching = true;
1668
+ }
1669
+ for (const queueKey of this.activeBatches.keys()) {
1670
+ if (!this.matchesSession(queueKey, sessionKey))
1671
+ continue;
1672
+ active = true;
1673
+ const taskId = this.activeStates.get(queueKey)?.taskId;
1674
+ if (taskId)
1675
+ activeTaskIds.add(taskId);
1676
+ }
1677
+ return {
1678
+ prequeue: this.prequeueReservations.get(sessionKey) ?? 0,
1679
+ active, queued, dispatching, cutoff: this.enqueueSequence,
1680
+ activeTaskIds: [...activeTaskIds],
1681
+ };
1682
+ }
1651
1683
  /**
1652
1684
  * Mark an admitted message before it enters debounce or other pre-queue
1653
1685
  * work. The returned release is idempotent so callers can use it both from