evolcore 0.0.21 → 0.0.23

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/AGENTS.md +62 -0
  2. package/CHANGELOG.md +53 -0
  3. package/README.md +2 -0
  4. package/bin/codex-managed-acl.ps1 +155 -0
  5. package/bin/codex-managed-hook.mjs +3 -0
  6. package/bin/install-codex-managed-hooks.mjs +376 -96
  7. package/dist/agents/baseagent-availability.js +48 -0
  8. package/dist/agents/baseagent.js +1 -1
  9. package/dist/agents/claude-runner.js +44 -226
  10. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +103 -17
  11. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  12. package/dist/agents/codex/catalog.js +70 -0
  13. package/dist/agents/codex/diagnostics.js +154 -0
  14. package/dist/agents/codex-runner.js +1292 -219
  15. package/dist/agents/ecagent-runner.js +10 -31
  16. package/dist/agents/gemini-runner.js +2 -6
  17. package/dist/aun/aid/store.js +2 -0
  18. package/dist/aun/menu-client.js +272 -0
  19. package/dist/aun/msg/group.js +65 -4
  20. package/dist/aun/msg/index.js +1 -1
  21. package/dist/aun/msg/managed-operation.js +51 -4
  22. package/dist/aun/outbox.js +7 -0
  23. package/dist/aun/rpc/client.js +8 -1
  24. package/dist/channels/aun.js +240 -36
  25. package/dist/channels/wechat.js +81 -10
  26. package/dist/cli/aun-commands.js +48 -4
  27. package/dist/cli/command-log.js +21 -2
  28. package/dist/cli/daemon-commands.js +169 -51
  29. package/dist/cli/index.js +20 -0
  30. package/dist/cli/init.js +2 -10
  31. package/dist/cli/menu-client.js +95 -0
  32. package/dist/cli/model.js +23 -8
  33. package/dist/cli/task-context.js +16 -2
  34. package/dist/cli/trigger-command.js +22 -5
  35. package/dist/config/config-field-policy.js +3 -0
  36. package/dist/config/config-manager.js +32 -5
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/schema-migration.js +14 -2
  39. package/dist/core/auth/agent-delegation.js +15 -0
  40. package/dist/core/auth/auth-gateway.js +8 -0
  41. package/dist/core/auth/authorization-audit.js +70 -6
  42. package/dist/core/auth/operation-catalog.js +4 -0
  43. package/dist/core/auth/remote-access-store.js +135 -0
  44. package/dist/core/bootstrap-messages.js +8 -0
  45. package/dist/core/bootstrap-service.js +93 -25
  46. package/dist/core/command/command-handler.js +62 -5
  47. package/dist/core/command/daemon-diagnostics.js +448 -0
  48. package/dist/core/command/menu-catalog.js +3 -0
  49. package/dist/core/command/menu-handler.js +150 -33
  50. package/dist/core/command/menu-protocol.js +120 -1
  51. package/dist/core/command/slash-gate.js +3 -3
  52. package/dist/core/command/slash-handler.js +83 -40
  53. package/dist/core/data-migration.js +11 -1
  54. package/dist/core/event-catalog.js +32 -0
  55. package/dist/core/handoff/runtime.js +23 -3
  56. package/dist/core/message/im-renderer.js +7 -3
  57. package/dist/core/message/message-bridge.js +60 -2
  58. package/dist/core/message/message-log.js +33 -0
  59. package/dist/core/message/message-queue.js +41 -15
  60. package/dist/core/message/response-engine.js +246 -114
  61. package/dist/core/model/catalog-service.js +308 -0
  62. package/dist/core/model/model-catalog.js +124 -334
  63. package/dist/core/model/model-diagnostics.js +4 -5
  64. package/dist/core/model/model-fallback.js +9 -16
  65. package/dist/core/model/model-permission.js +1 -1
  66. package/dist/core/permission/approval-gateway.js +16 -1
  67. package/dist/core/permission/ec-command-parser.js +272 -70
  68. package/dist/core/permission/protected-paths.js +74 -14
  69. package/dist/core/permission/sandbox-runtime.js +4 -2
  70. package/dist/core/permission/tool-error-code.js +12 -0
  71. package/dist/core/permission/tool-policy.js +290 -30
  72. package/dist/core/runtime-lock.js +13 -8
  73. package/dist/core/session/session-manager.js +30 -0
  74. package/dist/core/session/session-renew.js +18 -1
  75. package/dist/core/session/session-turn-coordinator.js +5 -1
  76. package/dist/index.js +104 -34
  77. package/dist/ipc.js +218 -96
  78. package/dist/paths.js +18 -0
  79. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  80. package/dist/stats/price-resolver.js +4 -0
  81. package/dist/trigger/feedback.js +14 -2
  82. package/dist/trigger/manager.js +3 -1
  83. package/dist/trigger/parser.js +86 -13
  84. package/dist/trigger/patch.js +104 -1
  85. package/dist/trigger/scheduler.js +20 -3
  86. package/dist/utils/ecweb-supervisor.js +11 -2
  87. package/dist/utils/ecweb-utils.js +8 -26
  88. package/dist/utils/error-utils.js +7 -0
  89. package/dist/utils/logger.js +9 -4
  90. package/dist/utils/npm-ops.js +47 -15
  91. package/dist/utils/tool-summary.js +59 -0
  92. package/dist/utils/windows-shell-trust.js +201 -0
  93. package/kits/docs/evolcore/INDEX.md +3 -2
  94. package/kits/docs/evolcore/agent-create.md +159 -0
  95. package/kits/docs/evolcore/agent.md +6 -0
  96. package/kits/docs/evolcore/group-collaboration.md +256 -0
  97. package/kits/docs/evolcore/group-rules.md +1 -19
  98. package/kits/docs/evolcore/group.md +3 -1
  99. package/kits/docs/evolcore/menu-client.md +34 -0
  100. package/kits/docs/evolcore/trigger.md +6 -3
  101. package/kits/docs/prompt-loading-architecture.md +6 -0
  102. package/kits/eck_message_manifest.json +6 -6
  103. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  104. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  105. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  106. package/kits/schemas/_meta.json +3 -2
  107. package/kits/schemas/agent-config.schema.12.json +427 -0
  108. package/kits/templates/message-fragments/item.md +1 -1
  109. package/kits/templates/roles/visitor.json +1 -0
  110. package/kits/templates/system-fragments/bootstrap.md +10 -2
  111. package/kits/templates/system-fragments/commands.md +2 -2
  112. package/package.json +11 -3
