evolcore 0.0.2 → 0.0.4

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 (206) hide show
  1. package/CHANGELOG.md +93 -778
  2. package/README.md +45 -10
  3. package/bin/ec-safe-output.js +89 -24
  4. package/dist/agents/baseagent.js +46 -0
  5. package/dist/agents/claude-runner.js +198 -48
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +203 -8
  8. package/dist/agents/runner-types.js +2 -2
  9. package/dist/aun/aid/agentmd.js +79 -0
  10. package/dist/aun/aid/encryption-seed-policy.js +29 -0
  11. package/dist/aun/aid/index.js +1 -1
  12. package/dist/aun/aid/store.js +2 -5
  13. package/dist/aun/outbox.js +8 -0
  14. package/dist/channels/aun.js +241 -116
  15. package/dist/channels/daemon.js +18 -4
  16. package/dist/channels/dingtalk.js +1005 -260
  17. package/dist/channels/feishu.js +184 -56
  18. package/dist/channels/qqbot.js +23 -1
  19. package/dist/channels/wechat.js +303 -155
  20. package/dist/channels/wecom.js +1132 -164
  21. package/dist/cli/agent-command.js +2 -1
  22. package/dist/cli/aun-commands.js +98 -43
  23. package/dist/cli/bench.js +2 -2
  24. package/dist/cli/contact.js +71 -0
  25. package/dist/cli/ctl-command.js +2 -2
  26. package/dist/cli/daemon-commands.js +77 -214
  27. package/dist/cli/handoff-command.js +4 -3
  28. package/dist/cli/help.js +9 -5
  29. package/dist/cli/index.js +132 -116
  30. package/dist/cli/init-channel.js +242 -129
  31. package/dist/cli/init.js +70 -31
  32. package/dist/cli/model.js +2 -1
  33. package/dist/cli/net-check.js +2 -2
  34. package/dist/cli/queue-command.js +30 -6
  35. package/dist/cli/raw-key-input.js +25 -0
  36. package/dist/cli/response.js +5 -6
  37. package/dist/cli/restart-monitor.js +25 -1
  38. package/dist/cli/stats.js +6 -4
  39. package/dist/cli/trigger-command.js +146 -25
  40. package/dist/cli/version.js +6 -1
  41. package/dist/config/access-policy-domain.js +38 -0
  42. package/dist/config/access-policy.js +134 -0
  43. package/dist/config/builtin-role-templates.js +42 -17
  44. package/dist/config/builtin-roles.js +31 -6
  45. package/dist/config/config-field-policy.js +17 -5
  46. package/dist/config/config-manager.js +409 -29
  47. package/dist/config/config-operation-service.js +35 -38
  48. package/dist/config/contact-alias.js +68 -0
  49. package/dist/config/contact-bind-code.js +274 -0
  50. package/dist/config/contact-book-store.js +622 -0
  51. package/dist/config/contact-book-v2-startup.js +35 -0
  52. package/dist/config/contact-book.js +185 -300
  53. package/dist/config/contact-operation-service.js +116 -0
  54. package/dist/config/contact-request-service.js +331 -0
  55. package/dist/config/peer-role-resolver.js +136 -55
  56. package/dist/config/role-ranks.js +18 -0
  57. package/dist/config/role-service.js +16 -19
  58. package/dist/config/role-store.js +16 -5
  59. package/dist/config/roles.js +10 -1
  60. package/dist/config/schema-registry.js +49 -24
  61. package/dist/config-store.js +14 -4
  62. package/dist/core/auth/agent-delegation.js +105 -11
  63. package/dist/core/auth/authorization-audit.js +16 -1
  64. package/dist/core/auth/operation-authorizer.js +10 -0
  65. package/dist/core/auth/operation-catalog.js +105 -1
  66. package/dist/core/auth/trigger-authorization.js +15 -0
  67. package/dist/core/bootstrap-service.js +2 -9
  68. package/dist/core/channel-loader.js +6 -2
  69. package/dist/core/command/command-handler.js +113 -21
  70. package/dist/core/command/connect-menu.js +599 -0
  71. package/dist/core/command/evol-menu-version-gate.js +38 -0
  72. package/dist/core/command/group-menu.js +421 -0
  73. package/dist/core/command/menu-handler.js +382 -100
  74. package/dist/core/command/menu-protocol.js +8 -2
  75. package/dist/core/command/menu-token-store.js +102 -0
  76. package/dist/core/command/role-menu.js +128 -29
  77. package/dist/core/command/slash-gate.js +1 -1
  78. package/dist/core/command/slash-handler.js +186 -49
  79. package/dist/core/daemon-file-cache.js +40 -6
  80. package/dist/core/event-catalog.js +99 -0
  81. package/dist/core/evolagent-registry.js +4 -40
  82. package/dist/core/evolagent.js +20 -9
  83. package/dist/core/handoff/runtime.js +162 -37
  84. package/dist/core/handoff/store.js +46 -2
  85. package/dist/core/handoff/types.js +1 -0
  86. package/dist/core/inference/text-inference.js +16 -74
  87. package/dist/core/message/file-markers.js +7 -0
  88. package/dist/core/message/im-renderer.js +20 -14
  89. package/dist/core/message/inbound-admission.js +124 -0
  90. package/dist/core/message/message-bridge.js +670 -57
  91. package/dist/core/message/message-log.js +1 -0
  92. package/dist/core/message/message-queue.js +195 -10
  93. package/dist/core/message/message-utils.js +8 -2
  94. package/dist/core/message/peer-mode.js +7 -8
  95. package/dist/core/message/response-engine.js +642 -96
  96. package/dist/core/message/send-receipt.js +24 -0
  97. package/dist/core/message/stream-debouncer.js +11 -2
  98. package/dist/core/permission/approval-gateway.js +17 -10
  99. package/dist/core/permission/ec-command-parser.js +101 -46
  100. package/dist/core/permission/tool-policy.js +115 -38
  101. package/dist/core/protected-paths.js +38 -11
  102. package/dist/core/session/session-fs-store.js +44 -3
  103. package/dist/core/session/session-manager.js +86 -8
  104. package/dist/core/session/session-mapper.js +2 -0
  105. package/dist/core/session/session-renew.js +125 -69
  106. package/dist/core/session/session-turn-coordinator.js +5 -1
  107. package/dist/eck/kit-renderer.js +12 -1
  108. package/dist/eck/message-renderer.js +79 -1
  109. package/dist/index.js +285 -94
  110. package/dist/ipc.js +114 -8
  111. package/dist/paths.js +3 -0
  112. package/dist/response-system/config-resolver.js +29 -0
  113. package/dist/response-system/coordinator.js +8 -32
  114. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  115. package/dist/response-system/index.js +1 -0
  116. package/dist/response-system/modes/single-session/index.js +7 -4
  117. package/dist/stats/billing.js +20 -8
  118. package/dist/trigger/manager.js +3 -0
  119. package/dist/trigger/parser.js +126 -11
  120. package/dist/trigger/patch.js +12 -2
  121. package/dist/trigger/scheduler.js +444 -40
  122. package/dist/trigger/validation.js +13 -0
  123. package/dist/utils/aid-bind.js +43 -29
  124. package/dist/utils/error-dict.json +7 -0
  125. package/dist/utils/evolcore-version.js +21 -0
  126. package/dist/utils/instance-registry.js +14 -7
  127. package/dist/utils/log-writer.js +46 -0
  128. package/dist/utils/logger.js +2 -2
  129. package/dist/utils/media-cache.js +4 -1
  130. package/dist/utils/model-prices.jsonl +6 -3
  131. package/dist/utils/restart-safety.js +31 -0
  132. package/dist/utils/stable-semver.js +21 -0
  133. package/dist/utils/stats.js +33 -9
  134. package/dist/utils/system-memory.js +62 -0
  135. package/kits/docs/INDEX.md +2 -1
  136. package/kits/docs/channels/aun.md +4 -13
  137. package/kits/docs/evolcore/INDEX.md +5 -3
  138. package/kits/docs/evolcore/agent.md +9 -1
  139. package/kits/docs/evolcore/aid.md +5 -2
  140. package/kits/docs/evolcore/config.md +47 -2
  141. package/kits/docs/evolcore/contact.md +61 -0
  142. package/kits/docs/evolcore/fs.md +9 -0
  143. package/kits/docs/evolcore/group-rules.md +46 -4
  144. package/kits/docs/evolcore/group.md +15 -6
  145. package/kits/docs/evolcore/model.md +4 -1
  146. package/kits/docs/evolcore/msg.md +12 -6
  147. package/kits/docs/evolcore/response.md +16 -21
  148. package/kits/docs/evolcore/rpc.md +2 -0
  149. package/kits/docs/evolcore/stats.md +15 -2
  150. package/kits/docs/evolcore/storage.md +1 -0
  151. package/kits/docs/evolcore/trigger.md +42 -10
  152. package/kits/docs/path-registry.md +36 -17
  153. package/kits/eck_manifest.json +12 -0
  154. package/kits/eck_message_manifest.json +12 -1
  155. package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
  156. package/kits/rules/01-overview.md +17 -7
  157. package/kits/rules/02-navigation.md +38 -18
  158. package/kits/rules/03-identity.md +28 -24
  159. package/kits/rules/04-relation.md +44 -28
  160. package/kits/rules/05-venue.md +31 -15
  161. package/kits/rules/06-channel.md +11 -7
  162. package/kits/schemas/_meta.json +21 -7
  163. package/kits/schemas/agent-config.schema.6.json +322 -0
  164. package/kits/schemas/agent-config.schema.7.json +303 -0
  165. package/kits/schemas/agent-config.schema.8.json +304 -0
  166. package/kits/schemas/agent-config.schema.9.json +374 -0
  167. package/kits/schemas/contact-book.schema.2.json +43 -0
  168. package/kits/schemas/contact-book.schema.3.json +67 -0
  169. package/kits/schemas/daemon.schema.1.json +3 -2
  170. package/kits/schemas/daemon.schema.2.json +101 -0
  171. package/kits/schemas/daemon.schema.3.json +123 -0
  172. package/kits/schemas/defaults.schema.2.json +85 -0
  173. package/kits/schemas/defaults.schema.3.json +73 -0
  174. package/kits/schemas/relation-config.schema.5.json +47 -0
  175. package/kits/schemas/relation-config.schema.6.json +46 -0
  176. package/kits/schemas/relation-config.schema.7.json +59 -0
  177. package/kits/schemas/role-config.schema.1.json +1 -0
  178. package/kits/schemas/role-registry.schema.1.json +2 -2
  179. package/kits/schemas/single-session.schema.2.json +57 -0
  180. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  181. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  182. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  183. package/kits/templates/roles/admin.json +47 -0
  184. package/kits/templates/roles/member.json +5 -0
  185. package/kits/templates/roles/owner.json +1 -0
  186. package/kits/templates/roles/visitor.json +5 -0
  187. package/kits/templates/system-fragments/channel.md +12 -2
  188. package/kits/templates/system-fragments/commands.md +3 -1
  189. package/kits/templates/system-fragments/identity.md +3 -1
  190. package/kits/templates/system-fragments/relation.md +1 -1
  191. package/kits/templates/system-fragments/session.md +6 -2
  192. package/package.json +7 -5
  193. package/MIGRATION-0.5.0.md +0 -378
  194. package/ROLE_ACCESS_CONTROL.md +0 -174
  195. package/assets/brand/evolcore/README.md +0 -19
  196. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  197. package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
  198. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  199. package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
  200. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  201. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  202. package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
  203. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  204. package/assets/brand/evolcore/evolcore-logo.svg +0 -14
  205. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  206. package/assets/brand/evolcore/evolcore-mark.svg +0 -10
