evolcore 0.0.24 → 0.0.26

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 (112) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/bin/ec.js +12 -1
  3. package/bin/install-codex-managed-hooks.mjs +28 -1
  4. package/dist/agents/baseagent.js +96 -46
  5. package/dist/agents/claude-runner.js +54 -13
  6. package/dist/agents/codex/app-server-client.js +13 -5
  7. package/dist/agents/codex/catalog.js +1 -1
  8. package/dist/agents/codex/diagnostics.js +34 -1
  9. package/dist/agents/codex-runner.js +128 -42
  10. package/dist/agents/ecagent-runner.js +14 -18
  11. package/dist/agents/gemini-runner.js +24 -9
  12. package/dist/agents/runner-types.js +25 -17
  13. package/dist/aun/aid/agentmd.js +135 -10
  14. package/dist/aun/aid/client.js +1 -1
  15. package/dist/aun/aid/identity.js +1 -1
  16. package/dist/aun/aid/index.js +2 -1
  17. package/dist/aun/aid/managed-operation.js +53 -1
  18. package/dist/aun/aid/store.js +17 -13
  19. package/dist/aun/menu-client.js +74 -0
  20. package/dist/aun/msg/group.js +1 -1
  21. package/dist/aun/msg/managed-operation.js +1 -1
  22. package/dist/aun/msg/p2p.js +6 -1
  23. package/dist/aun/msg/upload.js +30 -22
  24. package/dist/aun/storage/client.js +4 -1
  25. package/dist/aun/storage/index.js +1 -1
  26. package/dist/channels/aun.js +329 -94
  27. package/dist/cli/agent-command.js +125 -5
  28. package/dist/cli/agent-create.js +151 -11
  29. package/dist/cli/agent.js +119 -6
  30. package/dist/cli/aun-commands.js +74 -7
  31. package/dist/cli/config.js +27 -8
  32. package/dist/cli/contact.js +63 -6
  33. package/dist/cli/daemon-commands.js +90 -25
  34. package/dist/cli/diag.js +31 -0
  35. package/dist/cli/handoff-command.js +35 -3
  36. package/dist/cli/index.js +7 -1
  37. package/dist/cli/managed-command-guard.js +3 -0
  38. package/dist/cli/menu-client.js +38 -23
  39. package/dist/cli/restart-monitor.js +57 -10
  40. package/dist/config/builtin-roles.js +2 -0
  41. package/dist/config/config-manager.js +15 -0
  42. package/dist/config/config-operation-service.js +165 -3
  43. package/dist/config/contact-operation-service.js +96 -7
  44. package/dist/config/resolved-config-op.js +66 -8
  45. package/dist/core/agent-application-service.js +2 -1
  46. package/dist/core/auth/authorization-audit.js +20 -12
  47. package/dist/core/auth/operation-authorizer.js +87 -8
  48. package/dist/core/auth/operation-catalog.js +51 -4
  49. package/dist/core/auth/remote-access-store.js +40 -1
  50. package/dist/core/bootstrap-service.js +1 -1
  51. package/dist/core/command/agent-control.js +54 -25
  52. package/dist/core/command/cli-intent-parser.js +18 -0
  53. package/dist/core/command/command-handler.js +152 -20
  54. package/dist/core/command/diag-service.js +118 -0
  55. package/dist/core/command/menu-catalog.js +13 -3
  56. package/dist/core/command/menu-handler.js +88 -31
  57. package/dist/core/command/menu-protocol.js +6 -0
  58. package/dist/core/command/menu-token-store.js +41 -8
  59. package/dist/core/command/slash-gate.js +34 -4
  60. package/dist/core/command/slash-handler.js +77 -12
  61. package/dist/core/event-catalog.js +5 -0
  62. package/dist/core/evolagent.js +3 -3
  63. package/dist/core/handoff/runtime.js +205 -27
  64. package/dist/core/handoff/store.js +193 -4
  65. package/dist/core/message/im-renderer.js +72 -21
  66. package/dist/core/message/message-bridge.js +69 -9
  67. package/dist/core/message/message-queue.js +26 -0
  68. package/dist/core/message/message-utils.js +1 -0
  69. package/dist/core/message/response-engine.js +48 -23
  70. package/dist/core/model/catalog-service.js +79 -49
  71. package/dist/core/model/context-window-suffix.js +33 -0
  72. package/dist/core/model/model-catalog.js +1 -1
  73. package/dist/core/model/model-permission.js +12 -3
  74. package/dist/core/permission/ec-command-parser.js +51 -12
  75. package/dist/core/permission/sandbox-runtime.js +36 -14
  76. package/dist/core/permission/tool-policy.js +47 -7
  77. package/dist/index.js +195 -31
  78. package/dist/ipc.js +54 -2
  79. package/dist/response-system/engines/v1/proactive-flow.js +59 -2
  80. package/dist/stats/billing.js +41 -15
  81. package/dist/stats/price-resolver.js +28 -10
  82. package/dist/utils/avatar-upload.js +282 -23
  83. package/dist/utils/cross-platform.js +30 -1
  84. package/dist/utils/error-utils.js +21 -0
  85. package/dist/utils/evolcore-version.js +129 -0
  86. package/dist/utils/logger.js +3 -1
  87. package/dist/utils/model-prices.jsonl +3 -3
  88. package/dist/utils/npm-ops.js +176 -35
  89. package/kits/docs/context-assembly.md +1 -0
  90. package/kits/docs/evolcore/INDEX.md +3 -1
  91. package/kits/docs/evolcore/agent-create.md +30 -11
  92. package/kits/docs/evolcore/agent.md +18 -8
  93. package/kits/docs/evolcore/aid.md +6 -4
  94. package/kits/docs/evolcore/codex-windows-sandbox.md +146 -0
  95. package/kits/docs/evolcore/config.md +7 -0
  96. package/kits/docs/evolcore/contact.md +12 -5
  97. package/kits/docs/evolcore/menu-client.md +11 -3
  98. package/kits/docs/evolcore/msg.md +1 -0
  99. package/kits/docs/prompt-loading-architecture.md +1 -0
  100. package/kits/docs/venues/group.md +12 -0
  101. package/kits/eck_manifest.json +12 -0
  102. package/kits/rules/03-identity.md +3 -1
  103. package/kits/templates/message-fragments/handoff-request-to-target.md +3 -3
  104. package/kits/templates/roles/admin.json +14 -2
  105. package/kits/templates/roles/member.json +3 -2
  106. package/kits/templates/roles/visitor.json +4 -3
  107. package/kits/templates/system-fragments/bootstrap.md +16 -5
  108. package/kits/templates/system-fragments/channel.md +1 -1
  109. package/kits/templates/system-fragments/session.md +1 -1
  110. package/package.json +6 -4
  111. package/skills/eclink/SKILL.md +0 -84
  112. package/skills/eclink/agents/openai.yaml +0 -4
@@ -4,6 +4,7 @@ import { performance } from 'node:perf_hooks';
4
4
  import fs from 'fs';
5
5
  import path from 'path';
6
6
  import os from 'os';
7
+ import { fileURLToPath, pathToFileURL } from 'url';
7
8
  import { logger, localTimestamp } from '../utils/logger.js';
8
9
  import { LogWriter } from '../utils/log-writer.js';