@@ -34,7 +34,8 @@ import { deriveCausation, normalizeCausation } from '../core/causation/context.j
34
34
  import { recordCausationSpan } from '../core/causation/audit.js';
35
35
  import { refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
36
36
  import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
37
- import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, bootstrapInitialMessageOperationId, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
37
+ import { toolInputForDisplay } from '../utils/tool-summary.js';
38
+ import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, bootstrapInitialMessageOperationPrefix, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
38
39
  import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
39
40
  import { normalizeAunMentionEntries, } from '../aun/msg/mention-schema.js';
40
41
  import { isDeliveryTarget, sameDeliveryTarget } from '../core/message/message-utils.js';
@@ -45,6 +46,9 @@ const AUN_INBOUND_DEDUP_TTL_MS = 7 * 24 * 60 * 60 * 1000;
45
46
  const IMAGE_MIME_TYPE = /^image\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
46
47
  const AUN_ATTACHMENT_DOWNLOAD_ATTEMPTS = 3;
47
48
  const AUN_ATTACHMENT_RETRY_DELAYS_MS = [250, 750];
49
+ const OBSERVER_GROUP_MEMBERS_TTL_MS = 60_000;
50
+ const OBSERVER_GROUP_MEMBERS_PAGE_SIZE = 200;
51
+ const OBSERVER_GROUP_MEMBERS_MAX_PAGES = 100;
48
52
  const AUN_SEND_LOG_TEXT_MAX_LENGTH = 60;
49
53
  export function formatAunSendLogText(text) {
50
54
  return text.replace(/\s+/gu, ' ').trim().slice(0, AUN_SEND_LOG_TEXT_MAX_LENGTH);
@@ -480,7 +484,8 @@ function redactMenuTokensForTrace(value, seen = new WeakSet()) {
480
484
  return value.map(item => redactMenuTokensForTrace(item, seen));
481
485
  const redacted = {};
482
486
  for (const [key, item] of Object.entries(value)) {
483
- redacted[key] = key.toLowerCase() === 'menu_token'
487
+ const normalizedKey = key.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/-/g, '_').toLowerCase();
488
+ redacted[key] = /^(?:menu|ecweb)_token$/.test(normalizedKey)
484
489
  ? '[REDACTED]'
485
490
  : redactMenuTokensForTrace(item, seen);
486
491
  }
@@ -1062,6 +1067,18 @@ export class AUNChannel {
1062
1067
  .replace(/[ \t]+/g, ' ')
1063
1068
  .trim();
1064
1069
  }
1070
+ /** Normalize a command after a client-rendered friendly mention label. */
1071
+ normalizeSlashCommandText(text) {
1072
+ const trimmed = text.trim();
1073
+ const leadingLabels = trimmed.match(/^(?:@[^\s]+\s+)+(?=\/)/u);
1074
+ const commandText = leadingLabels
1075
+ ? trimmed.slice(leadingLabels[0].length).trimStart()
1076
+ : trimmed;
1077
+ // Keep the command contract consistent for mixed friendly/canonical @
1078
+ // tokens: selector labels are removed above, then known AID-shaped tokens
1079
+ // anywhere in the command are removed by the existing helper.
1080
+ return this.stripAllMentions(commandText);
1081
+ }
1065
1082
  parsePayloadMentionsOrReject(payload, context, messageId, seq) {
1066
1083
  const raw = payload && typeof payload === 'object' && !Array.isArray(payload)
1067
1084
  ? payload.mentions
@@ -1277,6 +1294,8 @@ export class AUNChannel {
1277
1294
  /** 撤权可能先于 outbox 卡片投递完成;迟到卡片注册后立即撤回。 */
1278
1295
  invalidatedInteractions = new Map();
1279
1296
  mentionModeResolver;
1297
+ /** Agent-level policy controlling whether group slash commands require structured mentions. */
1298
+ groupSlashRequireMentionsResolver;
1280
1299
  static PROACTIVE_ALLOW_TYPES = new Set([
1281
1300
  'text', 'quote', 'image', 'video', 'voice', 'file', 'json',
1282
1301
  'merge', 'link', 'location', 'personal_card',
@@ -1401,6 +1420,14 @@ export class AUNChannel {
1401
1420
  }
1402
1421
  }
1403
1422
  async _initClientInner() {
1423
+ this.observerGroupMembers.clear();
1424
+ for (const groupId of new Set([
1425
+ ...this.observerGroupMembershipEpoch.keys(),
1426
+ ...this.observerGroupMemberFetches.keys(),
1427
+ ])) {
1428
+ this.observerGroupMembershipEpoch.set(groupId, (this.observerGroupMembershipEpoch.get(groupId) ?? 0) + 1);
1429
+ }
1430
+ this.observerGroupMemberFetches.clear();
1404
1431
  // Clean up existing client if any
1405
1432
  if (this.client) {
1406
1433
  this.trace('OUT', 'client.close', { reason: 'initClient' });
@@ -1469,6 +1496,44 @@ export class AUNChannel {
1469
1496
  logger.debug(`${this.logPrefix()}[DIAG] group.message_created: group_id=${gid} sender=${sender}`);
1470
1497
  this.handleIncomingGroupMessage(data);
1471
1498
  });
1499
+ client.on('group.changed', (data) => {
1500
+ if (!isCurrentClient())
1501
+ return;
1502
+ const record = data && typeof data === 'object' ? data : {};
1503
+ const envelope = record.envelope && typeof record.envelope === 'object'
1504
+ ? record.envelope
1505
+ : {};
1506
+ this.invalidateObserverGroupMembers(envelope.group_id ?? record.group_id);
1507
+ });
1508
+ // fastaun 0.5.12+ exposes targeted invitations as a local, aggregated
1509
+ // event. It is a control signal, not a user message, so keep it out of
1510
+ // the Agent routing path while retaining it in diagnostics.
1511
+ client.on('group.invite_received', (data) => {
1512
+ if (!isCurrentClient())
1513
+ return;
1514
+ const record = data && typeof data === 'object' ? data : {};
1515
+ const groupId = record.group_id ?? record.group_aid ?? 'unknown';
1516
+ const inviteIds = Array.isArray(record.invite_ids) ? record.invite_ids : [];
1517
+ this.trace('IN', 'group.invite_received', data);
1518
+ logger.info(`${this.logPrefix()} Group invitation received: group=${groupId} pending=${inviteIds.length || 1}`);
1519
+ });
1520
+ }
1521
+ // 0.5.10/0.5.11 local lifecycle events summarize SDK delivery and Pull
1522
+ // progress. They must never be interpreted as inbound chat messages.
1523
+ for (const eventName of ['delivery.changed', 'sync.started', 'sync.progress', 'sync.stopped']) {
1524
+ client.on(eventName, (data) => {
1525
+ if (!isCurrentClient())
1526
+ return;
1527
+ this.trace('IN', eventName, data);
1528
+ if (eventName === 'sync.progress') {
1529
+ const record = data && typeof data === 'object' ? data : {};
1530
+ logger.debug(`${this.logPrefix()} AUN sync progress: namespace=${record.namespace ?? 'unknown'} page=${record.page ?? '?'} pulled=${record.page_pulled_count ?? record.pulled_count ?? 0}`);
1531
+ }
1532
+ else if (eventName === 'sync.stopped') {
1533
+ const record = data && typeof data === 'object' ? data : {};
1534
+ logger.info(`${this.logPrefix()} AUN sync stopped: reason=${record.reason ?? 'unknown'}`);
1535
+ }
1536
+ });
1472
1537
  }
1473
1538
  const handleSdkState = (source) => (data) => {
1474
1539
  if (!isCurrentClient())
@@ -2034,7 +2099,7 @@ export class AUNChannel {
2034
2099
  // 显式排除 observer.inject:它是 owner 对本 agent 的控制消息,不应镜像给观察者
2035
2100
  // (即便日后 from-owner 排除规则调整,也不会泄漏)。
2036
2101
  if (inboundType !== AUNChannel.INJECT_REQUEST_TYPE) {
2037
- this.forwardInbound(msg);
2102
+ void this.forwardInbound(msg);
2038
2103
  }
2039
2104
  // 回声过滤:自己发出的消息会被 gateway fanout 回来,
2040
2105
  // 只有 from_aid == self 且 chat_id 不匹配时才丢弃(说明是其它实例发的)
@@ -2205,7 +2270,7 @@ export class AUNChannel {
2205
2270
  }
2206
2271
  // Observer forward (inbound):群聊消息在所有过滤之前转发原始明文 payload。
2207
2272
  // forwardInbound 内部排除 self-echo 与 from-owner。
2208
- this.forwardInbound(msg);
2273
+ void this.forwardInbound(msg);
2209
2274
  logger.debug(`${this.logPrefix()}[DIAG-GRP] full_msg=${JSON.stringify(msg).substring(0, 500)}`);
2210
2275
  if (!groupId || !senderAid) {
2211
2276
  this.acknowledgeImmediately(messageId, seq);
@@ -2316,9 +2381,13 @@ export class AUNChannel {
2316
2381
  const firstLineGroup = text.split('\n')[0] || '';
2317
2382
  const hasEvolCoreTraceGroup = /\[EvolCore\.(receive|reply|agent)\]/.test(text);
2318
2383
  const isEchoMsg = /echo/i.test(firstLineGroup) && !hasEvolCoreTraceGroup;
2384
+ const slashCommandText = this.normalizeSlashCommandText(text);
2319
2385
  // 命令判定:剥离所有 @ 后看是否 / 开头(多 @ 场景如 @a @b /status 也能正确识别)。
2320
2386
  // echo 消息走独立的 trace 流程,不参与命令语义判定。
2321
- const isCommandMsg = !isEchoMsg && this.stripAllMentions(text).startsWith('/');
2387
+ const isCommandMsg = !isEchoMsg && slashCommandText.startsWith('/');
2388
+ const groupSlashRequireMentions = this.groupSlashRequireMentionsResolver
2389
+ ? await this.groupSlashRequireMentionsResolver().catch(() => undefined) ?? true
2390
+ : true;
2322
2391
  if (isEchoMsg) {
2323
2392
  // 短 echo(≤10 字符)已在前面的快速通道命中并 return,这里只处理长 echo
2324
2393
  // >10 字符:追加 trace,存 pending echo,跳过 mention 过滤继续走 Agent 流程
@@ -2345,9 +2414,10 @@ export class AUNChannel {
2345
2414
  }
2346
2415
  else {
2347
2416
  // 非 echo 消息:mention 标记(不再过滤,交给响应模式决定)
2348
- // slash 命令在任何 mentionMode 下都强制走 mention-only 语义
2349
- // 即必须 @ 本 agent(或 @all)才处理,避免广播群里一条命令被全部 agent 各自执行。
2350
- const enforceMention = mentionMode === 'mention-only' || isCommandMsg;
2417
+ // slash 命令默认要求结构化 @;Agent 可通过
2418
+ // groupSlashRequireMentions=false 选择仅按正文命令格式处理。
2419
+ const enforceMention = (mentionMode === 'mention-only' && !isCommandMsg)
2420
+ || (isCommandMsg && groupSlashRequireMentions);
2351
2421
  const isMentioned = mentionedSelf || mentionedAll;
2352
2422
  // 过滤逻辑下移到响应层:这里只标记,不过滤
2353
2423
  // 但为了保持现有行为兼容(避免大量未 @ 消息涌入),暂时保留过滤
@@ -2355,14 +2425,14 @@ export class AUNChannel {
2355
2425
  if (enforceMention && !isMentioned && !isOwnHandoff) {
2356
2426
  this.acknowledgeImmediately(messageId, seq);
2357
2427
  this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq);
2358
- logger.info(`${this.logPrefix()} Group dropped: unmentioned (group=${groupId} sender=${senderAid} mid=${messageId} mentionMode=${mentionMode} isCommand=${isCommandMsg} textPreview=${JSON.stringify(text.slice(0, 80))})`);
2428
+ logger.info(`${this.logPrefix()} Group dropped: unmentioned (group=${groupId} sender=${senderAid} mid=${messageId} mentionMode=${mentionMode} slashRequireMentions=${groupSlashRequireMentions} isCommand=${isCommandMsg} textPreview=${JSON.stringify(text.slice(0, 80))})`);
2359
2429
  return;
2360
2430
  }
2361
2431
  }
2362
2432
  // 命令消息:剥离所有 @(多 agent 被 @ 时各自拿到干净的 /status 各自执行);
2363
2433
  // 普通消息:仅在唯一 @ 是自己时剥离,保留其他 @ 供 agent 感知。
2364
2434
  const strippedText = isCommandMsg
2365
- ? this.stripAllMentions(text)
2435
+ ? slashCommandText
2366
2436
  : this.stripSelfMentionIfOnly(text, this._aid);
2367
2437
  // Detect attachments before the empty-text guard (顶层 + 嵌套)
2368
2438
  const rawAttachments = this.collectAllAttachments(payload);
@@ -2393,7 +2463,7 @@ export class AUNChannel {
2393
2463
  : mentionedSelf
2394
2464
  ? (structMentionSelf ? 'mention.self(struct)' : 'mention.self(text)')
2395
2465
  : `${mentionMode}.no-mention`;
2396
- logger.info(`${this.logPrefix()} Group mention decision: mid=${messageId} group=${groupId} sender=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${payloadType} mentionMode=${mentionMode} reason=${reason} structMentions=${JSON.stringify(payloadMentionEntries)} textMentionSelf=${textMentionSelf} textMentionAll=${textMentionAll} structMentionSelf=${structMentionSelf} structMentionAll=${structMentionAll} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
2466
+ logger.info(`${this.logPrefix()} Group mention decision: mid=${messageId} group=${groupId} sender=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${payloadType} mentionMode=${mentionMode} slashRequireMentions=${groupSlashRequireMentions} reason=${reason} structMentions=${JSON.stringify(payloadMentionEntries)} textMentionSelf=${textMentionSelf} textMentionAll=${textMentionAll} structMentionSelf=${structMentionSelf} structMentionAll=${structMentionAll} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
2397
2467
  // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
2398
2468
  if (payloadType === 'action_card_reply')
2399
2469
  return;
@@ -2564,6 +2634,9 @@ export class AUNChannel {
2564
2634
  // observable / owners 不在此处缓存——由 daemon 注入 resolver,从 EvolAgent 的
2565
2635
  // in-memory merged config(启动/重启/热重载时统一更新的唯一缓存)读取,避免重复缓存。
2566
2636
  observerConfigResolver;
2637
+ observerGroupMembers = new Map();
2638
+ observerGroupMemberFetches = new Map();
2639
+ observerGroupMembershipEpoch = new Map();
2567
2640
  /** 注入观察者配置读取器(daemon 侧从 EvolAgent merged config 读)。 */
2568
2641
  setObserverConfigResolver(fn) {
2569
2642
  this.observerConfigResolver = fn;
@@ -2582,23 +2655,129 @@ export class AUNChannel {
2582
2655
  * data 为 SDK message.received / group.message_created 回调的整个对象,
2583
2656
  * 不拆解、不重组——SDK 信封结构变化不影响此处。
2584
2657
  */
2658
+ async resolveObserverGroupMembers(groupId) {
2659
+ if (!groupId || !this.client)
2660
+ return undefined;
2661
+ const cached = this.observerGroupMembers.get(groupId);
2662
+ if (cached && cached.expiresAt > Date.now())
2663
+ return cached.aids;
2664
+ const pending = this.observerGroupMemberFetches.get(groupId);
2665
+ if (pending)
2666
+ return pending;
2667
+ const epoch = this.observerGroupMembershipEpoch.get(groupId) ?? 0;
2668
+ let request;
2669
+ request = (async () => {
2670
+ try {
2671
+ const result = await this.callAndTrace('group.get_members', {
2672
+ group_id: groupId,
2673
+ page: 1,
2674
+ size: OBSERVER_GROUP_MEMBERS_PAGE_SIZE,
2675
+ });
2676
+ const rawMembers = Array.isArray(result?.members)
2677
+ ? result.members
2678
+ : Array.isArray(result?.items) ? result.items : undefined;
2679
+ if (!rawMembers)
2680
+ return undefined;
2681
+ const hasTotal = Number.isFinite(Number(result?.total));
2682
+ const hasMore = result?.has_more === true || result?.hasMore === true;
2683
+ if (!hasTotal && (hasMore || rawMembers.length >= OBSERVER_GROUP_MEMBERS_PAGE_SIZE))
2684
+ return undefined;
2685
+ const allMembers = [...rawMembers];
2686
+ const total = Number(result?.total);
2687
+ const pageSize = Number(result?.size) > 0 ? Number(result.size) : OBSERVER_GROUP_MEMBERS_PAGE_SIZE;
2688
+ const pageCount = Number.isFinite(total) && total > allMembers.length
2689
+ ? Math.ceil(total / pageSize)
2690
+ : 1;
2691
+ if (pageCount > OBSERVER_GROUP_MEMBERS_MAX_PAGES)
2692
+ return undefined;
2693
+ for (let page = 2; page <= pageCount; page++) {
2694
+ const next = await this.callAndTrace('group.get_members', {
2695
+ group_id: groupId,
2696
+ page,
2697
+ size: OBSERVER_GROUP_MEMBERS_PAGE_SIZE,
2698
+ });
2699
+ const nextMembers = Array.isArray(next?.members)
2700
+ ? next.members
2701
+ : Array.isArray(next?.items) ? next.items : undefined;
2702
+ if (!nextMembers)
2703
+ return undefined;
2704
+ allMembers.push(...nextMembers);
2705
+ }
2706
+ if (hasTotal && total > allMembers.length)
2707
+ return undefined;
2708
+ const aids = new Set();
2709
+ for (const member of allMembers) {
2710
+ const aid = typeof member === 'string'
2711
+ ? member.trim()
2712
+ : typeof member?.aid === 'string' ? member.aid.trim()
2713
+ : typeof member?.member_aid === 'string' ? member.member_aid.trim() : '';
2714
+ if (aid)
2715
+ aids.add(aid);
2716
+ }
2717
+ if ((this.observerGroupMembershipEpoch.get(groupId) ?? 0) === epoch) {
2718
+ this.observerGroupMembers.set(groupId, { aids, expiresAt: Date.now() + OBSERVER_GROUP_MEMBERS_TTL_MS });
2719
+ }
2720
+ return aids;
2721
+ }
2722
+ catch (error) {
2723
+ logger.debug(`${this.logPrefix()} observer group membership lookup failed group=${groupId}: ${error}`);
2724
+ return undefined;
2725
+ }
2726
+ finally {
2727
+ if (this.observerGroupMemberFetches.get(groupId) === request) {
2728
+ this.observerGroupMemberFetches.delete(groupId);
2729
+ }
2730
+ }
2731
+ })();
2732
+ this.observerGroupMemberFetches.set(groupId, request);
2733
+ return request;
2734
+ }
2735
+ invalidateObserverGroupMembers(groupId) {
2736
+ if (typeof groupId !== 'string' || !groupId.trim())
2737
+ return;
2738
+ const key = groupId.trim();
2739
+ this.observerGroupMembers.delete(key);
2740
+ this.observerGroupMembershipEpoch.set(key, (this.observerGroupMembershipEpoch.get(key) ?? 0) + 1);
2741
+ }
2742
+ async observerRecipients(owners, envelope) {
2743
+ if (owners.length === 0)
2744
+ return owners;
2745
+ const groupId = typeof envelope.group_id === 'string' ? envelope.group_id.trim() : '';
2746
+ const members = groupId ? await this.resolveObserverGroupMembers(groupId) : undefined;
2747
+ // Unknown membership is fail-open for observation: only an explicit member
2748
+ // response may suppress the redundant owner DM.
2749
+ return owners.filter(owner => !(members?.has(owner)));
2750
+ }
2585
2751
  forwardInbound(data) {
2586
2752
  if (!this.connected || !this.client)
2587
- return;
2753
+ return Promise.resolve();
2588
2754
  if (payloadTypeForObserver(data)?.startsWith('menu.'))
2589
- return;
2755
+ return Promise.resolve();
2590
2756
  const { observable, owners } = this.getObserverConfig();
2591
2757
  if (!observable || owners.length === 0)
2592
- return;
2593
- const env = (data?.envelope && typeof data.envelope === 'object') ? data.envelope : {};
2758
+ return Promise.resolve();
2759
+ const env = (data?.envelope && typeof data.envelope === 'object')
2760
+ ? { ...data.envelope }
2761
+ : {};
2762
+ if (!env.group_id && typeof data.group_id === 'string')
2763
+ env.group_id = data.group_id;
2594
2764
  const from = env.from ?? '';
2595
2765
  if (this._aid && from === this._aid)
2596
- return; // self-echo:已在出站转过
2766
+ return Promise.resolve(); // self-echo:已在出站转过
2597
2767
  // 排除来源 owner(不把"owner A 发来的"再转回 A),但仍转给其他 owner。
2598
- const recipientOwners = owners.filter(o => o !== from);
2599
- if (recipientOwners.length === 0)
2600
- return;
2601
- this.emitForward('inbound', data, recipientOwners);
2768
+ const candidateOwners = owners.filter(o => o !== from);
2769
+ if (candidateOwners.length === 0)
2770
+ return Promise.resolve();
2771
+ if (typeof env.group_id !== 'string' || !env.group_id.trim()) {
2772
+ this.emitForward('inbound', data, candidateOwners);
2773
+ return Promise.resolve();
2774
+ }
2775
+ return this.observerRecipients(candidateOwners, env)
2776
+ .then(recipientOwners => {
2777
+ if (recipientOwners.length > 0)
2778
+ this.emitForward('inbound', data, recipientOwners);
2779
+ })
2780
+ .catch(error => logger.debug(`${this.logPrefix()} observer inbound recipient lookup failed: ${error}`));
2602
2781
  }
2603
2782
  /**
2604
2783
  * 出站转发:Agent 经 AUN 真实发出的消息原样转发给 owner。
@@ -2607,20 +2786,33 @@ export class AUNChannel {
2607
2786
  */
2608
2787
  forwardOutbound(result) {
2609
2788
  if (!this.connected || !this.client)
2610
- return;
2789
+ return Promise.resolve();
2611
2790
  if (payloadTypeForObserver(result)?.startsWith('menu.'))
2612
- return;
2791
+ return Promise.resolve();
2613
2792
  const { observable, owners } = this.getObserverConfig();
2614
2793
  if (!observable || owners.length === 0)
2615
- return;
2616
- const env = (result?.envelope && typeof result.envelope === 'object') ? result.envelope : {};
2794
+ return Promise.resolve();
2795
+ const env = (result?.envelope && typeof result.envelope === 'object')
2796
+ ? { ...result.envelope }
2797
+ : {};
2798
+ if (!env.group_id && typeof result.group_id === 'string')
2799
+ env.group_id = result.group_id;
2617
2800
  const to = env.to ?? env.group_id ?? '';
2618
2801
  // 过滤:若对端本身是 owner,不转发给该 owner(避免"回复你"转给你自己);
2619
2802
  // 但仍转发给其他 owner。
2620
- const recipientOwners = owners.filter(o => o !== to);
2621
- if (recipientOwners.length === 0)
2622
- return;
2623
- this.emitForward('outbound', result, recipientOwners);
2803
+ const candidateOwners = owners.filter(o => o !== to);
2804
+ if (candidateOwners.length === 0)
2805
+ return Promise.resolve();
2806
+ if (typeof env.group_id !== 'string' || !env.group_id.trim()) {
2807
+ this.emitForward('outbound', result, candidateOwners);
2808
+ return Promise.resolve();
2809
+ }
2810
+ return this.observerRecipients(candidateOwners, env)
2811
+ .then(recipientOwners => {
2812
+ if (recipientOwners.length > 0)
2813
+ this.emitForward('outbound', result, recipientOwners);
2814
+ })
2815
+ .catch(error => logger.debug(`${this.logPrefix()} observer outbound recipient lookup failed: ${error}`));
2624
2816
  }
2625
2817
  /**
2626
2818
  * 实际投递 observer.forward 给每个 owner,使用 daemon 的全局 AUN 默认加密设置。
@@ -3104,6 +3296,9 @@ export class AUNChannel {
3104
3296
  setMentionModeResolver(resolver) {
3105
3297
  this.mentionModeResolver = resolver;
3106
3298
  }
3299
+ setGroupSlashRequireMentionsResolver(resolver) {
3300
+ this.groupSlashRequireMentionsResolver = resolver;
3301
+ }
3107
3302
  onRecall(handler) {
3108
3303
  this.recallHandler = handler;
3109
3304
  }
@@ -3711,9 +3906,15 @@ export class AUNChannel {
3711
3906
  return truncated;
3712
3907
  }
3713
3908
  buildActivityPayload(envelope, context, item) {
3714
- const activityItem = item && typeof item === 'object' && !Array.isArray(item)
3909
+ const rawItem = item && typeof item === 'object' && !Array.isArray(item)
3715
3910
  ? { ...item }
3716
3911
  : { kind: 'unknown', text: String(item ?? '') };
3912
+ const displayArguments = rawItem.kind === 'tool_call'
3913
+ ? toolInputForDisplay(rawItem.name, rawItem.arguments)
3914
+ : undefined;
3915
+ const activityItem = rawItem.kind === 'tool_call' && displayArguments !== rawItem.arguments
3916
+ ? { ...rawItem, arguments: displayArguments }
3917
+ : rawItem;
3717
3918
  return {
3718
3919
  ...this.buildTaskPayloadBase(envelope, context),
3719
3920
  type: 'activity',
@@ -5119,13 +5320,11 @@ export class AUNChannel {
5119
5320
  const owner = getFirstStaticAgentOwner(aid);
5120
5321
  if (!owner)
5121
5322
  return;
5122
- const operationIds = new Set([
5123
- bootstrapInitialMessageOperationId(aid),
5124
- postBootstrapWelcomeOperationId(aid),
5125
- ]);
5126
- const entries = [...operationIds]
5127
- .map(operationId => outbox.findByDedupeKey(aid, operationId, { includeTerminal: true }))
5128
- .filter((entry) => !!entry?.channelId);
5323
+ const completionEntry = outbox.findByDedupeKey(aid, postBootstrapWelcomeOperationId(aid), { includeTerminal: true });
5324
+ const entries = [
5325
+ ...outbox.findByDedupePrefix(aid, bootstrapInitialMessageOperationPrefix(aid), { includeTerminal: true }),
5326
+ ...(completionEntry ? [completionEntry] : []),
5327
+ ].filter((entry) => !!entry?.channelId);
5129
5328
  for (const entry of entries) {
5130
5329
  const delivery = { chatType: 'private' };
5131
5330
  const nestedDelivery = entry.context?.delivery;
@@ -5915,6 +6114,11 @@ export class AUNChannelPlugin {
5915
6114
  }, { cache: true }).mentionMode;
5916
6115
  });
5917
6116
  }
6117
+ if (typeof channel.setGroupSlashRequireMentionsResolver === 'function') {
6118
+ channel.setGroupSlashRequireMentionsResolver(async () => {
6119
+ return resolveEffective({ self: aid }, { cache: true }).groupSlashRequireMentions ?? true;
6120
+ });
6121
+ }
5918
6122
  },
5919
6123
  };
5920
6124
  }
@@ -40,6 +40,8 @@ const BACKOFF_DELAY_MS = 30_000;
40
40
  const RETRY_DELAY_MS = 2_000;
41
41
  const TYPING_TICKET_TTL_MS = 5 * 60 * 1000; // 5 min cache
42
42
  const SESSION_EXPIRED_ERRCODE = -14;
43
+ const INVALID_SYNC_CURSOR_ERRCODE = -1;
44
+ const MAX_SYNC_CURSOR_BYTES = 64 * 1024;
43
45
  export class WechatApiError extends Error {
44
46
  endpoint;
45
47
  ret;
@@ -71,6 +73,26 @@ export class WechatContextTokenRefreshRequiredError extends Error {
71
73
  }
72
74
  }
73
75
  }
76
+ /**
77
+ * iLink cursors are opaque text, so validation deliberately stays conservative:
78
+ * reject values that cannot be safely round-tripped through the UTF-8 state file
79
+ * without imposing a specific encoding on future protocol versions.
80
+ */
81
+ export function validateWechatSyncCursor(cursor) {
82
+ if (typeof cursor !== 'string') {
83
+ return { valid: false, reason: 'invalid_type' };
84
+ }
85
+ if (Buffer.byteLength(cursor, 'utf8') > MAX_SYNC_CURSOR_BYTES) {
86
+ return { valid: false, reason: 'too_large' };
87
+ }
88
+ if (cursor.includes('\uFFFD')) {
89
+ return { valid: false, reason: 'invalid_utf8' };
90
+ }
91
+ if (/[\s\u0000-\u001F\u007F-\u009F]/u.test(cursor)) {
92
+ return { valid: false, reason: 'invalid_character' };
93
+ }
94
+ return { valid: true };
95
+ }
74
96
  /** Classify fetch/undici failures using the categories exposed by Tencent 2.4.6. */
75
97
  export function classifyFetchError(err) {
76
98
  if (err instanceof Error && err.name === 'AbortError') {
@@ -196,6 +218,7 @@ export class WechatChannel {
196
218
  pendingContextMessages = new Map();
197
219
  typingTicketCache = new Map();
198
220
  getUpdatesBuf = '';
221
+ lastRejectedSyncCursor;
199
222
  syncBufPath;
200
223
  contextTokensPath;
201
224
  onSessionExpired;
@@ -243,14 +266,24 @@ export class WechatChannel {
243
266
  throw new Error('WeChat reauthentication required; run: ec init wechat');
244
267
  }
245
268
  // 恢复游标
269
+ this.getUpdatesBuf = '';
270
+ this.lastRejectedSyncCursor = undefined;
246
271
  try {
247
272
  if (fs.existsSync(this.syncBufPath)) {
248
- this.getUpdatesBuf = fs.readFileSync(this.syncBufPath, 'utf-8');
249
- logger.info(`[WeChat] Restored sync cursor (${this.getUpdatesBuf.length} bytes)`);
273
+ const restoredCursor = fs.readFileSync(this.syncBufPath, 'utf-8');
274
+ const validation = validateWechatSyncCursor(restoredCursor);
275
+ if (validation.valid) {
276
+ this.getUpdatesBuf = restoredCursor;
277
+ logger.info(`[WeChat] Restored sync cursor (${Buffer.byteLength(restoredCursor, 'utf8')} bytes)`);
278
+ }
279
+ else {
280
+ this.clearSyncCursor(`persisted cursor is invalid (${validation.reason})`, restoredCursor);
281
+ }
250
282
  }
251
283
  }
252
- catch {
253
- // ignore
284
+ catch (err) {
285
+ logger.warn(`[WeChat] Failed to restore sync cursor; starting from an empty cursor: ${err instanceof Error ? err.message : String(err)}`);
286
+ this.getUpdatesBuf = '';
254
287
  }
255
288
  // 通知 ilink 后端:bot 上线
256
289
  try {
@@ -431,13 +464,22 @@ export class WechatChannel {
431
464
  }
432
465
  consecutiveFailures = 0;
433
466
  // 保存游标
434
- if (resp.get_updates_buf) {
435
- this.getUpdatesBuf = resp.get_updates_buf;
436
- try {
437
- fs.writeFileSync(this.syncBufPath, this.getUpdatesBuf, 'utf-8');
467
+ if (resp.get_updates_buf !== undefined) {
468
+ const validation = validateWechatSyncCursor(resp.get_updates_buf);
469
+ if (!validation.valid) {
470
+ this.clearSyncCursor(`server returned an invalid cursor (${validation.reason})`, typeof resp.get_updates_buf === 'string' ? resp.get_updates_buf : undefined);
438
471
  }
439
- catch {
440
- // best-effort
472
+ else if (resp.get_updates_buf) {
473
+ if (resp.get_updates_buf !== this.lastRejectedSyncCursor) {
474
+ this.lastRejectedSyncCursor = undefined;
475
+ }
476
+ this.getUpdatesBuf = resp.get_updates_buf;
477
+ try {
478
+ fs.writeFileSync(this.syncBufPath, this.getUpdatesBuf, 'utf-8');
479
+ }
480
+ catch {
481
+ // best-effort
482
+ }
441
483
  }
442
484
  }
443
485
  // 处理消息
@@ -450,6 +492,17 @@ export class WechatChannel {
450
492
  return;
451
493
  if (this.isSessionPaused())
452
494
  return;
495
+ if (err instanceof WechatApiError &&
496
+ err.endpoint === 'ilink/bot/getupdates' &&
497
+ err.hasCode(INVALID_SYNC_CURSOR_ERRCODE) &&
498
+ this.getUpdatesBuf &&
499
+ this.getUpdatesBuf !== this.lastRejectedSyncCursor) {
500
+ const rejectedCursor = this.getUpdatesBuf;
501
+ this.clearSyncCursor('getUpdates returned ret=-1', rejectedCursor);
502
+ consecutiveFailures = 0;
503
+ logger.warn('[WeChat] Retrying getUpdates immediately with an empty sync cursor');
504
+ continue;
505
+ }
453
506
  consecutiveFailures++;
454
507
  if (err instanceof WechatApiError) {
455
508
  logger.error(`[WeChat] getUpdates API error (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}): ${err.message}`);
@@ -470,6 +523,24 @@ export class WechatChannel {
470
523
  }
471
524
  }
472
525
  }
526
+ clearSyncCursor(reason, rejectedCursor) {
527
+ if (rejectedCursor)
528
+ this.lastRejectedSyncCursor = rejectedCursor;
529
+ this.getUpdatesBuf = '';
530
+ try {
531
+ let persistedAction = 'no persisted cursor';
532
+ if (fs.existsSync(this.syncBufPath)) {
533
+ const quarantinePath = `${this.syncBufPath}.corrupt-${Date.now()}`;
534
+ fs.renameSync(this.syncBufPath, quarantinePath);
535
+ persistedAction = `quarantined as ${path.basename(quarantinePath)}`;
536
+ }
537
+ logger.warn(`[WeChat] Cleared sync cursor: ${reason} (${persistedAction})`);
538
+ }
539
+ catch (err) {
540
+ logger.warn(`[WeChat] Cleared in-memory sync cursor but failed to quarantine persisted cursor: ` +
541
+ `${err instanceof Error ? err.message : String(err)}`);
542
+ }
543
+ }
473
544
  // ── Inbound Message Handler ──────────────────────────────────────────
474
545
  async handleInboundMessage(msg) {
475
546
  if (msg.message_type !== MSG_TYPE_USER)