@@ -320,6 +320,7 @@ export function buildOutboundEntry(opts) {
320
320
  usage: opts.usage ?? null,
321
321
  encrypt: opts.encrypt,
322
322
  chatmode: opts.chatmode,
323
+ transport: opts.transport,
323
324
  peerType: opts.peerType,
324
325
  source: opts.source ?? 'daemon',
325
326
  handoff_trace: opts.handoff_trace,
@@ -14,6 +14,7 @@ export class MessageQueue {
14
14
  activeStates = new Map(); // queueKey → active task state
15
15
  interruptCallback;
16
16
  interruptBarriers = new Map();
17
+ activeTerminalReasons = new Map();
17
18
  eventBus;
18
19
  recentMessageIds = new Set();
19
20
  DEDUP_WINDOW = 60_000; // 1 分钟窗口
@@ -24,6 +25,8 @@ export class MessageQueue {
24
25
  processingStartTime = new Map(); // queueKey → 处理开始时间戳
25
26
  queueKeyToSessionKey = new Map(); // queueKey → sessionKey(human-readable 格式)
26
27
  droppedTriggerSessionIds = new Set();
28
+ shuttingDown = false;
29
+ shutdownPromise;
27
30
  /** 逻辑队列桥接:管理出队顺序(响应模式可注入自定义策略,默认 FIFO) */
28
31
  logicalQueue = new LogicalQueueBridge();
29
32
  constructor(handler, options) {
@@ -43,7 +46,15 @@ export class MessageQueue {
43
46
  return existing;
44
47
  if (!this.interruptCallback)
45
48
  return Promise.resolve();
46
- const barrier = this.interruptCallback(sessionKey, baseagent, evolagentName, reason)
49
+ this.activeTerminalReasons.set(queueKey, reason);
50
+ let interruption;
51
+ try {
52
+ interruption = Promise.resolve(this.interruptCallback(sessionKey, baseagent, evolagentName, reason));
53
+ }
54
+ catch (error) {
55
+ interruption = Promise.reject(error);
56
+ }
57
+ const barrier = interruption
47
58
  .catch(error => {
48
59
  logger.warn(`[Queue] Interrupt barrier failed for ${queueKey}: ${error instanceof Error ? error.message : String(error)}`);
49
60
  })
@@ -57,6 +68,29 @@ export class MessageQueue {
57
68
  }
58
69
  setEventBus(eventBus) {
59
70
  this.eventBus = eventBus;
71
+ const subscribe = eventBus.subscribe;
72
+ if (typeof subscribe !== 'function')
73
+ return;
74
+ subscribe.call(eventBus, 'task:interrupted', event => {
75
+ if ('sessionId' in event && event.sessionId) {
76
+ this.markActiveTerminal(event.sessionId, event.reason || 'interrupted');
77
+ }
78
+ });
79
+ subscribe.call(eventBus, 'task:completed', event => {
80
+ if ('sessionId' in event && event.sessionId)
81
+ this.markActiveTerminal(event.sessionId, 'completed');
82
+ });
83
+ subscribe.call(eventBus, 'task:error', event => {
84
+ if ('sessionId' in event && event.sessionId)
85
+ this.markActiveTerminal(event.sessionId, 'error');
86
+ });
87
+ }
88
+ /** 标记 active handler 已有业务终态,避免 shutdown 将其重新放回恢复队列。 */
89
+ markActiveTerminal(sessionId, reason) {
90
+ for (const queueKey of this.activeBatches.keys()) {
91
+ if (this.matchesSession(queueKey, sessionId))
92
+ this.activeTerminalReasons.set(queueKey, reason);
93
+ }
60
94
  }
61
95
  /**
62
96
  * 注册一次性消息拦截器:下一条来自 sessionKey 的消息不入队、不触发 interrupt,
@@ -213,7 +247,9 @@ export class MessageQueue {
213
247
  images: undefined,
214
248
  mentions: undefined,
215
249
  mentionAids: undefined,
216
- messageId: undefined,
250
+ // Retain the inbound anchor so the resumed task can replace the
251
+ // daemon-restart CrossMark with its current processing state.
252
+ messageId: source.messageId,
217
253
  batchRole: item.role,
218
254
  items: [{
219
255
  kind: 'restart-resume',
@@ -326,6 +362,69 @@ export class MessageQueue {
326
362
  persistQueuesImmediate() {
327
363
  this.persistQueuesSync();
328
364
  }
365
+ /**
366
+ * 冻结当前队列并优雅中断所有正在执行的主任务。
367
+ *
368
+ * activeBatches 会原样保留到进程退出,供下次启动恢复主消息;runner 内部
369
+ * 子任务不进入队列,也不在这里重建。task:interrupted 必须同步发布,确保
370
+ * ResponseEngine 在任何 handler continuation 之前先使旧 turn lease 失效。
371
+ */
372
+ shutdownGracefully(timeoutMs = 6_500) {
373
+ if (this.shutdownPromise)
374
+ return this.shutdownPromise;
375
+ this.shuttingDown = true;
376
+ const allActive = Array.from(this.activeBatches.entries());
377
+ const active = allActive.filter(([queueKey]) => !this.activeTerminalReasons.has(queueKey));
378
+ const barriers = [];
379
+ // 已因 stop/new_message/timeout/recalled 进入中断的任务不应被 restart 恢复。
380
+ // pending 队列仍照常保留,下一次启动只处理尚未消费的后续输入。
381
+ for (const [queueKey] of allActive) {
382
+ if (!this.activeTerminalReasons.has(queueKey))
383
+ continue;
384
+ this.activeBatches.delete(queueKey);
385
+ const existingBarrier = this.interruptBarriers.get(queueKey);
386
+ if (existingBarrier)
387
+ barriers.push(existingBarrier);
388
+ }
389
+ const persisted = this.persistQueuesSync() || this.persistQueuesSync();
390
+ if (!persisted) {
391
+ logger.error('[Queue] CRITICAL: graceful shutdown snapshot could not be persisted');
392
+ }
393
+ for (const [queueKey, item] of active) {
394
+ const sessionKey = this.sessionKeyFromQueueKey(queueKey);
395
+ const agentName = this.processingAgent.get(queueKey) || item.agentName;
396
+ this.eventBus?.publish({
397
+ type: 'task:interrupted',
398
+ sessionId: sessionKey,
399
+ reason: 'daemon_restart',
400
+ agentName,
401
+ causation: item.message.causation,
402
+ });
403
+ barriers.push(this.triggerInterrupt(queueKey, sessionKey, this.activeStates.get(queueKey)?.baseagent, agentName, 'daemon_restart'));
404
+ }
405
+ this.shutdownPromise = (async () => {
406
+ let timedOut = false;
407
+ let timer;
408
+ if (barriers.length > 0) {
409
+ await Promise.race([
410
+ Promise.allSettled(barriers),
411
+ new Promise(resolve => {
412
+ timer = setTimeout(() => { timedOut = true; resolve(); }, Math.max(1, timeoutMs));
413
+ }),
414
+ ]);
415
+ }
416
+ if (timer)
417
+ clearTimeout(timer);
418
+ if (timedOut) {
419
+ logger.warn(`[Queue] Graceful shutdown interrupt barrier timed out after ${timeoutMs}ms`);
420
+ }
421
+ // 发布 interrupt 前的同步写盘是唯一恢复点。runner 收尾可能继续修改
422
+ // handler 持有的 message 对象,不能在这里用可变对象覆盖冻结快照。
423
+ logger.info(`[Queue] Graceful shutdown froze ${active.length} active task(s)`);
424
+ return active.length;
425
+ })();
426
+ return this.shutdownPromise;
427
+ }
329
428
  persistQueuesSync() {
330
429
  if (!this.persistencePath)
331
430
  return true;
@@ -430,10 +529,15 @@ export class MessageQueue {
430
529
  if (rawParts.length === 0)
431
530
  continue;
432
531
  const first = rawParts[0];
433
- const submittedPart = first.message.handoffDelivery
434
- ? this.restoredPart(first)
435
- : this.restoreSubmittedPart(first);
436
- const submittedItem = this.itemFromPart(submittedPart);
532
+ const restoredOriginParts = rawParts.length > 1
533
+ && rawParts.every(part => this.isOriginHandoff(part.message))
534
+ ? rawParts.map(part => this.restoredPart(part))
535
+ : undefined;
536
+ const submittedItem = restoredOriginParts
537
+ ? this.buildCoalescedItem(restoredOriginParts)
538
+ : this.itemFromPart(first.message.handoffDelivery
539
+ ? this.restoredPart(first)
540
+ : this.restoreSubmittedPart(first));
437
541
  submittedItem.message.timestamp = first.message.timestamp;
438
542
  queue.push(submittedItem);
439
543
  for (const part of rawParts) {
@@ -505,6 +609,11 @@ export class MessageQueue {
505
609
  return false;
506
610
  }
507
611
  async enqueue(sessionKey, message, projectPath, options) {
612
+ if (this.shuttingDown) {
613
+ const error = new Error('Daemon is shutting down');
614
+ error.code = 'DAEMON_SHUTTING_DOWN';
615
+ return Promise.reject(error);
616
+ }
508
617
  if (options?.signal?.aborted) {
509
618
  return Promise.reject(abortError(options.signal.reason));
510
619
  }
@@ -680,6 +789,11 @@ export class MessageQueue {
680
789
  this.processing.add(queueKey);
681
790
  logger.info(`[Queue] processNext: start key=${queueKey}`);
682
791
  while (true) {
792
+ if (this.shuttingDown) {
793
+ this.processing.delete(queueKey);
794
+ this.processingAgent.delete(queueKey);
795
+ return;
796
+ }
683
797
  const interruptBarrier = this.interruptBarriers.get(queueKey);
684
798
  if (interruptBarrier) {
685
799
  logger.debug(`[Queue] Waiting for interrupt barrier on ${queueKey}`);
@@ -691,6 +805,11 @@ export class MessageQueue {
691
805
  logger.debug(`[Queue] Waiting for external lock on ${queueKey}`);
692
806
  await lock;
693
807
  }
808
+ if (this.shuttingDown) {
809
+ this.processing.delete(queueKey);
810
+ this.processingAgent.delete(queueKey);
811
+ return;
812
+ }
694
813
  const queue = this.queues.get(queueKey);
695
814
  if (!queue || queue.length === 0) {
696
815
  logger.info(`[Queue] processNext: queue empty, releasing key=${queueKey}`);
@@ -766,10 +885,18 @@ export class MessageQueue {
766
885
  rejects.forEach(r => r(error));
767
886
  }
768
887
  finally {
769
- this.activeBatches.delete(queueKey);
770
- this.processingStartTime.delete(queueKey);
771
- this.activeStates.delete(queueKey);
772
- this.persistQueuesImmediate();
888
+ if (!this.shuttingDown) {
889
+ this.activeBatches.delete(queueKey);
890
+ this.processingStartTime.delete(queueKey);
891
+ this.activeStates.delete(queueKey);
892
+ this.activeTerminalReasons.delete(queueKey);
893
+ this.persistQueuesImmediate();
894
+ }
895
+ }
896
+ if (this.shuttingDown) {
897
+ this.processing.delete(queueKey);
898
+ this.processingAgent.delete(queueKey);
899
+ return;
773
900
  }
774
901
  }
775
902
  }
@@ -781,6 +908,15 @@ export class MessageQueue {
781
908
  dequeueGreedy(queue) {
782
909
  const first = queue.shift();
783
910
  const result = [first];
911
+ if (this.isOriginHandoff(first.message)) {
912
+ while (queue.length > 0 && this.canMergeOriginHandoffs(first, queue[0])) {
913
+ result.push(queue.shift());
914
+ }
915
+ if (result.length > 1) {
916
+ logger.info(`[Queue] Greedy dequeue: merged ${result.length} pending origin handoff result(s)`);
917
+ }
918
+ return result;
919
+ }
784
920
  if (this.isMergeBarrier(first.message))
785
921
  return result;
786
922
  const mergeSubmittedResume = first.message.restartResume?.submitted === true;
@@ -833,6 +969,24 @@ export class MessageQueue {
833
969
  isMergeBarrier(message) {
834
970
  return !!message.handoffDelivery || !!message.triggerMeta || !!message.causation?.trigger?.path.length;
835
971
  }
972
+ isOriginHandoff(message) {
973
+ return message.source === 'handoff' && message.handoffDelivery?.direction === 'origin';
974
+ }
975
+ canMergeOriginHandoffs(first, next) {
976
+ if (!this.isOriginHandoff(next.message))
977
+ return false;
978
+ if (first.projectPath !== next.projectPath || first.agentName !== next.agentName)
979
+ return false;
980
+ const a = first.message.handoffDelivery?.originAuthorization;
981
+ const b = next.message.handoffDelivery?.originAuthorization;
982
+ if (!a || !b)
983
+ return a === b;
984
+ return a.actorId === b.actorId
985
+ && a.channelKey === b.channelKey
986
+ && a.channelType === b.channelType
987
+ && a.chatType === b.chatType
988
+ && a.peerKey === b.peerKey;
989
+ }
836
990
  firstTimestamp(item) {
837
991
  const parts = this.partsOf(item);
838
992
  let ts = Number.POSITIVE_INFINITY;
@@ -856,6 +1010,7 @@ export class MessageQueue {
856
1010
  * - items: 保留每条子消息(含各自 peer/timestamp),供消息渲染层逐条渲染
857
1011
  * - images / mentions: 扁平合并
858
1012
  * - messageId: 取最新一条的 messageId(用于 thought 锚定与中断追踪)
1013
+ * - sourceMessageIds: 保留全部原始入站消息 ID(用于渠道确认状态批量迁移)
859
1014
  * - replyContext / peerName / 其余字段: 取最后一条
860
1015
  */
861
1016
  mergeItems(items) {
@@ -864,8 +1019,13 @@ export class MessageQueue {
864
1019
  const allMentions = [];
865
1020
  const subMessages = [];
866
1021
  let mergedPeerType;
1022
+ const sourceMessageIds = [];
1023
+ const seenSourceMessageIds = new Set();
867
1024
  let hasRestartResume = false;
868
1025
  let hasMessagesAfterRestartResume = false;
1026
+ const originHandoffIds = [];
1027
+ const seenOriginHandoffIds = new Set();
1028
+ const allOriginHandoffs = items.every(item => this.isOriginHandoff(item.message));
869
1029
  for (const item of items) {
870
1030
  const m = item.message;
871
1031
  if (hasRestartResume && !m.restartResume?.submitted) {
@@ -879,6 +1039,23 @@ export class MessageQueue {
879
1039
  allImages.push(...m.images);
880
1040
  if (m.mentions)
881
1041
  allMentions.push(...m.mentions);
1042
+ if (allOriginHandoffs && m.handoffDelivery) {
1043
+ const ids = m.handoffDelivery.handoffIds?.length
1044
+ ? m.handoffDelivery.handoffIds
1045
+ : [m.handoffDelivery.handoffId];
1046
+ for (const id of ids) {
1047
+ if (!seenOriginHandoffIds.has(id)) {
1048
+ seenOriginHandoffIds.add(id);
1049
+ originHandoffIds.push(id);
1050
+ }
1051
+ }
1052
+ }
1053
+ for (const id of [...(m.sourceMessageIds ?? []), ...(m.messageId ? [m.messageId] : [])]) {
1054
+ if (!seenSourceMessageIds.has(id)) {
1055
+ seenSourceMessageIds.add(id);
1056
+ sourceMessageIds.push(id);
1057
+ }
1058
+ }
882
1059
  if (m.peerType && m.peerType !== 'human') {
883
1060
  mergedPeerType = m.peerType;
884
1061
  }
@@ -919,6 +1096,7 @@ export class MessageQueue {
919
1096
  images: allImages.length > 0 ? allImages : undefined,
920
1097
  mentions: allMentions.length > 0 ? allMentions : undefined,
921
1098
  messageId: latestMessageId,
1099
+ sourceMessageIds: sourceMessageIds.length > 0 ? sourceMessageIds : undefined,
922
1100
  // 密文优先:合并批次内任一条密文,则整轮(interactive 自动回复的单值出站)按密文。
923
1101
  // 任一条 encrypted===true 即 true;否则若有明确的 false 则 false;全 undefined(非 aun)则 undefined。
924
1102
  encrypted: subMessages.some(s => s.encrypted === true)
@@ -929,6 +1107,13 @@ export class MessageQueue {
929
1107
  restartResume: hasRestartResume
930
1108
  ? { submitted: true, pendingInterrupted: hasMessagesAfterRestartResume }
931
1109
  : undefined,
1110
+ handoffDelivery: allOriginHandoffs && originHandoffIds.length > 0
1111
+ ? {
1112
+ ...last.message.handoffDelivery,
1113
+ handoffId: originHandoffIds[0],
1114
+ handoffIds: originHandoffIds,
1115
+ }
1116
+ : last.message.handoffDelivery,
932
1117
  };
933
1118
  return {
934
1119
  message: merged,
@@ -8,8 +8,10 @@ import { renderCommandCardAsText, renderActionAsText } from '../interaction-rout
8
8
  * 构造出站消息信封
9
9
  */
10
10
  export function buildEnvelope(opts) {
11
+ const taskId = opts.taskId ?? `interaction-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
11
12
  return {
12
- taskId: opts.taskId ?? `interaction-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
13
+ taskId,
14
+ operationId: opts.operationId ?? taskId,
13
15
  sessionId: opts.sessionId,
14
16
  channel: opts.channel,
15
17
  channelId: opts.channelId,
@@ -61,7 +63,11 @@ export async function sendInteractionPayload(adapter, envelope, interaction, fal
61
63
  const enriched = replyCtx
62
64
  ? { ...envelope, replyContext: replyCtx }
63
65
  : envelope;
64
- await adapter.send(enriched, payload);
66
+ const receipt = await adapter.send(enriched, payload);
67
+ if (receipt?.status === 'suppressed')
68
+ return false;
69
+ if (receipt?.status === 'sent')
70
+ return receipt.messages[0]?.messageId || false;
65
71
  return 'sent';
66
72
  }
67
73
  catch {
@@ -77,21 +77,20 @@ export function resolveChatModeForPeer(params) {
77
77
  const field = chatmodeFieldForPeer(params.chatType, params.peerType);
78
78
  return params.configured?.[field] ?? chatmodeFactoryDefaults()[field];
79
79
  }
80
- let _factoryChatmode = null;
81
80
  /**
82
81
  * chatMode 出厂默认表 —— 唯一来源是 agent-config schema 的 chatmode.default
83
82
  * (配置数据,非代码分支)。agent 级配置改默认表、关系级按键覆盖,均经
84
83
  * ConfigManager 合并后以 `configured` 传入;此表仅在配置链完全未设值时兜底。
84
+ *
85
+ * 不做二级 memo:`loadSchema` 自己走 FileCache,这里再存一份等于绕过
86
+ * invalidateKitCache——schema 换了新的,出厂默认表却永久停在进程启动那一刻。
85
87
  */
86
88
  export function chatmodeFactoryDefaults() {
87
- if (!_factoryChatmode) {
88
- const d = loadSchema('agent-config').fields.get('chatmode')?.default;
89
- if (!d?.private || !d?.group || !d?.nothuman) {
90
- throw new Error('[peer-mode] agent-config schema 缺少 chatmode 出厂默认表(default.private/group/nothuman)');
91
- }
92
- _factoryChatmode = d;
89
+ const d = loadSchema('agent-config').fields.get('chatmode')?.default;
90
+ if (!d?.private || !d?.group || !d?.nothuman) {
91
+ throw new Error('[peer-mode] agent-config schema 缺少 chatmode 出厂默认表(default.private/group/nothuman)');
93
92
  }
94
- return _factoryChatmode;
93
+ return { ...d }; // d 挂在缓存的 schema 对象上,拷一层避免调用方改写污染缓存
95
94
  }
96
95
  export function chatmodeFieldForPeer(chatType, peerType) {
97
96
  const kind = normalizePeerType(peerType);