9
10
  import { middleOutputModePolicy, resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
@@ -33,6 +34,7 @@ import { deriveCausation, normalizeCausation } from '../core/causation/context.j
33
34
  import { recordCausationSpan } from '../core/causation/audit.js';
34
35
  import { refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
35
36
  import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
37
+ import { ManagedMenuClient } from '../aun/menu-client.js';
36
38
  import { toolInputForDisplay } from '../utils/tool-summary.js';
37
39
  import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, pendingPostBootstrapWelcomeOwners, isPostBootstrapWelcomeOperationId, findPostBootstrapWelcomeOutbox, bootstrapInitialMessageOperationPrefix, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
38
40
  import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
@@ -52,6 +54,51 @@ const AUN_SEND_LOG_TEXT_MAX_LENGTH = 60;
52
54
  export function formatAunSendLogText(text) {
53
55
  return text.replace(/\s+/gu, ' ').trim().slice(0, AUN_SEND_LOG_TEXT_MAX_LENGTH);
54
56
  }
57
+ /**
58
+ * Echo trace 行的墙钟前缀。
59
+ *
60
+ * 格式必须保持 `HH:MM:SS.mmm` 且毫秒后紧跟空格——Evol App 的
61
+ * `evol/lib/utils/echo_utils.dart` 用 `^(\d{2}):(\d{2}):(\d{2})\.(\d{3})\s` 匹配这一段,
62
+ * 在毫秒和空格之间插任何东西(比如时区偏移)都会让旧版 App 静默失配、不再显示 RTT。
63
+ * 时区信息改由行尾的 {@link echoTraceOriginLabel} 提供,前缀一字不动,两端发版顺序因此解耦。
64
+ */
65
+ export function echoTraceClock(now = Date.now()) {
66
+ const d = new Date(now);
67
+ const pad = (n, width = 2) => String(n).padStart(width, '0');
68
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
69
+ }
70
+ /**
71
+ * Echo trace 行尾的权威时间标记:`epoch=<ms> tz=<±HH:MM>`。
72
+ *
73
+ * 前缀的墙钟没有日期也没有时区,消费方只能按自己的本地时区解析,两端 TZ 不一致时算出的
74
+ * RTT 会整小时偏移(UTC 对 Asia/Shanghai 就是 +28800000ms),跨午夜还会得到负值。
75
+ * `epoch` 是绝对值,消费方应当用它相减算延迟;`tz` 仅供人工核对墙钟,不参与计算。
76
+ */
77
+ export function echoTraceOriginLabel(now = Date.now()) {
78
+ const d = new Date(now);
79
+ const pad = (n, width = 2) => String(n).padStart(width, '0');
80
+ const offsetMinutes = -d.getTimezoneOffset();
81
+ const sign = offsetMinutes >= 0 ? '+' : '-';
82
+ const absOffset = Math.abs(offsetMinutes);
83
+ return ` epoch=${now} tz=${sign}${pad(Math.floor(absOffset / 60))}:${pad(absOffset % 60)}`;
84
+ }
85
+ /**
86
+ * 入站腿延迟(对端/网关发出 → 本进程收到)。
87
+ *
88
+ * `created_at` 由对端或网关打戳,因此结果含对端时钟漂移,只能当估算值——标签用
89
+ * `inbound_est` 而非 `inbound` 就是为了让消费方知道这点。负值说明对端时钟快于本机,
90
+ * 照实输出而不夹到 0:那本身就是有用的时钟偏移信号。
91
+ */
92
+ export function echoInboundLegLabel(createdAt, receivedAt) {
93
+ if (typeof createdAt !== 'number' || !Number.isFinite(createdAt) || createdAt <= 0)
94
+ return '';
95
+ // 网关历史上有按秒打戳的实现,按量级归一到毫秒。
96
+ const createdMs = createdAt < 1e12 ? createdAt * 1000 : createdAt;
97
+ const delta = receivedAt - createdMs;
98
+ if (!Number.isFinite(delta))
99
+ return '';
100
+ return ` inbound_est=${Math.round(delta)}ms`;
101
+ }
55
102
  function attachmentDownloadHost(url) {
56
103
  try {
57
104
  return new URL(url).host || '<unknown>';
@@ -431,6 +478,8 @@ export function aunOptsToInbound(opts, channel, channelType) {
431
478
  content: opts.content,
432
479
  chatType: opts.chatType || 'private',
433
480
  peerId: opts.peerId || '',
481
+ peerDeviceId: opts.peerDeviceId,
482
+ peerSlotId: opts.peerSlotId,
434
483
  peerName: opts.peerName,
435
484
  peerType: opts.peerType,
436
485
  sameDevice: opts.sameDevice,
@@ -502,9 +551,26 @@ function payloadTypeForObserver(value) {
502
551
  const type = payload.type;
503
552
  return typeof type === 'string' ? type : undefined;
504
553
  }
554
+ function errorCodeForLog(error) {
555
+ if (!error || typeof error !== 'object')
556
+ return '';
557
+ const value = error;
558
+ return typeof value.code === 'string' || typeof value.code === 'number' ? String(value.code) : '';
559
+ }
505
560
  export class AUNChannel {
506
561
  config;
562
+ managedMenuClient = new ManagedMenuClient(params => {
563
+ if (!this.connected || !this.client)
564
+ return Promise.reject(new Error('AUN channel is not connected'));
565
+ return this.callAndTrace('message.send', params);
566
+ });
567
+ requestRemoteMenu(options) {
568
+ if (options.fromAid !== (this._aid || this.config.aid))
569
+ throw new Error('Menu sender does not match channel identity');
570
+ return this.managedMenuClient.request(options);
571
+ }
507
572
  client = null;
573
+ localSenderInstallation;
508
574
  store = null;
509
575
  /** 实际连接的网关 URL(来自 authenticate() 返回值 / connection.state 事件),替代旧 (client as any)._gatewayUrl。 */
510
576
  gatewayUrl = '';
@@ -562,46 +628,88 @@ export class AUNChannel {
562
628
  * 统一的 RPC 调用包装:自动记录 OUT 发送、.ok 结果、.error 错误(含 trace + daemon.log 失败日志)。
563
629
  * 所有 client.call() 都应通过此方法调用,保证 aun-trace 里每个 OUT 调用都有"发+收/错"成对记录。
564
630
  */
565
- callClient(method, params) {
566
- return this.withOutboundSendGate(method, () => (this.client.call(method, params).then(value => value)));
631
+ callClient(method, params, lane = 'normal', onStart) {
632
+ return this.withOutboundSendGate(method, () => (this.client.call(method, params).then(value => value)), lane, onStart);
567
633
  }
568
- withOutboundSendGate(method, run) {
569
- if (!AUNChannel.OUTBOUND_SEND_METHODS.has(method))
634
+ withOutboundSendGate(method, run, lane = 'normal', onStart) {
635
+ if (!AUNChannel.OUTBOUND_SEND_METHODS.has(method)) {
636
+ onStart?.();
570
637
  return run();
638
+ }
639
+ const queue = lane === 'observer' ? this.observerSendQueue : this.outboundSendQueue;
640
+ if (lane === 'observer' && queue.length >= AUNChannel.OBSERVER_SEND_QUEUE_MAX) {
641
+ this.noteObserverQueueDrop();
642
+ const error = Object.assign(new Error('observer send queue is full'), {
643
+ code: 'AUN_OBSERVER_QUEUE_FULL',
644
+ });
645
+ return Promise.reject(error);
646
+ }
571
647
  return new Promise((resolve, reject) => {
572
- this.outboundSendQueue.push({
648
+ queue.push({
573
649
  run: run,
574
650
  resolve: resolve,
575
651
  reject,
652
+ onStart,
576
653
  });
577
654
  this.pumpOutboundSendGate();
578
655
  });
579
656
  }
580
657
  pumpOutboundSendGate() {
581
- while (this.outboundSendActive < AUNChannel.OUTBOUND_SEND_CONCURRENCY
582
- && this.outboundSendQueue.length > 0) {
583
- const job = this.outboundSendQueue.shift();
658
+ while (this.outboundSendActive + this.observerSendActive < AUNChannel.OUTBOUND_SEND_CONCURRENCY && this.outboundSendQueue.length > 0) {
659
+ this.startOutboundSendJob(this.outboundSendQueue.shift(), 'normal');
660
+ }
661
+ while (this.outboundSendActive + this.observerSendActive < AUNChannel.OUTBOUND_SEND_CONCURRENCY
662
+ && this.observerSendActive < AUNChannel.OBSERVER_SEND_CONCURRENCY
663
+ && this.outboundSendQueue.length === 0
664
+ && this.observerSendQueue.length > 0) {
665
+ this.startOutboundSendJob(this.observerSendQueue.shift(), 'observer');
666
+ }
667
+ }
668
+ clearQueuedObserverSends(reason) {
669
+ if (this.observerSendQueue.length === 0)
670
+ return;
671
+ const error = Object.assign(new Error(`observer send discarded while ${reason}`), {
672
+ code: 'AUN_OBSERVER_QUEUE_DROPPED',
673
+ });
674
+ const queued = this.observerSendQueue.splice(0);
675
+ for (const job of queued)
676
+ job.reject(error);
677
+ this.noteObserverQueueDrop(queued.length);
678
+ }
679
+ startOutboundSendJob(job, lane) {
680
+ if (lane === 'observer')
681
+ this.observerSendActive++;
682
+ else
584
683
  this.outboundSendActive++;
585
- Promise.resolve()
586
- .then(job.run)
587
- .then(job.resolve, job.reject)
588
- .finally(() => {
684
+ Promise.resolve()
685
+ .then(() => {
686
+ job.onStart?.();
687
+ return job.run();
688
+ })
689
+ .then(job.resolve, job.reject)
690
+ .finally(() => {
691
+ if (lane === 'observer')
692
+ this.observerSendActive--;
693
+ else
589
694
  this.outboundSendActive--;
590
- this.pumpOutboundSendGate();
591
- });
592
- }
695
+ this.pumpOutboundSendGate();
696
+ });
593
697
  }
594
698
  async callAndTrace(method, params, opts) {
595
699
  this.trace('OUT', method, params);
596
- // RPC 往返计时:区分「网关慢」与「本地队列堵塞」。message.send/group.send 的
597
- // duration 若普遍逼近 SDK 的 rpc 超时(默认 10s),说明发送管线已饱和或链路抖动,
598
- // 是 /evolhelp 等命令「回复慢/无回复」的直接可观测信号。
700
+ // 计时同时覆盖排队和 SDK 往返;callStartedAt 让日志能区分「本地队列堵塞」
701
+ // 与「网关慢」。两者对 /evolhelp 等命令的回复延迟都有直接影响。
599
702
  const rpcStart = Date.now();
600
703
  // SLOW_RPC_WARN_MS 需明显低于 SDK 默认 10s 超时,以便在真正超时前提前告警。
601
704
  const SLOW_RPC_WARN_MS = 3000;
705
+ let callStartedAt = rpcStart;
602
706
  try {
603
- const result = await this.callClient(method, params);
707
+ const result = await this.callClient(method, params, opts?.lane ?? 'normal', () => {
708
+ callStartedAt = Date.now();
709
+ });
604
710
  const durationMs = Date.now() - rpcStart;
711
+ const rpcDurationMs = Date.now() - callStartedAt;
712
+ const queueWaitMs = Math.max(0, callStartedAt - rpcStart);
605
713
  if (!opts?.silentOk) {
606
714
  const r = result;
607
715
  const snap = r && typeof r === 'object'
@@ -609,25 +717,66 @@ export class AUNChannel {
609
717
  : { duration_ms: durationMs };
610
718
  this.trace('OUT', `${method}.ok`, snap);
611
719
  }
612
- if (durationMs >= SLOW_RPC_WARN_MS) {
613
- logger.warn(`${this.logPrefix()} rpc ${method} SLOW: duration=${durationMs}ms (发送管线可能饱和或链路抖动)`);
614
- }
720
+ if (durationMs >= SLOW_RPC_WARN_MS)
721
+ this.logRpcWarning('slow', method, params, durationMs, queueWaitMs, rpcDurationMs);
615
722
  return result;
616
723
  }
617
724
  catch (e) {
618
725
  const durationMs = Date.now() - rpcStart;
726
+ const queueWaitMs = Math.max(0, callStartedAt - rpcStart);
727
+ const rpcDurationMs = Math.max(0, Date.now() - callStartedAt);
619
728
  this.trace('OUT', `${method}.error`, {
620
729
  error: e?.message ?? String(e),
621
730
  code: e?.code,
622
731
  name: e?.name,
623
732
  duration_ms: durationMs,
624
733
  });
625
- if (!opts?.silentError) {
626
- logger.warn(`${this.logPrefix()} rpc ${method} failed after ${durationMs}ms: ${e?.name ?? ''}(${e?.code ?? ''}) ${e?.message ?? e}`);
734
+ if (!opts?.silentError && e?.code !== 'AUN_OBSERVER_QUEUE_FULL' && e?.code !== 'AUN_OBSERVER_QUEUE_DROPPED') {
735
+ this.logRpcWarning('failed', method, params, durationMs, queueWaitMs, rpcDurationMs, e);
627
736
  }
628
737
  throw e;
629
738
  }
630
739
  }
740
+ logRpcWarning(kind, method, params, durationMs, queueWaitMs, rpcDurationMs, error) {
741
+ const payloadType = payloadTypeForObserver(params) ?? 'unknown';
742
+ const code = kind === 'failed' ? errorCodeForLog(error) : '';
743
+ const key = `${kind}|${method}|${payloadType}`;
744
+ const now = Date.now();
745
+ const state = this.rpcWarningStates.get(key) ?? { lastWarnAt: 0, suppressed: 0, maxDurationMs: 0 };
746
+ state.maxDurationMs = Math.max(state.maxDurationMs, durationMs);
747
+ if (state.lastWarnAt > 0 && now - state.lastWarnAt < AUNChannel.RPC_WARN_INTERVAL_MS) {
748
+ state.suppressed++;
749
+ this.rpcWarningStates.set(key, state);
750
+ return;
751
+ }
752
+ const suppressed = state.suppressed;
753
+ const maxDurationMs = state.maxDurationMs;
754
+ state.lastWarnAt = now;
755
+ state.suppressed = 0;
756
+ state.maxDurationMs = 0;
757
+ this.rpcWarningStates.set(key, state);
758
+ const summary = suppressed > 0 ? ` suppressed=${suppressed} max_duration=${maxDurationMs}ms` : '';
759
+ if (kind === 'failed') {
760
+ const name = error instanceof Error ? error.name : '';
761
+ const message = error instanceof Error ? error.message : String(error ?? '');
762
+ const errorLabel = `${name}${code ? `(${code})` : ''} ${message}`.trim();
763
+ logger.warn(`${this.logPrefix()} rpc ${method} failed after ${durationMs}ms: ${errorLabel} queue_wait=${queueWaitMs}ms rpc=${rpcDurationMs}ms payload=${payloadType}${summary}`);
764
+ return;
765
+ }
766
+ logger.warn(`${this.logPrefix()} rpc ${method} SLOW: duration=${durationMs}ms queue_wait=${queueWaitMs}ms rpc=${rpcDurationMs}ms payload=${payloadType}${summary} (发送管线可能饱和或链路抖动)`);
767
+ }
768
+ observerQueueDropCount = 0;
769
+ observerQueueDropLastWarnAt = 0;
770
+ noteObserverQueueDrop(count = 1) {
771
+ this.observerQueueDropCount += count;
772
+ const now = Date.now();
773
+ if (now - this.observerQueueDropLastWarnAt < AUNChannel.RPC_WARN_INTERVAL_MS)
774
+ return;
775
+ const dropped = this.observerQueueDropCount;
776
+ this.observerQueueDropCount = 0;
777
+ this.observerQueueDropLastWarnAt = now;
778
+ logger.warn(`${this.logPrefix()} observer send queue full: dropped=${dropped} (observer telemetry is being sampled while the gateway is unavailable)`);
779
+ }
631
780
  /** Resolve the authenticated transport route for an ordinary outbound send.
632
781
  * The channel id is an address only; it is intentionally never used to infer
633
782
  * whether the address is a group.
@@ -693,7 +842,13 @@ export class AUNChannel {
693
842
  context.threadId = threadId;
694
843
  if (operatorId && delivery.chatType === 'group')
695
844
  context.peerId = operatorId;
696
- void this.sendMessage(channelId, text, context).catch((err) => {
845
+ const payload = {
846
+ type: 'notice',
847
+ ...this.buildTaskPayloadBase({}, context),
848
+ text,
849
+ severity: 'info',
850
+ };
851
+ void this.sendReliableStructured(channelId, payload, context, text).catch((err) => {
697
852
  logger.error(`${this.logPrefix()} Failed to send card action error:`, err);
698
853
  });
699
854
  }
@@ -713,6 +868,12 @@ export class AUNChannel {
713
868
  : typeof obj.card_message_id === 'string' ? obj.card_message_id : '';
714
869
  const cardInfo = cardMsgId ? this.cardMessageIdMap.get(cardMsgId) : undefined;
715
870
  const cardDelivery = cardInfo?.delivery ?? delivery;
871
+ const savedTopic = this.ownedCardTopics.get(cardMsgId);
872
+ // Keep the topic after the actionable mapping expires, including for
873
+ // older clients that omit thread_id in their callbacks.
874
+ const threadId = cardInfo?.replyContext?.threadId
875
+ ?? (savedTopic?.channelId === channelId ? savedTopic?.threadId : undefined)
876
+ ?? (typeof obj.thread_id === 'string' ? obj.thread_id : undefined);
716
877
  if (cardInfo) {
717
878
  // compose 按钮由新版客户端在卡片内采集输入;旧版客户端可继续从主编辑器
718
879
  // 采集。两者最终都回传 action_card_reply{behavior:"compose", text}。
@@ -728,10 +889,6 @@ export class AUNChannel {
728
889
  const actionValue = isComposeReply ? '_custom_input'
729
890
  : typeof obj.value === 'string' ? obj.value
730
891
  : typeof obj.action_value === 'string' ? obj.action_value : text;
731
- // Prefer the trusted context persisted with the card. A callback's
732
- // thread_id is client-controlled and may be absent or stale.
733
- const threadId = cardInfo?.replyContext?.threadId
734
- ?? (typeof obj.thread_id === 'string' ? obj.thread_id : undefined);
735
892
  // 卡片点击者身份:只信认证信封(senderAid 参数,由调用方从 msg.from / msg.sender_aid 提取)。
736
893
  // payload 自报字段(from / sender_aid / user_id)不可信,可被客户端伪造,不读取。
737
894
  // 两类卡片共用:CommandCard → 伪入站消息的 peerId,ActionInteraction → operatorId。
@@ -774,9 +931,11 @@ export class AUNChannel {
774
931
  peerName: typeof obj.label === 'string' ? obj.label : typeof obj.action_label === 'string' ? obj.action_label : undefined,
775
932
  messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
776
933
  threadId,
777
- replyContext: cardInfo?.replyContext ?? ((threadId || cardDelivery)
778
- ? { ...(threadId ? { threadId } : {}), ...(cardDelivery ? { delivery: cardDelivery } : {}) }
779
- : undefined),
934
+ replyContext: {
935
+ ...(cardInfo.replyContext ?? {}),
936
+ ...(threadId ? { threadId } : {}),
937
+ ...(cardDelivery ? { delivery: cardDelivery } : {}),
938
+ },
780
939
  source: 'card-trigger',
781
940
  })).catch((err) => {
782
941
  logger.error(`${this.logPrefix()} CommandCard handler failed: action=${actionValue} mid=${cardMsgId}`, err);
@@ -887,9 +1046,9 @@ export class AUNChannel {
887
1046
  }
888
1047
  }
889
1048
  else if (this.ownedCardMsgIds.has(cardMsgId)) {
890
- // 本 agent 发出的卡片,但 entry 已过期(24h TTL)
1049
+ // 本 agent 发出的卡片,但 entry 已过期或被清理。
891
1050
  logger.debug(`${this.logPrefix()} action_card_reply expired: cardMsgId=${cardMsgId}`);
892
- this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起', undefined, undefined, delivery);
1051
+ this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起', senderAid, threadId, delivery);
893
1052
  }
894
1053
  else {
895
1054
  // 非本 agent 发出的卡片(broadcast 模式下其他 agent 的卡)→ 静默忽略
@@ -1295,6 +1454,7 @@ export class AUNChannel {
1295
1454
  cardMessageIdMap = new Map();
1296
1455
  /** 本 agent 曾发出过的卡片 msgId(只增不删,用于区分"过期失效"vs"他人发的卡") */
1297
1456
  ownedCardMsgIds = new Set();
1457
+ ownedCardTopics = new Map();
1298
1458
  /** requestId → 已投递的卡片 message_id 集合(撤权/超时时撤回) */
1299
1459
  interactionCardMessageIds = new Map();
1300
1460
  /** 撤权可能先于 outbox 卡片投递完成;迟到卡片注册后立即撤回。 */
@@ -1304,7 +1464,7 @@ export class AUNChannel {
1304
1464
  groupSlashRequireMentionsResolver;
1305
1465
  static PROACTIVE_ALLOW_TYPES = new Set([
1306
1466
  'text', 'quote', 'image', 'video', 'voice', 'file', 'json',
1307
- 'merge', 'link', 'location', 'personal_card',
1467
+ 'merge', 'link', 'location', 'personal_card', 'contact.request.response',
1308
1468
  ]);
1309
1469
  /** 观察者插话请求类型(owner → agent.AID)。详见 docs/observer-insert-design.md。 */
1310
1470
  static INJECT_REQUEST_TYPE = 'observer.inject';
@@ -1344,7 +1504,7 @@ export class AUNChannel {
1344
1504
  aidState;
1345
1505
  aidStatsCollector;
1346
1506
  outboxInFlight = new Set();
1347
- /** Shared gateway-facing send limit for replies, activity, observer and thought messages. */
1507
+ /** Shared total gateway-facing send capacity; normal traffic has priority over observer telemetry. */
1348
1508
  static OUTBOUND_SEND_METHODS = new Set([
1349
1509
  'message.send',
1350
1510
  'group.send',
@@ -1352,8 +1512,14 @@ export class AUNChannel {
1352
1512
  'group.thought.put',
1353
1513
  ]);
1354
1514
  static OUTBOUND_SEND_CONCURRENCY = 4;
1515
+ static OBSERVER_SEND_CONCURRENCY = 1;
1516
+ static OBSERVER_SEND_QUEUE_MAX = 256;
1517
+ static RPC_WARN_INTERVAL_MS = 60_000;
1355
1518
  outboundSendActive = 0;
1356
1519
  outboundSendQueue = [];
1520
+ observerSendActive = 0;
1521
+ observerSendQueue = [];
1522
+ rpcWarningStates = new Map();
1357
1523
  constructor(config) {
1358
1524
  this.config = config;
1359
1525
  this.agentDir = agentDirPath(config.aid);
@@ -1425,6 +1591,27 @@ export class AUNChannel {
1425
1591
  this.initInProgress = false;
1426
1592
  }
1427
1593
  }
1594
+ /** Optional local compatibility code is never required by a clean checkout or npm install. */
1595
+ async loadLocalSenderInstallation(client) {
1596
+ this.localSenderInstallation = undefined;
1597
+ const currentFile = fileURLToPath(import.meta.url);
1598
+ const modulePath = path.join(path.dirname(currentFile), '..', 'aun', `sender-installation${path.extname(currentFile)}`);
1599
+ if (!fs.existsSync(modulePath))
1600
+ return;
1601
+ try {
1602
+ // Use a runtime URL so TypeScript can build when the local file is absent.
1603
+ const extension = await import(pathToFileURL(modulePath).href);
1604
+ if (typeof extension.enableAunSenderInstallation !== 'function'
1605
+ || typeof extension.aunSenderInstallation !== 'function') {
1606
+ throw new Error('Invalid local sender installation module');
1607
+ }
1608
+ extension.enableAunSenderInstallation(client);
1609
+ this.localSenderInstallation = extension;
1610
+ }
1611
+ catch (error) {
1612
+ logger.warn(`${this.logPrefix()} Optional local sender installation disabled: ${error instanceof Error ? error.message : String(error)}`);
1613
+ }
1614
+ }
1428
1615
  async _initClientInner() {
1429
1616
  this.observerGroupMembers.clear();
1430
1617
  for (const groupId of new Set([
@@ -1454,6 +1641,7 @@ export class AUNChannel {
1454
1641
  this.store = null;
1455
1642
  }
1456
1643
  this.connected = false;
1644
+ this.clearQueuedObserverSends('client reinitialization');
1457
1645
  this.lastConnectionStateSignature = '';
1458
1646
  const aunPath = this.config.keystorePath || resolveRoot();
1459
1647
  const aidName = this.config.aid;
@@ -1473,6 +1661,7 @@ export class AUNChannel {
1473
1661
  const client = await loadClient(store, aidName);
1474
1662
  enableFullGroupPullPagination(client);
1475
1663
  enableReliableMessageDelivery(client);
1664
+ await this.loadLocalSenderInstallation(client);
1476
1665
  this.client = client;
1477
1666
  // A reconnect replaces the SDK client. Late events from the retired
1478
1667
  // instance must not mutate the new instance's health or message state.
@@ -2115,6 +2304,15 @@ export class AUNChannel {
2115
2304
  return;
2116
2305
  const protectedHeaders = verifiedProtectedHeaders(msg, env);
2117
2306
  const payload = msg.payload ?? '';
2307
+ if (this.managedMenuClient.accept(fromAid, payload, !!env.encrypted)) {
2308
+ this.acknowledgeImmediately(messageId, seq);
2309
+ return;
2310
+ }
2311
+ if (payloadTypeForObserver(msg) === 'observer.forward') {
2312
+ this.acknowledgeImmediately(messageId, seq);
2313
+ logger.debug(`${this.logPrefix()} P2P dropped observer.forward control envelope: mid=${messageId}`);
2314
+ return;
2315
+ }
2118
2316
  const mentions = this.parsePayloadMentionsOrReject(payload, 'p2p.inbound', messageId, seq);
2119
2317
  if (!mentions)
2120
2318
  return;
@@ -2191,12 +2389,17 @@ export class AUNChannel {
2191
2389
  if (p2pPayloadType.startsWith('menu.')
2192
2390
  || p2pPayloadType === AUNChannel.CONTACT_REQUEST_QUERY_TYPE
2193
2391
  || p2pPayloadType === AUNChannel.CONTACT_REQUEST_SUBMIT_TYPE) {
2392
+ const installation = p2pPayloadType.startsWith('menu.')
2393
+ ? this.localSenderInstallation?.aunSenderInstallation(msg) : undefined;
2194
2394
  this.acknowledgeImmediately(messageId, seq);
2195
2395
  this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
2196
2396
  this.dispatchMessage({
2197
2397
  channelId: chatId, userId: fromAid,
2198
2398
  text: JSON.stringify(payload),
2199
2399
  chatType: 'private', messageId, seq, receivedAt, receivedAtMono, gatewaySeq: seq,
2400
+ peerDeviceId: installation?.deviceId,
2401
+ peerSlotId: installation?.slotId,
2402
+ threadId,
2200
2403
  peerName: displayName || undefined,
2201
2404
  peerType: peerIdentity.type,
2202
2405
  encrypted: msgEncrypted,
@@ -2228,6 +2431,7 @@ export class AUNChannel {
2228
2431
  channelId: chatId, userId: fromAid,
2229
2432
  text: JSON.stringify(payload),
2230
2433
  chatType: 'private', messageId, seq, receivedAt, gatewaySeq: seq,
2434
+ threadId,
2231
2435
  peerName: displayName || undefined,
2232
2436
  peerType: peerIdentity.type,
2233
2437
  encrypted: msgEncrypted,
@@ -2260,6 +2464,7 @@ export class AUNChannel {
2260
2464
  chatType: 'private',
2261
2465
  messageId,
2262
2466
  receivedAt,
2467
+ createdAt: typeof msg.created_at === 'number' ? msg.created_at : undefined,
2263
2468
  gatewaySeq: seq,
2264
2469
  causation,
2265
2470
  seq,
@@ -2297,6 +2502,12 @@ export class AUNChannel {
2297
2502
  const groupId = env.group_id ?? '';
2298
2503
  const senderAid = env.from ?? '';
2299
2504
  const payload = msg.payload ?? '';
2505
+ const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
2506
+ if (inboundType === 'observer.forward') {
2507
+ this.acknowledgeImmediately(messageId, seq);
2508
+ logger.debug(`${this.logPrefix()} Group dropped observer.forward control envelope: mid=${messageId}`);
2509
+ return;
2510
+ }
2300
2511
  const payloadMentionEntries = this.parsePayloadMentionsOrReject(payload, 'group.inbound', messageId, seq);
2301
2512
  if (!payloadMentionEntries)
2302
2513
  return;
@@ -2307,7 +2518,6 @@ export class AUNChannel {
2307
2518
  const topicName = (typeof payload === 'object' && payload !== null)
2308
2519
  ? this.sanitizeTopicName(payload.topicName)
2309
2520
  : undefined;
2310
- const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
2311
2521
  // 终态错误只作为展示信号,不能进入 Agent,也不能被 observer 再次传播。
2312
2522
  if (inboundType === 'error') {
2313
2523
  this.acknowledgeImmediately(messageId, seq);
@@ -2377,27 +2587,10 @@ export class AUNChannel {
2377
2587
  {
2378
2588
  const payloadObj = (payload && typeof payload === 'object') ? payload : null;
2379
2589
  const payloadType = typeof payloadObj?.type === 'string' ? payloadObj.type : '';
2380
- // menu.* 协议:自定义消息快速路径
2590
+ // Menu control requests are private-only, including token issuance.
2381
2591
  if (payloadType.startsWith('menu.')) {
2382
- const menuEncrypted = !!env.encrypted;
2383
2592
  this.acknowledgeImmediately(messageId, seq);
2384
- this.appendInboundPayloadJsonl(groupId, senderAid, messageId, menuEncrypted, true, logDescriptor, threadId, receivedAt, seq);
2385
- this.dispatchMessage({
2386
- channelId: groupId, userId: senderAid,
2387
- text: JSON.stringify(payload),
2388
- chatType: 'group', messageId, seq, receivedAt, receivedAtMono, gatewaySeq: seq, groupId,
2389
- encrypted: menuEncrypted,
2390
- protectedHeaders,
2391
- mentions: payloadMentionEntries,
2392
- mentionAids: payloadMentionAids,
2393
- isMentioned: hasMentionAll(payloadMentionEntries)
2394
- || (!!this._aid && mentionEntryTargets(payloadMentionEntries).includes(this._aid)),
2395
- replyContext: this.buildGroupReplyContext(threadId, senderAid, menuEncrypted, messageId, payloadObj?.chatmode, topicName, groupId),
2396
- msgType: logDescriptor.msgType,
2397
- payloadType: logDescriptor.payloadType,
2398
- payloadSummary: logDescriptor.payloadSummary,
2399
- messageLogContent: logDescriptor.content,
2400
- });
2593
+ logger.debug(`${this.logPrefix()} Group Menu dropped: code=MENU_PRIVATE_ONLY mid=${messageId}`);
2401
2594
  return;
2402
2595
  }
2403
2596
  if (payloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(payloadType)) {
@@ -2437,17 +2630,15 @@ export class AUNChannel {
2437
2630
  if (isEchoMsg) {
2438
2631
  // 短 echo(≤10 字符)已在前面的快速通道命中并 return,这里只处理长 echo
2439
2632
  // >10 字符:追加 trace,存 pending echo,跳过 mention 过滤继续走 Agent 流程
2440
- const echoTs = () => {
2441
- const d = new Date();
2442
- return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
2443
- };
2633
+ const echoCreatedAt = data.created_at;
2634
+ const echoReceiveTs = Date.now();
2444
2635
  let echoText = text;
2445
- echoText += `\n${echoTs()} [EvolCore.receive] from=${senderAid} mid=${messageId} chat=group self=${this._aid || 'unknown'} conn_uptime=${this.connectedAt ? Math.round((Date.now() - this.connectedAt) / 1000) + 's' : 'unknown'}`;
2636
+ echoText += `\n${echoTraceClock(echoReceiveTs)} [EvolCore.receive] from=${senderAid} mid=${messageId} chat=group self=${this._aid || 'unknown'} conn_uptime=${this.connectedAt ? Math.round((echoReceiveTs - this.connectedAt) / 1000) + 's' : 'unknown'}${echoInboundLegLabel(echoCreatedAt, receivedAt)}${echoTraceOriginLabel(echoReceiveTs)}`;
2446
2637
  const msgChatmodeEcho = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
2447
2638
  this.pendingEchoMessages.set(messageId, {
2448
2639
  text: echoText,
2449
2640
  channelId: groupId,
2450
- context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId, msgChatmodeEcho, undefined, groupId),
2641
+ context: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmodeEcho, topicName, groupId),
2451
2642
  receiveTs: Date.now(),
2452
2643
  });
2453
2644
  // 继续走正常 Agent 流程(下面的代码会 dispatch)
@@ -2536,6 +2727,7 @@ export class AUNChannel {
2536
2727
  chatType: 'group',
2537
2728
  messageId,
2538
2729
  receivedAt,
2730
+ createdAt: typeof msg.created_at === 'number' ? msg.created_at : undefined,
2539
2731
  gatewaySeq: seq,
2540
2732
  seq,
2541
2733
  threadId,
@@ -2564,6 +2756,7 @@ export class AUNChannel {
2564
2756
  && authenticatedDelivery.chatType === 'group'
2565
2757
  && suppliedDelivery.groupId === authenticatedDelivery.groupId;
2566
2758
  const delivery = suppliedDelivery && suppliedMatches ? suppliedDelivery : authenticatedDelivery;
2759
+ const threadId = event.threadId ?? event.replyContext?.threadId;
2567
2760
  if (suppliedDelivery && delivery !== suppliedDelivery) {
2568
2761
  logger.warn(`${this.logPrefix()} Ignoring mismatched inbound reply route: channel=${event.channelId} chatType=${event.chatType}`);
2569
2762
  }
@@ -2585,17 +2778,15 @@ export class AUNChannel {
2585
2778
  }
2586
2779
  // 长 echo(>10 字符):存 pending,继续交给 agent 处理
2587
2780
  if (/echo/i.test(firstLine) && firstLine.trim().length > 10 && !hasEvolCoreTracePrivate) {
2588
- const echoTs = () => {
2589
- const d = new Date();
2590
- return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
2591
- };
2781
+ const echoReceiveTs = Date.now();
2592
2782
  let echoText = event.text;
2593
- echoText += `\n${echoTs()} [EvolCore.receive] from=${event.userId}${event.peerName ? `(${event.peerName})` : ''} mid=${event.messageId} chat=${event.chatType} self=${this._aid || 'unknown'} conn_uptime=${this.connectedAt ? Math.round((Date.now() - this.connectedAt) / 1000) + 's' : 'unknown'}`;
2783
+ echoText += `\n${echoTraceClock(echoReceiveTs)} [EvolCore.receive] from=${event.userId}${event.peerName ? `(${event.peerName})` : ''} mid=${event.messageId} chat=${event.chatType} self=${this._aid || 'unknown'} conn_uptime=${this.connectedAt ? Math.round((echoReceiveTs - this.connectedAt) / 1000) + 's' : 'unknown'}${echoInboundLegLabel(event.createdAt, event.receivedAt ?? echoReceiveTs)}${echoTraceOriginLabel(echoReceiveTs)}`;
2594
2784
  this.pendingEchoMessages.set(event.messageId, {
2595
2785
  text: echoText,
2596
2786
  channelId: event.channelId,
2597
2787
  context: {
2598
2788
  ...(event.replyContext?.metadata ? { metadata: event.replyContext.metadata } : {}),
2789
+ ...(threadId ? { threadId } : {}),
2599
2790
  delivery,
2600
2791
  },
2601
2792
  receiveTs: Date.now(),
@@ -2610,7 +2801,7 @@ export class AUNChannel {
2610
2801
  const replyContext = {
2611
2802
  ...(event.replyContext ?? {}),
2612
2803
  delivery,
2613
- ...(event.threadId && !event.replyContext?.threadId ? { threadId: event.threadId } : {}),
2804
+ ...(threadId ? { threadId } : {}),
2614
2805
  };
2615
2806
  this.messageHandler({
2616
2807
  channelId: event.channelId || '',
@@ -2622,6 +2813,8 @@ export class AUNChannel {
2622
2813
  peerId: event.userId || event.channelId || '',
2623
2814
  peerName: event.peerName,
2624
2815
  peerType: event.peerType,
2816
+ peerDeviceId: event.peerDeviceId,
2817
+ peerSlotId: event.peerSlotId,
2625
2818
  sameDevice: event.sameDevice,
2626
2819
  sameNetwork: event.sameNetwork,
2627
2820
  sameEgressIp: event.sameEgressIp,
@@ -2632,7 +2825,7 @@ export class AUNChannel {
2632
2825
  receivedAtMono: event.receivedAtMono,
2633
2826
  gatewaySeq: event.gatewaySeq ?? event.seq,
2634
2827
  causation: event.causation,
2635
- threadId: event.threadId,
2828
+ threadId,
2636
2829
  mentions: event.mentions,
2637
2830
  mentionAids: event.mentionAids,
2638
2831
  isMentioned: event.isMentioned, // ← 新增:传递 isMentioned 标记
@@ -2797,7 +2990,10 @@ export class AUNChannel {
2797
2990
  forwardInbound(data) {
2798
2991
  if (!this.connected || !this.client)
2799
2992
  return Promise.resolve();
2800
- if (payloadTypeForObserver(data)?.startsWith('menu.'))
2993
+ const inboundType = payloadTypeForObserver(data);
2994
+ // Observer envelopes are control-plane telemetry. Mirroring one again
2995
+ // would amplify every activity event across observer-connected agents.
2996
+ if (inboundType === 'observer.forward' || inboundType?.startsWith('menu.'))
2801
2997
  return Promise.resolve();
2802
2998
  const { observable, owners } = this.getObserverConfig();
2803
2999
  if (!observable || owners.length === 0)
@@ -2833,7 +3029,8 @@ export class AUNChannel {
2833
3029
  forwardOutbound(result) {
2834
3030
  if (!this.connected || !this.client)
2835
3031
  return Promise.resolve();
2836
- if (payloadTypeForObserver(result)?.startsWith('menu.'))
3032
+ const outboundType = payloadTypeForObserver(result);
3033
+ if (outboundType === 'observer.forward' || outboundType?.startsWith('menu.'))
2837
3034
  return Promise.resolve();
2838
3035
  const { observable, owners } = this.getObserverConfig();
2839
3036
  if (!observable || owners.length === 0)
@@ -2874,8 +3071,8 @@ export class AUNChannel {
2874
3071
  };
2875
3072
  const encrypt = this.shouldEncrypt('observer.forward');
2876
3073
  for (const ownerAid of owners) {
2877
- this.callAndTrace('message.send', { to: ownerAid, payload: forwardPayload, encrypt })
2878
- .catch(e => logger.warn(`${this.logPrefix()} observer.forward send to ${ownerAid} failed: ${e}`));
3074
+ this.callAndTrace('message.send', { to: ownerAid, payload: forwardPayload, encrypt }, { lane: 'observer' })
3075
+ .catch(e => logger.debug(`${this.logPrefix()} observer.forward send to ${ownerAid} failed: ${e}`));
2879
3076
  }
2880
3077
  }
2881
3078
  // ── 观察者插话(Observer Insert,v0.3 待用上下文提示) ──────────────
@@ -2978,14 +3175,12 @@ export class AUNChannel {
2978
3175
  }
2979
3176
  }
2980
3177
  handleEcho(event) {
2981
- const ts = () => {
2982
- const d = new Date();
2983
- return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
2984
- };
3178
+ const receiveTs = Date.now();
2985
3179
  // 在收到的消息文本末尾追加 trace 行(只追加,不修改原文)
2986
3180
  let echoText = event.text;
2987
- echoText += `\n${ts()} [EvolCore.receive] from=${event.userId}${event.peerName ? `(${event.peerName})` : ''} mid=${event.messageId} chat=${event.chatType} self=${this._aid || 'unknown'} conn_uptime=${this.connectedAt ? Math.round((Date.now() - this.connectedAt) / 1000) + 's' : 'unknown'}`;
2988
- echoText += `\n${ts()} [EvolCore.reply] echo回声发出 conn_uptime=${this.connectedAt ? Math.round((Date.now() - this.connectedAt) / 1000) + 's' : 'unknown'}`;
3181
+ echoText += `\n${echoTraceClock(receiveTs)} [EvolCore.receive] from=${event.userId}${event.peerName ? `(${event.peerName})` : ''} mid=${event.messageId} chat=${event.chatType} self=${this._aid || 'unknown'} conn_uptime=${this.connectedAt ? Math.round((receiveTs - this.connectedAt) / 1000) + 's' : 'unknown'}${echoInboundLegLabel(event.createdAt, receiveTs)}${echoTraceOriginLabel(receiveTs)}`;
3182
+ const replyTs = Date.now();
3183
+ echoText += `\n${echoTraceClock(replyTs)} [EvolCore.reply] echo回声发出 conn_uptime=${this.connectedAt ? Math.round((replyTs - this.connectedAt) / 1000) + 's' : 'unknown'}${echoTraceOriginLabel(replyTs)}`;
2989
3184
  if (Buffer.byteLength(echoText, 'utf-8') > 4096) {
2990
3185
  echoText += `\n[TRUNCATED]`;
2991
3186
  }
@@ -2995,7 +3190,7 @@ export class AUNChannel {
2995
3190
  ? { chatType: 'group', groupId: event.channelId }
2996
3191
  : { chatType: 'private' };
2997
3192
  const context = {
2998
- threadId: event.threadId,
3193
+ threadId: event.threadId ?? event.replyContext?.threadId,
2999
3194
  metadata: event.replyContext?.metadata,
3000
3195
  delivery: authenticatedDelivery,
3001
3196
  };
@@ -3098,6 +3293,7 @@ export class AUNChannel {
3098
3293
  else if (state === 'disconnected' || state === 'reconnecting') {
3099
3294
  const wasConnected = this.connected;
3100
3295
  this.connected = false;
3296
+ this.clearQueuedObserverSends(state);
3101
3297
  // The nine-state SDK emits standby -> retry_backoff -> reconnecting for
3102
3298
  // one reconnect cycle. Count the connected-to-down edge once instead of
3103
3299
  // inflating the metric for every intermediate state.
@@ -3281,6 +3477,7 @@ export class AUNChannel {
3281
3477
  this.client = null;
3282
3478
  }
3283
3479
  this.connected = false;
3480
+ this.clearQueuedObserverSends(`takeover_${reason}`);
3284
3481
  const delaySec = Math.round(delayMs / 1000);
3285
3482
  logger.info(`${this.logPrefix()} Scheduling TS-layer reconnect (${reason}) in ${delaySec}s`);
3286
3483
  this.trace('OUT', 'reconnect.scheduled', { reason, delayMs, generation: gen });
@@ -3498,6 +3695,9 @@ export class AUNChannel {
3498
3695
  if (!current || typeof current !== 'object')
3499
3696
  continue;
3500
3697
  for (const key of Object.keys(current)) {
3698
+ // Topic routing must survive content truncation and fallback.
3699
+ if (current === limited && key === 'thread_id')
3700
+ continue;
3501
3701
  const value = current[key];
3502
3702
  if (typeof value === 'string') {
3503
3703
  leaves.push({ parent: current, key, value, bytes: Buffer.byteLength(value, 'utf8') });
@@ -3544,6 +3744,8 @@ export class AUNChannel {
3544
3744
  };
3545
3745
  if (typeof payload.chatmode === 'string')
3546
3746
  fallback.chatmode = truncateUtf8ToBytes(payload.chatmode, 32);
3747
+ if (typeof payload.thread_id === 'string')
3748
+ fallback.thread_id = payload.thread_id;
3547
3749
  logger.info(`${this.logPrefix()} Thought payload reduced to fallback (${Buffer.byteLength(JSON.stringify(fallback), 'utf8')} bytes)`);
3548
3750
  return fallback;
3549
3751
  }
@@ -3611,6 +3813,9 @@ export class AUNChannel {
3611
3813
  replyContext,
3612
3814
  });
3613
3815
  this.ownedCardMsgIds.add(messageId);
3816
+ if (replyContext?.threadId) {
3817
+ this.ownedCardTopics.set(messageId, { channelId, threadId: replyContext.threadId });
3818
+ }
3614
3819
  let messageIds = this.interactionCardMessageIds.get(action.requestId);
3615
3820
  if (!messageIds) {
3616
3821
  messageIds = new Set();
@@ -3629,7 +3834,10 @@ export class AUNChannel {
3629
3834
  this.interactionCardMessageIds.delete(action.requestId);
3630
3835
  }, mapTtl);
3631
3836
  mapTimer.unref?.();
3632
- const ownedTimer = setTimeout(() => this.ownedCardMsgIds.delete(messageId), 24 * 60 * 60 * 1000);
3837
+ const ownedTimer = setTimeout(() => {
3838
+ this.ownedCardMsgIds.delete(messageId);
3839
+ this.ownedCardTopics.delete(messageId);
3840
+ }, 24 * 60 * 60 * 1000);
3633
3841
  ownedTimer.unref?.();
3634
3842
  const invalidationReason = this.invalidatedInteractions.get(action.requestId);
3635
3843
  if (invalidationReason) {
@@ -3889,10 +4097,11 @@ export class AUNChannel {
3889
4097
  /**
3890
4098
  * Truncate payload if it exceeds AUN protocol limits.
3891
4099
  * AUN SDK enforces MAX_WS_PAYLOAD_SIZE = 1_000_000 bytes (1MB) for the entire JSON-RPC message.
3892
- * We use a conservative 800KB limit for the payload itself to leave room for RPC envelope.
4100
+ * We use a conservative 512KB limit for the payload itself to leave room for
4101
+ * the encrypted AUN envelope and RPC framing.
3893
4102
  */
3894
4103
  truncatePayloadIfNeeded(payload, label) {
3895
- const MAX_PAYLOAD_SIZE = 800 * 1024; // 800KB conservative limit
4104
+ const MAX_PAYLOAD_SIZE = 512 * 1024; // 512KB conservative limit
3896
4105
  const MAX_FIELD_SIZE = 256 * 1024; // 256KB per field
3897
4106
  // First check total size
3898
4107
  const payloadJson = JSON.stringify(payload);
@@ -3981,6 +4190,8 @@ export class AUNChannel {
3981
4190
  throw Object.assign(new Error(result.error ?? 'AUN structured send failed'), {
3982
4191
  code: result.code ?? 'AUN_SEND_FAILED',
3983
4192
  outboxId: result.outboxId,
4193
+ permanent: true,
4194
+ status: 'failed',
3984
4195
  });
3985
4196
  }
3986
4197
  return result;
@@ -3994,7 +4205,7 @@ export class AUNChannel {
3994
4205
  const routedContext = this.withDelivery(context, delivery);
3995
4206
  // 长 echo: agent 首次回复前先发 echo trace(echo 自身发送时跳过)
3996
4207
  if (!this.isEchoSending) {
3997
- await this.flushPendingEcho(channelId, delivery);
4208
+ await this.flushPendingEcho(channelId, delivery, context?.threadId);
3998
4209
  }
3999
4210
  let finalText = text;
4000
4211
  this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
@@ -4321,20 +4532,20 @@ export class AUNChannel {
4321
4532
  return { ok: false, error: e?.message || String(e), code: e?.code };
4322
4533
  }
4323
4534
  }
4324
- async flushPendingEcho(channelId, delivery) {
4535
+ async flushPendingEcho(channelId, delivery, threadId) {
4325
4536
  // 查找该 channelId 是否有 pending echo(长 echo 等待 agent 首次回复)
4326
4537
  for (const [key, echo] of this.pendingEchoMessages) {
4327
- if (echo.channelId === channelId) {
4538
+ if (echo.channelId === channelId
4539
+ && (echo.context?.threadId ?? '') === (threadId ?? '')
4540
+ && echo.context?.delivery?.chatType === delivery.chatType) {
4328
4541
  this.pendingEchoMessages.delete(key);
4329
4542
  logger.info(`${this.logPrefix()} [Echo] flushPendingEcho triggered: key=${key} channelId=${channelId} pendingCount=${this.pendingEchoMessages.size}`);
4330
- const ts = () => {
4331
- const d = new Date();
4332
- return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
4333
- };
4334
- const agentDuration = Date.now() - echo.receiveTs;
4543
+ const agentDoneTs = Date.now();
4544
+ const agentDuration = agentDoneTs - echo.receiveTs;
4335
4545
  let echoText = echo.text;
4336
- echoText += `\n${ts()} [EvolCore.agent] duration=${agentDuration}ms`;
4337
- echoText += `\n${ts()} [EvolCore.reply] echo回声发出 conn_uptime=${this.connectedAt ? Math.round((Date.now() - this.connectedAt) / 1000) + 's' : 'unknown'}`;
4546
+ echoText += `\n${echoTraceClock(agentDoneTs)} [EvolCore.agent] duration=${agentDuration}ms${echoTraceOriginLabel(agentDoneTs)}`;
4547
+ const replyTs = Date.now();
4548
+ echoText += `\n${echoTraceClock(replyTs)} [EvolCore.reply] echo回声发出 conn_uptime=${this.connectedAt ? Math.round((replyTs - this.connectedAt) / 1000) + 's' : 'unknown'}${echoTraceOriginLabel(replyTs)}`;
4338
4549
  if (Buffer.byteLength(echoText, 'utf-8') > 4096) {
4339
4550
  echoText += `\n[TRUNCATED]`;
4340
4551
  }
@@ -4777,6 +4988,8 @@ export class AUNChannel {
4777
4988
  const payloadWithChatmode = context?.metadata?.chatmode && !finalPayload.chatmode
4778
4989
  ? { ...finalPayload, chatmode: context.metadata.chatmode }
4779
4990
  : finalPayload;
4991
+ if (context?.threadId && !payloadWithChatmode.thread_id)
4992
+ payloadWithChatmode.thread_id = context.threadId;
4780
4993
  const params = {
4781
4994
  context: { type: 'task', id: taskId },
4782
4995
  payload: this.limitThoughtPayload(payloadWithChatmode),
@@ -5525,6 +5738,7 @@ export class AUNChannel {
5525
5738
  await this.sendStructuredOrThrow(channelId, payloadObj, context);
5526
5739
  }
5527
5740
  async disconnect() {
5741
+ this.managedMenuClient.close();
5528
5742
  this.intentionalDisconnect = true;
5529
5743
  if (this.reconnectTimer) {
5530
5744
  clearTimeout(this.reconnectTimer);
@@ -5549,6 +5763,7 @@ export class AUNChannel {
5549
5763
  this.store = null;
5550
5764
  }
5551
5765
  this.connected = false;
5766
+ this.clearQueuedObserverSends('intentional disconnect');
5552
5767
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'disconnected', aid: this.config.aid, reason: 'intentional' });
5553
5768
  appendAidLifecycle({ ts: Date.now(), iso: new Date().toISOString(), event: 'disconnected', aid: this.config.aid, reason: 'intentional' });
5554
5769
  this.setAidStatus('disabled');
@@ -5626,7 +5841,7 @@ export class AUNChannel {
5626
5841
  try {
5627
5842
  const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
5628
5843
  const identity = await PeerIdentityCache.resolve('aun', aid, selfAgentDir, this.store, false);
5629
- const type = identity.type === 'human' ? 'human' : 'ai';
5844
+ const type = identity.type === 'human' ? 'human' : 'agent';
5630
5845
  const name = identity.name || undefined;
5631
5846
  const info = { type, name };
5632
5847
  this.peerInfoCache.set(aid, info);
@@ -5777,6 +5992,26 @@ export class AUNChannelPlugin {
5777
5992
  case 'result.text':
5778
5993
  case 'command.result':
5779
5994
  case 'command.error': {
5995
+ // Group command feedback is control traffic, not model input for
5996
+ // the other agents in the conversation.
5997
+ if (delivery.chatType === 'group'
5998
+ && (payload.kind === 'command.result' || payload.kind === 'command.error')) {
5999
+ const isError = payload.kind === 'command.error';
6000
+ const feedback = {
6001
+ type: isError ? 'error' : 'notice',
6002
+ ...taskBase(),
6003
+ subtype: payload.kind,
6004
+ text: payload.text,
6005
+ ...(isError
6006
+ ? { message: payload.text, user_message: payload.text, recoverable: true, terminal: false }
6007
+ : { severity: 'info' }),
6008
+ };
6009
+ for (const key of ['format', 'reason', 'reasonCode', 'nextStep']) {
6010
+ setIfDefined(feedback, key, payload[key]);
6011
+ }
6012
+ await channel.sendReliableStructured(channelId, feedback, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
6013
+ return;
6014
+ }
5780
6015
  const sendCtx = {
5781
6016
  ...(replyCtx ?? {}),
5782
6017
  metadata: {