evolcore 0.0.28 → 0.0.29

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 (183) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/bin/ec-safe-output.js +270 -54
  3. package/dist/agents/baseagent-availability.js +37 -1
  4. package/dist/agents/baseagent.js +19 -9
  5. package/dist/agents/claude-runner.js +171 -13
  6. package/dist/agents/codex/app-server-client.js +6 -2
  7. package/dist/agents/codex/diagnostics.js +4 -3
  8. package/dist/agents/codex-runner.js +396 -95
  9. package/dist/agents/gemini-runner.js +35 -17
  10. package/dist/aun/group/history.js +130 -90
  11. package/dist/aun/group/operations.js +1 -3
  12. package/dist/aun/menu/transport.js +5 -3
  13. package/dist/aun/msg/managed-operation.js +70 -0
  14. package/dist/aun/msg/p2p.js +1 -3
  15. package/dist/aun/msg/upload.js +2 -2
  16. package/dist/aun/outbox.js +1 -37
  17. package/dist/channels/aun.js +249 -89
  18. package/dist/channels/daemon.js +18 -1
  19. package/dist/channels/feishu.js +1 -2
  20. package/dist/cli/agent-command.js +23 -3
  21. package/dist/cli/agent-create.js +68 -2
  22. package/dist/cli/agent.js +12 -2
  23. package/dist/cli/aun-commands.js +87 -13
  24. package/dist/cli/cli-argv.js +26 -0
  25. package/dist/cli/daemon-commands.js +40 -10
  26. package/dist/cli/data-command.js +4 -4
  27. package/dist/cli/handoff-command.js +2 -0
  28. package/dist/cli/index.js +18 -1
  29. package/dist/cli/managed-command-guard.js +10 -0
  30. package/dist/cli/memory-command.js +441 -0
  31. package/dist/cli/restart-monitor.js +62 -6
  32. package/dist/cli/skill-command.js +41 -0
  33. package/dist/cli/task-context.js +2 -23
  34. package/dist/cli/trigger-command.js +9 -1
  35. package/dist/cli/watch-msg-diag.js +209 -0
  36. package/dist/config/config-field-policy.js +52 -3
  37. package/dist/config/config-manager.js +17 -4
  38. package/dist/config/contact-book-store.js +2 -2
  39. package/dist/config/mention-mode.js +7 -2
  40. package/dist/config/merge.js +28 -9
  41. package/dist/config/resolved-config-op.js +1 -1
  42. package/dist/config/runtime-env.js +137 -0
  43. package/dist/config/schema-registry.js +11 -1
  44. package/dist/config-store.js +10 -10
  45. package/dist/core/audit/event-key.js +3 -1
  46. package/dist/core/audit/log-integrity.js +4 -0
  47. package/dist/core/auth/agent-delegation.js +2 -1
  48. package/dist/core/auth/auth-gateway.js +13 -6
  49. package/dist/core/auth/authorization-audit.js +54 -19
  50. package/dist/core/auth/operation-authorizer.js +17 -0
  51. package/dist/core/auth/operation-catalog.js +9 -0
  52. package/dist/core/causation/context.js +5 -1
  53. package/dist/core/command/agent-control.js +1 -2
  54. package/dist/core/command/command-handler.js +51 -23
  55. package/dist/core/command/memory-slash.js +270 -0
  56. package/dist/core/command/menu-catalog.js +1 -0
  57. package/dist/core/command/menu-handler.js +315 -60
  58. package/dist/core/command/menu-protocol.js +42 -4
  59. package/dist/core/command/slash-gate.js +10 -10
  60. package/dist/core/command/slash-handler.js +206 -64
  61. package/dist/core/data-migration.js +282 -505
  62. package/dist/core/event-catalog.js +26 -0
  63. package/dist/core/evolagent-registry.js +6 -0
  64. package/dist/core/evolagent.js +0 -10
  65. package/dist/core/handoff/store.js +1 -20
  66. package/dist/core/inference/text-inference.js +57 -1
  67. package/dist/core/memory/consolidation-context.js +505 -0
  68. package/dist/core/memory/consolidation-ops-records.js +124 -0
  69. package/dist/core/memory/consolidation-trigger.js +394 -0
  70. package/dist/core/memory/index-manager.js +308 -0
  71. package/dist/core/memory/launch-consolidation.js +395 -0
  72. package/dist/core/memory/memory-admin.js +234 -0
  73. package/dist/core/memory/memory-document.js +100 -0
  74. package/dist/core/memory/memory-session-policy.js +41 -0
  75. package/dist/core/memory/memory-signature.js +257 -0
  76. package/dist/core/memory/memory-writer.js +432 -0
  77. package/dist/core/memory/staleness.js +117 -0
  78. package/dist/core/message/auto-mention-gate.js +454 -0
  79. package/dist/core/message/im-renderer.js +69 -16
  80. package/dist/core/message/message-bridge.js +287 -21
  81. package/dist/core/message/message-log.js +1 -0
  82. package/dist/core/message/message-queue.js +114 -27
  83. package/dist/core/message/pending-hints.js +329 -161
  84. package/dist/core/message/response-engine.js +567 -80
  85. package/dist/core/permission/ec-command-parser.js +1029 -80
  86. package/dist/core/permission/protected-paths.js +376 -126
  87. package/dist/core/permission/sandbox-runtime.js +40 -12
  88. package/dist/core/permission/tool-error-code.js +96 -13
  89. package/dist/core/permission/tool-policy.js +276 -23
  90. package/dist/core/role/runtime-policy.js +3 -7
  91. package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
  92. package/dist/core/session/session-fs-store.js +0 -76
  93. package/dist/core/session/session-idle-monitor.js +355 -0
  94. package/dist/core/session/session-manager.js +478 -17
  95. package/dist/core/session/session-mapper.js +33 -16
  96. package/dist/core/session/session-renew.js +217 -155
  97. package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
  98. package/dist/core/skill/global-watcher.js +282 -0
  99. package/dist/core/skill/index-generator.js +84 -0
  100. package/dist/core/skill/startup.js +73 -0
  101. package/dist/core/skill/switch-resolver.js +115 -0
  102. package/dist/core/task/task-activity-store.js +219 -0
  103. package/dist/eck/kit-renderer.js +30 -3
  104. package/dist/eck/manifest-engine.js +26 -4
  105. package/dist/index.js +708 -18
  106. package/dist/ipc.js +564 -2
  107. package/dist/paths.js +30 -9
  108. package/dist/response-system/engines/v1/proactive-flow.js +116 -15
  109. package/dist/response-system/modes/single-session/index.js +3 -0
  110. package/dist/trigger/feedback.js +1 -0
  111. package/dist/trigger/manager.js +0 -10
  112. package/dist/trigger/scheduler.js +11 -0
  113. package/dist/trigger/validation.js +22 -0
  114. package/dist/utils/avatar-upload.js +59 -18
  115. package/dist/utils/error-dict.json +6 -3
  116. package/dist/utils/error-utils.js +27 -0
  117. package/dist/utils/logger.js +32 -10
  118. package/dist/utils/media-cache.js +84 -0
  119. package/dist/utils/msg-diag.js +97 -0
  120. package/dist/utils/welcome.js +37 -8
  121. package/kits/docs/AUTHORING.md +314 -0
  122. package/kits/docs/GUIDE.md +2 -0
  123. package/kits/docs/INDEX.md +2 -0
  124. package/kits/docs/aun/CHEATSHEET.md +4 -0
  125. package/kits/docs/aun/SDK.md +60 -0
  126. package/kits/docs/context-assembly.md +10 -6
  127. package/kits/docs/evolcore/INDEX.md +17 -3
  128. package/kits/docs/evolcore/agent-create.md +24 -0
  129. package/kits/docs/evolcore/agent.md +5 -2
  130. package/kits/docs/evolcore/config.md +1 -1
  131. package/kits/docs/evolcore/ec-capabilities.md +364 -0
  132. package/kits/docs/evolcore/group.md +34 -18
  133. package/kits/docs/evolcore/memory-architecture.md +78 -0
  134. package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
  135. package/kits/docs/evolcore/memory-consolidation.md +263 -0
  136. package/kits/docs/evolcore/memory-tags.md +63 -0
  137. package/kits/docs/evolcore/memory.md +110 -0
  138. package/kits/docs/evolcore/menu-client.md +27 -11
  139. package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
  140. package/kits/docs/evolcore/menu-protocol.md +213 -0
  141. package/kits/docs/evolcore/msg.md +2 -0
  142. package/kits/docs/evolcore/trigger.md +7 -5
  143. package/kits/docs/path-registry.md +6 -8
  144. package/kits/docs/prompt-loading-architecture.md +5 -3
  145. package/kits/docs/venues/group.md +5 -4
  146. package/kits/eck_manifest.json +294 -88
  147. package/kits/eck_manifest.memory.json +257 -0
  148. package/kits/rules/03-identity.md +8 -8
  149. package/kits/rules/05-venue.md +4 -2
  150. package/kits/rules/06-channel.md +1 -1
  151. package/kits/schemas/_meta.json +276 -47
  152. package/kits/schemas/agent-config.schema.12.json +38 -2
  153. package/kits/schemas/agent-config.schema.13.json +591 -0
  154. package/kits/schemas/daemon.schema.6.json +9 -9
  155. package/kits/schemas/defaults.schema.5.json +29 -0
  156. package/kits/schemas/relation-config.schema.8.json +14 -1
  157. package/kits/schemas/relation-config.schema.9.json +129 -0
  158. package/kits/schemas/role-config.schema.1.json +3 -3
  159. package/kits/skills/.switches.json +3 -0
  160. package/kits/skills/ec-skill/SKILL.md +217 -0
  161. package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
  162. package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
  163. package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
  164. package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
  165. package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
  166. package/kits/templates/decision/auto-mention.md +25 -0
  167. package/kits/templates/decision/session-renew.md +22 -0
  168. package/kits/templates/memory-consolidation/forget.md +104 -0
  169. package/kits/templates/memory-consolidation/generalize.md +76 -0
  170. package/kits/templates/memory-consolidation/notes.md +133 -0
  171. package/kits/templates/memory-consolidation/profile.md +58 -0
  172. package/kits/templates/memory-consolidation/promote.md +72 -0
  173. package/kits/templates/memory-consolidation/workflow.md +75 -0
  174. package/kits/templates/system-fragments/bootstrap.md +14 -5
  175. package/kits/templates/system-fragments/commands.md +1 -0
  176. package/kits/templates/system-fragments/group-history.md +6 -0
  177. package/kits/templates/system-fragments/identity.md +0 -3
  178. package/kits/templates/system-fragments/memory.md +79 -0
  179. package/kits/templates/system-fragments/reply.md +1 -0
  180. package/kits/templates/system-fragments/venue.md +1 -1
  181. package/package.json +4 -6
  182. package/dist/trigger/legacy-session-history.js +0 -19
  183. package/kits/templates/system-fragments/group-unread-history.md +0 -7
@@ -11,13 +11,13 @@ import { LogWriter } from '../utils/log-writer.js';
11
11
  import { middleOutputModePolicy, resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
12
12
  import { DEFAULT_FLUSH_DELAY_SECONDS } from '../types.js';
13
13
  import { resolvePaths, agentDir as agentDirPath, resolveRoot, channelStatePath } from '../paths.js';
14
- import { saveToUploads, sanitizeFileName, bufferToInboundImage, safeFetch } from '../utils/media-cache.js';
14
+ import { saveToUploads, saveRelationAttachment, sanitizeFileName, bufferToInboundImage, safeFetch } from '../utils/media-cache.js';
15
15
  import { appendAidEvent } from '../utils/instance-registry.js';
16
16
  import { appendMessageLog, appendMessageLogStrict, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog, findMessageLogOperationMessageId, hasMessageLogOperation } from '../core/message/message-log.js';
17
17
  import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
18
18
  import { chatDirPath } from '../core/session/session-fs-store.js';
19
- import { readTopicLable } from '../core/session/session-mapper.js';
20
- import { appendHintAdd, appendHintRemove, parseInjectRequest } from '../core/message/pending-hints.js';
19
+ import { readTopicLabel, readTopicStatus } from '../core/session/session-mapper.js';
20
+ import { appendHintAddDetailed, appendHintRemoveDetailed, appendHintPromoteDetailed, parseInjectRequest, peekHints, withHintTargetLock } from '../core/message/pending-hints.js';
21
21
  import { appendAidLifecycle } from '../aun/aid/identity.js';
22
22
  import { enableFullGroupPullPagination, enableReliableMessageDelivery, getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
23
23
  import { MAX_AUN_ATTACHMENT_SIZE, uploadBufferAndBuildPayload, uploadFileAndBuildPayload } from '../aun/msg/upload.js';
@@ -25,7 +25,7 @@ import { loadAgent } from '../config-store.js';
25
25
  import { resolveAgentLifecycle } from '../config/lifecycle.js';
26
26
  import { resolveEffective } from '../config/config-manager.js';
27
27
  import { isManagementRole } from '../config/builtin-roles.js';
28
- import { normalizeMentionMode } from '../config/mention-mode.js';
28
+ import { normalizeAunMentionMode, normalizeMentionMode } from '../config/mention-mode.js';
29
29
  import { getProcessStartTime } from '../utils/process-introspect.js';
30
30
  import * as outbox from '../aun/outbox.js';
31
31
  import { formatSize } from '../utils/media-cache.js';
@@ -1338,7 +1338,10 @@ export class AUNChannel {
1338
1338
  // Keep the command contract consistent for mixed friendly/canonical @
1339
1339
  // tokens: selector labels are removed above, then known AID-shaped tokens
1340
1340
  // anywhere in the command are removed by the existing helper.
1341
- return this.stripAllMentions(commandText);
1341
+ const stripped = this.stripAllMentions(commandText);
1342
+ // Keep the removed /mentionmode spelling compatible at the channel
1343
+ // boundary while the command handler uses /mention internally.
1344
+ return stripped.replace(/^\/mentionmode(?=\s|$)/, '/mention');
1342
1345
  }
1343
1346
  parsePayloadMentionsOrReject(payload, context, messageId, seq) {
1344
1347
  const raw = payload && typeof payload === 'object' && !Array.isArray(payload)
@@ -1391,7 +1394,7 @@ export class AUNChannel {
1391
1394
  return sanitized;
1392
1395
  return undefined;
1393
1396
  }
1394
- buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId, lable) {
1397
+ buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId, label, topicStatus) {
1395
1398
  if (!groupId)
1396
1399
  throw new Error('buildGroupReplyContext requires groupId');
1397
1400
  const replyContext = {
@@ -1409,11 +1412,13 @@ export class AUNChannel {
1409
1412
  replyContext.metadata = {};
1410
1413
  replyContext.metadata.topicName = topicName;
1411
1414
  }
1412
- if (lable && threadId)
1413
- replyContext.metadata.lable = lable;
1415
+ if (label && threadId)
1416
+ replyContext.metadata.label = label;
1417
+ if (topicStatus && threadId)
1418
+ replyContext.metadata.topicStatus = topicStatus;
1414
1419
  return replyContext;
1415
1420
  }
1416
- buildPrivateReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, refMessageId, lable) {
1421
+ buildPrivateReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, refMessageId, label, topicStatus) {
1417
1422
  const replyContext = {
1418
1423
  metadata: { encrypted, chatmode, refMessageId },
1419
1424
  delivery: { chatType: 'private' },
@@ -1425,8 +1430,10 @@ export class AUNChannel {
1425
1430
  replyContext.replyToMessageId = messageId;
1426
1431
  if (topicName && threadId)
1427
1432
  replyContext.metadata.topicName = topicName;
1428
- if (lable && threadId)
1429
- replyContext.metadata.lable = lable;
1433
+ if (label && threadId)
1434
+ replyContext.metadata.label = label;
1435
+ if (topicStatus && threadId)
1436
+ replyContext.metadata.topicStatus = topicStatus;
1430
1437
  return replyContext;
1431
1438
  }
1432
1439
  claimInboundMessage(kind, messageId, seq) {
@@ -1545,7 +1552,7 @@ export class AUNChannel {
1545
1552
  peerInfoCache = new Map();
1546
1553
  peerInfoRevision = 0;
1547
1554
  messageSeqMap = new Map(); // messageId → seq (for ack/diagnostics)
1548
- sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
1555
+ sentCount = new Map(); // "channelId\0threadId" → 本轮已发消息计数(seqInTurn/turnMessageCount 用)
1549
1556
  peerE2ee = new Map();
1550
1557
  static E2EE_PROBE_TTL = 10 * 60 * 1000; // 10min
1551
1558
  plaintextRecv = 0;
@@ -1842,6 +1849,18 @@ export class AUNChannel {
1842
1849
  const kind = (data && typeof data === 'object') ? data.kind ?? '' : '';
1843
1850
  const keys = (data && typeof data === 'object') ? Object.keys(data).join(',') : typeof data;
1844
1851
  logger.debug(`${this.logPrefix()}[DIAG] message.received: kind=${kind} keys=${keys}`);
1852
+ // 最早入站打点:SDK message.received 回调触发瞬间
1853
+ if (data && typeof data === 'object') {
1854
+ const d = data;
1855
+ const payloadType = d.payload?.type ?? d.envelope?.type ?? '';
1856
+ if (payloadType === 'text') {
1857
+ const msgId = d.message_id ?? d.id ?? '';
1858
+ const from = d.from ?? d.envelope?.from ?? '';
1859
+ const to = d.to ?? d.envelope?.to ?? this.config.aid ?? '';
1860
+ const text = d.payload?.text ?? '';
1861
+ import('../utils/msg-diag.js').then(m => m.diagLog('sdk_received', { aid: this.config.aid, from, to, msgId, text })).catch(() => { });
1862
+ }
1863
+ }
1845
1864
  await this.handleIncomingPrivateMessage(data);
1846
1865
  });
1847
1866
  // pureIdentity(控制 AID):协议层不接群消息,不注册 group 创建监听
@@ -2264,24 +2283,39 @@ export class AUNChannel {
2264
2283
  }
2265
2284
  const fileParts = [];
2266
2285
  for (const att of rawAttachments) {
2267
- const filePath = await this.downloadAttachment(att, channelId, delivery);
2268
- if (!filePath)
2286
+ const attachmentBuffer = await this.downloadAttachment(att, channelId, delivery);
2287
+ if (!attachmentBuffer)
2269
2288
  continue;
2270
2289
  const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
2271
2290
  let img = null;
2272
2291
  try {
2273
- const { readFileSync } = await import('node:fs');
2274
- img = await bufferToInboundImage(readFileSync(filePath), {
2292
+ img = await bufferToInboundImage(attachmentBuffer, {
2275
2293
  contentType: att.content_type, mimeType: att.mime_type, filename: name,
2276
2294
  });
2277
2295
  }
2278
2296
  catch { /* read failed, treat as non-image file */ }
2279
2297
  if (img) {
2298
+ try {
2299
+ const attachment = saveRelationAttachment(attachmentBuffer, this.config.aid, formatPeerKey('aun', channelId), img.mimeType);
2300
+ img = { ...img, attachmentId: attachment.attachmentId, localPath: attachment.filePath };
2301
+ }
2302
+ catch (error) {
2303
+ logger.warn(`${this.logPrefix()} Failed to persist relation attachment for ${name}: ${error instanceof Error ? error.message : String(error)}`);
2304
+ }
2280
2305
  images.push(img);
2281
2306
  // 图片已注入视觉通道,不追加 [文件: …] 文本行
2282
2307
  }
2283
2308
  else {
2284
- fileParts.push(`[文件: ${name} → ${filePath}]`);
2309
+ const projectPath = this.projectPathProvider
2310
+ ? await this.projectPathProvider(channelId)
2311
+ : process.cwd();
2312
+ try {
2313
+ const saved = saveToUploads(attachmentBuffer, name, projectPath);
2314
+ fileParts.push(`[文件: ${name} → ${saved.filePath}]`);
2315
+ }
2316
+ catch (error) {
2317
+ logger.warn(`${this.logPrefix()} saveToUploads failed for ${name}: ${error}`);
2318
+ }
2285
2319
  }
2286
2320
  }
2287
2321
  const parts = [];
@@ -2392,18 +2426,7 @@ export class AUNChannel {
2392
2426
  return null;
2393
2427
  }
2394
2428
  }
2395
- const projectPath = this.projectPathProvider
2396
- ? await this.projectPathProvider(channelId)
2397
- : process.cwd();
2398
- try {
2399
- const result = saveToUploads(buffer, filename, projectPath);
2400
- logger.info(`${this.logPrefix()} Saved attachment: ${result.filePath} (${result.size} bytes)`);
2401
- return result.filePath;
2402
- }
2403
- catch (e) {
2404
- logger.warn(`${this.logPrefix()} saveToUploads failed for ${filename}: ${e}`);
2405
- return null;
2406
- }
2429
+ return buffer;
2407
2430
  }
2408
2431
  extractAttachmentUrlToken(url) {
2409
2432
  try {
@@ -2555,9 +2578,13 @@ export class AUNChannel {
2555
2578
  // forwardInbound 内部排除 self-echo 与 from-owner。
2556
2579
  // 显式排除 observer.inject:它是 owner 对本 agent 的控制消息,不应镜像给观察者
2557
2580
  // (即便日后 from-owner 排除规则调整,也不会泄漏)。
2558
- if (inboundType !== AUNChannel.INJECT_REQUEST_TYPE) {
2581
+ if (typeof inboundType !== 'string' || !inboundType.startsWith('observer.inject')) {
2559
2582
  void this.forwardInbound(msg);
2560
2583
  }
2584
+ if (inboundType === 'observer.inject.status' || inboundType === 'observer.inject.ack') {
2585
+ this.acknowledgeImmediately(messageId, seq);
2586
+ return;
2587
+ }
2561
2588
  // 回声过滤:自己发出的消息会被 gateway fanout 回来,
2562
2589
  // 只有 from_aid == self 且 chat_id 不匹配时才丢弃(说明是其它实例发的)
2563
2590
  const msgChatId = typeof payload === 'object' && payload !== null && payload.chat_id;
@@ -2593,6 +2620,9 @@ export class AUNChannel {
2593
2620
  ? payload.type
2594
2621
  : '';
2595
2622
  logger.info(`${this.logPrefix()} P2P dispatch decision: mid=${messageId} from=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${p2pPayloadType} chatId=${chatId} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
2623
+ if (p2pPayloadType === 'text') {
2624
+ import('../utils/msg-diag.js').then(m => m.diagLog('aun_in', { aid: this.config.aid, from: fromAid, to: this.config.aid, msgId: messageId, text })).catch(() => { });
2625
+ }
2596
2626
  // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
2597
2627
  if (p2pPayloadType === 'action_card_reply') {
2598
2628
  this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
@@ -2640,7 +2670,9 @@ export class AUNChannel {
2640
2670
  // observer 插话(v0.3):只落盘到 pending-hints,不进 Agent、不回 owner。详见 docs/observer-insert-design.md。
2641
2671
  if (p2pPayloadType === AUNChannel.INJECT_REQUEST_TYPE) {
2642
2672
  this.acknowledgeImmediately(messageId, seq);
2643
- this.handleObserverInject(fromAid, payload, displayName, peerIdentity.type);
2673
+ void Promise.resolve(this.handleObserverInject(fromAid, payload, displayName, peerIdentity.type)).catch(error => {
2674
+ logger.error(`${this.logPrefix()} observer.inject failed: ${error instanceof Error ? error.message : String(error)}`);
2675
+ });
2644
2676
  return;
2645
2677
  }
2646
2678
  // bind.request:Evol App 绑定请求。绕过白名单透传原始 payload JSON 给 bridge,
@@ -2678,7 +2710,7 @@ export class AUNChannel {
2678
2710
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: fromAid, msgId: messageId, kind: 'text', len: finalText.length });
2679
2711
  const isSystemP2P = p2pPayloadType === 'event';
2680
2712
  this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P, msgEncrypted, msgChatmode, isSystemP2P ? 'notify' : 'send');
2681
- const replyContext = this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId, msgChatmode, topicName, refMessageId, readTopicLable(payload));
2713
+ const replyContext = this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId, msgChatmode, topicName, refMessageId, readTopicLabel(payload), readTopicStatus(payload));
2682
2714
  this.dispatchMessage({
2683
2715
  channelId: chatId,
2684
2716
  userId: fromAid,
@@ -2725,9 +2757,9 @@ export class AUNChannel {
2725
2757
  const senderAid = env.from ?? '';
2726
2758
  const payload = msg.payload ?? '';
2727
2759
  const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
2728
- if (inboundType === 'observer.forward') {
2760
+ if (inboundType === 'observer.forward' || (typeof inboundType === 'string' && inboundType.startsWith('observer.inject'))) {
2729
2761
  this.acknowledgeImmediately(messageId, seq);
2730
- logger.debug(`${this.logPrefix()} Group dropped observer.forward control envelope: mid=${messageId}`);
2762
+ logger.debug(`${this.logPrefix()} Group dropped observer control envelope: mid=${messageId}`);
2731
2763
  return;
2732
2764
  }
2733
2765
  const payloadMentionEntries = this.parsePayloadMentionsOrReject(payload, 'group.inbound', messageId, seq);
@@ -2790,7 +2822,7 @@ export class AUNChannel {
2790
2822
  peerName: displayName,
2791
2823
  peerType: peerInfo?.type || 'unknown',
2792
2824
  seq,
2793
- replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName, groupId, readTopicLable(payload)),
2825
+ replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName, groupId, readTopicLabel(payload), readTopicStatus(payload)),
2794
2826
  createdAt,
2795
2827
  });
2796
2828
  return;
@@ -2827,8 +2859,8 @@ export class AUNChannel {
2827
2859
  if (!msgEncrypted)
2828
2860
  this.plaintextRecv++;
2829
2861
  // mention_mode 是唯一的服务端群 @ 策略字段;缺失/非法时使用协议内置默认 disabled。
2830
- const serverMentionMode = normalizeMentionMode(msg.mention_mode)
2831
- ?? normalizeMentionMode(payload?.mention_mode)
2862
+ const serverMentionMode = normalizeAunMentionMode(msg.mention_mode)
2863
+ ?? normalizeAunMentionMode(payload?.mention_mode)
2832
2864
  ?? 'disabled';
2833
2865
  const localMentionMode = normalizeMentionMode(this.mentionModeResolver
2834
2866
  ? await this.mentionModeResolver(groupId).catch(() => undefined)
@@ -2846,6 +2878,13 @@ export class AUNChannel {
2846
2878
  // 命令判定:剥离所有 @ 后看是否 / 开头(多 @ 场景如 @a @b /status 也能正确识别)。
2847
2879
  // echo 消息走独立的 trace 流程,不参与命令语义判定。
2848
2880
  const isCommandMsg = !isEchoMsg && slashCommandText.startsWith('/');
2881
+ const commandMentionAids = payloadMentionAids.length > 0
2882
+ ? payloadMentionAids
2883
+ : this.extractMentionAidsFromText(text);
2884
+ const commandTargetsAnotherAgent = isCommandMsg
2885
+ && commandMentionAids.length > 0
2886
+ && !commandMentionAids.includes(this._aid ?? '')
2887
+ && !mentionedAll;
2849
2888
  const groupSlashRequireMentions = this.groupSlashRequireMentionsResolver
2850
2889
  ? await this.groupSlashRequireMentionsResolver().catch(() => undefined) ?? true
2851
2890
  : true;
@@ -2860,7 +2899,7 @@ export class AUNChannel {
2860
2899
  this.pendingEchoMessages.set(messageId, {
2861
2900
  text: echoText,
2862
2901
  channelId: groupId,
2863
- context: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmodeEcho, topicName, groupId, readTopicLable(payload)),
2902
+ context: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmodeEcho, topicName, groupId, readTopicLabel(payload), readTopicStatus(payload)),
2864
2903
  receiveTs: Date.now(),
2865
2904
  });
2866
2905
  // 继续走正常 Agent 流程(下面的代码会 dispatch)
@@ -2876,15 +2915,15 @@ export class AUNChannel {
2876
2915
  // slash 命令默认要求结构化 @;Agent 可通过
2877
2916
  // groupSlashRequireMentions=false 选择仅按正文命令格式处理。
2878
2917
  const enforceMention = (mentionMode === 'mention-only' && !isCommandMsg)
2879
- || (isCommandMsg && groupSlashRequireMentions);
2918
+ || (isCommandMsg && (mentionMode === 'auto' || groupSlashRequireMentions));
2880
2919
  const isMentioned = mentionedSelf || mentionedAll;
2881
2920
  // 过滤逻辑下移到响应层:这里只标记,不过滤
2882
2921
  // 但为了保持现有行为兼容(避免大量未 @ 消息涌入),暂时保留过滤
2883
2922
  // TODO: Phase 6 实施 selective-response 模式后,移除此过滤,只保留 isMentioned 标记
2884
- if (enforceMention && !isMentioned && !isOwnHandoff) {
2923
+ if ((commandTargetsAnotherAgent || (enforceMention && !isMentioned)) && !isOwnHandoff) {
2885
2924
  try {
2886
2925
  if (text.trim() || this.collectAllAttachments(payload).length) {
2887
- this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq, 'unmentioned', 'filtered');
2926
+ this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq, isCommandMsg ? 'command' : 'unmentioned', 'filtered');
2888
2927
  }
2889
2928
  }
2890
2929
  catch (error) {
@@ -2896,7 +2935,8 @@ export class AUNChannel {
2896
2935
  return;
2897
2936
  }
2898
2937
  this.acknowledgeImmediately(messageId, seq);
2899
- 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))})`);
2938
+ const dropReason = commandTargetsAnotherAgent ? 'command-targeted-other-agent' : 'unmentioned';
2939
+ logger.info(`${this.logPrefix()} Group dropped: ${dropReason} (group=${groupId} sender=${senderAid} mid=${messageId} mentionMode=${mentionMode} slashRequireMentions=${groupSlashRequireMentions} isCommand=${isCommandMsg} commandMentionAids=${JSON.stringify(commandMentionAids)} textPreview=${JSON.stringify(text.slice(0, 80))})`);
2900
2940
  return;
2901
2941
  }
2902
2942
  }
@@ -2970,7 +3010,7 @@ export class AUNChannel {
2970
3010
  isMentioned: isOwnHandoff || mentionedSelf || mentionedAll,
2971
3011
  source: isOwnHandoff ? 'handoff' : undefined,
2972
3012
  encrypted: msgEncrypted,
2973
- replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName, groupId, readTopicLable(payload)),
3013
+ replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName, groupId, readTopicLabel(payload), readTopicStatus(payload)),
2974
3014
  mentionMode: serverMentionMode,
2975
3015
  images: inboundImages.length > 0 ? inboundImages : undefined,
2976
3016
  msgType: logDescriptor.msgType,
@@ -3077,7 +3117,7 @@ export class AUNChannel {
3077
3117
  }
3078
3118
  appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId, receivedAt, gatewaySeq, groupAttention, groupDelivery) {
3079
3119
  try {
3080
- const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
3120
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid, this.config.channelName);
3081
3121
  const entry = buildInboundEntry({
3082
3122
  from: fromAid,
3083
3123
  to: this.config.aid,
@@ -3115,6 +3155,7 @@ export class AUNChannel {
3115
3155
  // observable / owners 不在此处缓存——由 daemon 注入 resolver,从 EvolAgent 的
3116
3156
  // in-memory merged config(启动/重启/热重载时统一更新的唯一缓存)读取,避免重复缓存。
3117
3157
  observerConfigResolver;
3158
+ observerHintRuntime;
3118
3159
  observerGroupMembers = new Map();
3119
3160
  observerGroupMemberFetches = new Map();
3120
3161
  observerGroupMembershipEpoch = new Map();
@@ -3122,6 +3163,15 @@ export class AUNChannel {
3122
3163
  setObserverConfigResolver(fn) {
3123
3164
  this.observerConfigResolver = fn;
3124
3165
  }
3166
+ setObserverHintRuntime(runtime) {
3167
+ this.observerHintRuntime = runtime;
3168
+ }
3169
+ notifyObserverHintConsumed(hint, target, meta) {
3170
+ this.emitInjectStatus(hint.ownerAid, hint.id, target, 'consumed', 0, meta?.action ?? (hint.promoteRequestId ? 'promote' : 'add'), meta?.requestId ?? hint.promoteRequestId);
3171
+ }
3172
+ notifyObserverHintFailed(hint, target, failure) {
3173
+ this.emitInjectStatus(hint.ownerAid, hint.id, target, 'failed', 0, failure.action ?? (hint.promoteRequestId ? 'promote' : 'add'), failure.requestId ?? hint.promoteRequestId, failure);
3174
+ }
3125
3175
  /** 读取 observable 开关 + owners;未接入 daemon 时没有 owning Agent,保持关闭。 */
3126
3176
  getObserverConfig() {
3127
3177
  return this.observerConfigResolver?.() ?? { observable: false, owners: [] };
@@ -3235,7 +3285,7 @@ export class AUNChannel {
3235
3285
  const inboundType = payloadTypeForObserver(data);
3236
3286
  // Observer envelopes are control-plane telemetry. Mirroring one again
3237
3287
  // would amplify every activity event across observer-connected agents.
3238
- if (inboundType === 'observer.forward' || inboundType?.startsWith('menu.'))
3288
+ if (inboundType === 'observer.forward' || inboundType?.startsWith('observer.inject') || inboundType?.startsWith('menu.'))
3239
3289
  return Promise.resolve();
3240
3290
  const { observable, owners } = this.getObserverConfig();
3241
3291
  if (!observable || owners.length === 0)
@@ -3272,7 +3322,7 @@ export class AUNChannel {
3272
3322
  if (!this.connected || !this.client)
3273
3323
  return Promise.resolve();
3274
3324
  const outboundType = payloadTypeForObserver(result);
3275
- if (outboundType === 'observer.forward' || outboundType?.startsWith('menu.'))
3325
+ if (outboundType === 'observer.forward' || outboundType?.startsWith('observer.inject') || outboundType?.startsWith('menu.'))
3276
3326
  return Promise.resolve();
3277
3327
  const { observable, owners } = this.getObserverConfig();
3278
3328
  if (!observable || owners.length === 0)
@@ -3317,13 +3367,7 @@ export class AUNChannel {
3317
3367
  .catch(e => logger.debug(`${this.logPrefix()} observer.forward send to ${ownerAid} failed: ${e}`));
3318
3368
  }
3319
3369
  }
3320
- // ── 观察者插话(Observer Insert,v0.3 待用上下文提示) ──────────────
3321
- //
3322
- // owner 经 message.send 给 agent 自身 AID 发 observer.inject(payload 为对象)。
3323
- // 鉴权 from∈owners 后,把提示【只落盘】到 agent↔对端 会话的 pending-hints.jsonl
3324
- // (不 dispatch、不跑 LLM、不回 owner);下一条对端消息到达时由 message-processor
3325
- // 回放消费、注入渲染层。action=add 加提示 / remove 撤销。
3326
- // 详见 docs/observer-insert-design.md 第一部分。
3370
+ // ── 观察者插话 ──────────────
3327
3371
  /** 回 observer.inject.ack 给 owner;加密使用 daemon 的全局 AUN 默认设置。 */
3328
3372
  emitInjectAck(ownerAid, injectId, data, error) {
3329
3373
  if (!this.connected || !this.client)
@@ -3338,12 +3382,51 @@ export class AUNChannel {
3338
3382
  this.callAndTrace('message.send', { to: ownerAid, payload: ackPayload, encrypt: this.shouldEncrypt(ownerAid) })
3339
3383
  .catch(e => logger.warn(`${this.logPrefix()} observer.inject.ack send to ${ownerAid} failed: ${e}`));
3340
3384
  }
3341
- /** 处理 observer.inject:鉴权 + 校验 + 只落盘到 pending-hints(不触发处理、不回 owner)。 */
3342
- handleObserverInject(fromAid, payload, displayName, peerType) {
3385
+ emitInjectStatus(ownerAid, hintId, target, status, attempt = 0, action, requestId, failure) {
3386
+ if (!this.getObserverConfig().owners.includes(ownerAid))
3387
+ return;
3388
+ const payload = {
3389
+ type: 'observer.inject.status', id: hintId,
3390
+ target: { channel_id: target.channelId, chat_type: target.chatType,
3391
+ ...(target.threadId ? { thread_id: target.threadId } : {}) },
3392
+ data: {
3393
+ status,
3394
+ hint_id: hintId,
3395
+ ...(action ? { action } : {}),
3396
+ ...(requestId ? { request_id: requestId } : {}),
3397
+ ...(status === 'consumed' ? { consumed_at: Date.now() } : {}),
3398
+ ...(failure ? { error: { code: failure.code, message: failure.message } } : {}),
3399
+ },
3400
+ };
3401
+ const delivery = this.connected && this.client
3402
+ ? this.callAndTrace('message.send', { to: ownerAid, payload, encrypt: this.shouldEncrypt(ownerAid) })
3403
+ : Promise.reject(new Error('AUN is disconnected'));
3404
+ void delivery.catch(error => {
3405
+ if (attempt < 3) {
3406
+ const timer = setTimeout(() => this.emitInjectStatus(ownerAid, hintId, target, status, attempt + 1, action, requestId, failure), [250, 1000, 5000][attempt]);
3407
+ timer.unref?.();
3408
+ }
3409
+ else
3410
+ logger.warn(`${this.logPrefix()} observer.inject.status delivery failed owner=${ownerAid} hint=${hintId} status=${status}: ${error}`);
3411
+ });
3412
+ }
3413
+ async handleObserverInject(fromAid, payload, displayName, peerType) {
3343
3414
  void peerType;
3344
3415
  const { owners } = this.getObserverConfig();
3345
3416
  const ts = Date.now();
3346
3417
  const req = parseInjectRequest(payload, fromAid, owners, ts);
3418
+ const requestAction = req.kind === 'reject' ? req.action : req.kind;
3419
+ const requestHintId = req.kind === 'add' ? req.id : 'targetId' in req ? req.targetId : undefined;
3420
+ const requestTarget = 'channelId' in req && req.channelId
3421
+ ? { channel_id: req.channelId, chat_type: req.chatType ?? 'private', ...(req.threadId ? { thread_id: req.threadId } : {}) }
3422
+ : undefined;
3423
+ const ackData = (status, hintId) => ({
3424
+ status,
3425
+ action: requestAction,
3426
+ ...(hintId || requestHintId ? { hint_id: hintId || requestHintId } : {}),
3427
+ ...(req.injectId ? { request_id: req.injectId } : {}),
3428
+ ...(requestTarget ? { target: requestTarget } : {}),
3429
+ });
3347
3430
  if (req.kind === 'reject') {
3348
3431
  logger.warn(`${this.logPrefix()} observer.inject rejected: ${this.getShortAid(fromAid)} ${req.code}`);
3349
3432
  this.emitInjectAck(fromAid, req.injectId, { status: 'rejected', action: req.action }, { code: req.code, message: req.message });
@@ -3351,24 +3434,67 @@ export class AUNChannel {
3351
3434
  }
3352
3435
  const selfAID = this.config.aid;
3353
3436
  const sessionsDir = resolvePaths().sessionsDir;
3354
- let ok;
3355
- if (req.kind === 'remove') {
3356
- ok = appendHintRemove(sessionsDir, 'aun', req.channelId, selfAID, { targetId: req.targetId, threadId: req.threadId, ts });
3357
- }
3358
- else {
3359
- ok = appendHintAdd(sessionsDir, 'aun', req.channelId, selfAID, { id: req.id, text: req.text, threadId: req.threadId, ownerAid: req.ownerAid, ts });
3437
+ const channelKey = this.config.channelName ?? 'aun';
3438
+ try {
3439
+ await withHintTargetLock(sessionsDir, 'aun', req.channelId, selfAID, req.chatType, req.threadId, async () => {
3440
+ const immediate = (req.kind === 'add' && req.mode === 'interrupt') || req.kind === 'promote';
3441
+ const target = { channelKey, channelId: req.channelId, chatType: req.chatType, threadId: req.threadId, selfAid: selfAID };
3442
+ if (immediate && (!this.observerHintRuntime || !await this.observerHintRuntime.hasSession(target))) {
3443
+ this.emitInjectAck(fromAid, req.injectId, { status: 'rejected', action: req.kind }, { code: 'SESSION_NOT_FOUND', message: '目标 Agent session 不存在' });
3444
+ return;
3445
+ }
3446
+ const result = req.kind === 'add'
3447
+ ? appendHintAddDetailed(sessionsDir, 'aun', req.channelId, selfAID, {
3448
+ id: req.id, text: req.text, mode: req.mode, chatType: req.chatType,
3449
+ threadId: req.threadId, ownerAid: req.ownerAid, ts,
3450
+ }, channelKey)
3451
+ : req.kind === 'remove'
3452
+ ? appendHintRemoveDetailed(sessionsDir, 'aun', req.channelId, selfAID, {
3453
+ targetId: req.targetId, chatType: req.chatType, threadId: req.threadId, ts,
3454
+ }, channelKey)
3455
+ : appendHintPromoteDetailed(sessionsDir, 'aun', req.channelId, selfAID, {
3456
+ targetId: req.targetId, chatType: req.chatType, threadId: req.threadId, requestId: req.injectId, ts,
3457
+ }, channelKey);
3458
+ if (!result.ok) {
3459
+ this.emitInjectAck(fromAid, req.injectId, { status: 'rejected', action: req.kind }, { code: result.code ?? 'STORE_FAILED', message: '提示状态操作失败' });
3460
+ return;
3461
+ }
3462
+ const hintId = req.kind === 'add' ? req.id : req.targetId;
3463
+ logger.info(`${this.logPrefix()} observer.inject ${req.kind} stored: from=${this.getShortAid(fromAid)}(${displayName}) target=${req.channelId} chatType=${req.chatType} thread=${req.threadId ?? 'main'} hint=${hintId ?? 'all'}`);
3464
+ this.emitInjectAck(fromAid, req.injectId, ackData('accepted', hintId));
3465
+ if (req.kind === 'remove') {
3466
+ for (const hint of result.removedHints ?? [])
3467
+ this.emitInjectStatus(hint.ownerAid, hint.id, target, 'removed', 0, 'remove', req.injectId);
3468
+ }
3469
+ else if (req.kind === 'add') {
3470
+ this.emitInjectStatus(req.ownerAid, req.id, target, result.state ?? 'pending', 0, 'add', req.injectId || req.id);
3471
+ }
3472
+ else {
3473
+ const stored = peekHints(sessionsDir, 'aun', req.channelId, selfAID, req.threadId, req.chatType, undefined, channelKey)
3474
+ .find(item => item.id === req.targetId);
3475
+ if (stored)
3476
+ this.emitInjectStatus(stored.ownerAid, req.targetId, target, 'pending', 0, 'promote', stored.promoteRequestId || req.injectId);
3477
+ }
3478
+ if (immediate && result.state === 'pending') {
3479
+ try {
3480
+ const scheduled = await this.observerHintRuntime.schedule({ ...target, hintId: hintId, requestId: req.injectId });
3481
+ if (scheduled === 'SESSION_NOT_FOUND')
3482
+ logger.warn(`${this.logPrefix()} observer hint session vanished before scheduling hint=${hintId}`);
3483
+ }
3484
+ catch (error) {
3485
+ logger.warn(`${this.logPrefix()} observer hint scheduling failed hint=${hintId}: ${error}`);
3486
+ }
3487
+ }
3488
+ const watchText = req.kind === 'add' ? req.text
3489
+ : req.kind === 'promote' ? `[立即执行提示] id=${req.targetId}`
3490
+ : `[撤销提示]${req.targetId ? ` id=${req.targetId}` : '(全部)'}`;
3491
+ this.recordInjectWatch('in', fromAid, req.channelId, req.chatType, `inject-${req.injectId || ts}`, watchText, req.threadId, channelKey);
3492
+ }, channelKey);
3360
3493
  }
3361
- if (!ok) {
3362
- this.emitInjectAck(fromAid, req.injectId, { status: 'rejected', action: req.kind }, { code: 'STORE_FAILED', message: '提示落盘失败' });
3363
- return;
3494
+ catch (error) {
3495
+ logger.warn(`${this.logPrefix()} observer.inject failed before acceptance: ${error instanceof Error ? error.message : String(error)}`);
3496
+ this.emitInjectAck(fromAid, req.injectId, { status: 'rejected', action: req.kind }, { code: 'STORE_FAILED', message: '提示状态操作失败' });
3364
3497
  }
3365
- logger.info(`${this.logPrefix()} observer.inject ${req.kind} stored: from=${this.getShortAid(fromAid)}(${displayName}) target=${req.channelId} chatType=${req.chatType} thread=${req.threadId ?? 'main'}${req.kind === 'add' ? ` textLen=${req.text.length}` : `${req.targetId ? ` targetId=${req.targetId}` : ' (clear-all)'}`}`);
3366
- this.emitInjectAck(fromAid, req.injectId, { status: 'accepted', action: req.kind });
3367
- // 记录到 watch(被观察的 agent↔对端 会话),带 owner-inject 标记区分对端真实消息。
3368
- // v0.3:只记"提示已添加/已撤销",不触发处理、不产生 agent→owner 回应。
3369
- const watchText = req.kind === 'remove' ? `[撤销提示]${req.targetId ? ` id=${req.targetId}` : '(全部)'}` : req.text;
3370
- const synthId = `inject-${req.injectId || ts}`;
3371
- this.recordInjectWatch('in', fromAid, req.channelId, req.chatType, synthId, watchText, req.threadId);
3372
3498
  }
3373
3499
  /**
3374
3500
  * 把 observer 插话 / 对插话的回应记录到 watch(被观察的 agent↔对端 会话),
@@ -3377,11 +3503,11 @@ export class AUNChannel {
3377
3503
  * @param dir 'in'=owner→agent 插话;'out'=agent→owner 对插话的回应
3378
3504
  * @param peerChannelId 被观察会话的对端(agent↔对端),日志落点 = sessions/aun/<self>/<peerChannelId>/
3379
3505
  */
3380
- recordInjectWatch(dir, ownerAid, peerChannelId, chatType, msgId, text, threadId) {
3506
+ recordInjectWatch(dir, ownerAid, peerChannelId, chatType, msgId, text, threadId, channelKey) {
3381
3507
  try {
3382
3508
  const selfAID = this.config.aid;
3383
3509
  const isGroup = chatType === 'group';
3384
- const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', peerChannelId, selfAID);
3510
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', peerChannelId, selfAID, channelKey);
3385
3511
  const entry = dir === 'in'
3386
3512
  ? buildInboundEntry({
3387
3513
  from: ownerAid, to: selfAID, chatType,
@@ -4144,6 +4270,9 @@ export class AUNChannel {
4144
4270
  if (isMenuPayload && typeof context?.metadata?.menuClientSdkVersion === 'string') {
4145
4271
  params.menu_client_sdk_version = context.metadata.menuClientSdkVersion;
4146
4272
  }
4273
+ if (isMenuPayload && context?.metadata?.menuClientVolatileV1 === true) {
4274
+ params.menu_client_volatile_v1 = true;
4275
+ }
4147
4276
  if (isMenuPayload && encrypt && context?.metadata?.protectedHeaders) {
4148
4277
  params.protected_headers = context.metadata.protectedHeaders;
4149
4278
  }
@@ -4388,17 +4517,24 @@ export class AUNChannel {
4388
4517
  const rawItem = item && typeof item === 'object' && !Array.isArray(item)
4389
4518
  ? { ...item }
4390
4519
  : { kind: 'unknown', text: String(item ?? '') };
4520
+ // Lift evolCommand out of the item and into the payload's ext dict.
4521
+ const evolCommand = typeof rawItem.evolCommand === 'string' ? rawItem.evolCommand : undefined;
4522
+ if (evolCommand)
4523
+ delete rawItem.evolCommand;
4391
4524
  const displayArguments = rawItem.kind === 'tool_call'
4392
4525
  ? toolInputForDisplay(rawItem.name, rawItem.arguments)
4393
4526
  : undefined;
4394
4527
  const activityItem = rawItem.kind === 'tool_call' && displayArguments !== rawItem.arguments
4395
4528
  ? { ...rawItem, arguments: displayArguments }
4396
4529
  : rawItem;
4397
- return withAunAttention({
4530
+ const base = {
4398
4531
  ...this.buildTaskPayloadBase(envelope, context),
4399
4532
  type: 'activity',
4400
4533
  item: this.stripUndefinedDeep(activityItem),
4401
- });
4534
+ };
4535
+ if (evolCommand)
4536
+ base.ext = { evolCommand };
4537
+ return withAunAttention(base);
4402
4538
  }
4403
4539
  async sendReliableStructured(channelId, payload, context, logText) {
4404
4540
  const result = await this.sendContentPayload(channelId, payload, {
@@ -4426,13 +4562,17 @@ export class AUNChannel {
4426
4562
  return { status: 'failed', error: 'message text is empty', code: 'EMPTY_MESSAGE' };
4427
4563
  }
4428
4564
  const delivery = this.requireDelivery(channelId, context);
4429
- const routedContext = this.withDelivery(context, delivery);
4565
+ let routedContext = this.withDelivery(context, delivery);
4430
4566
  // 长 echo: agent 首次回复前先发 echo trace(echo 自身发送时跳过)
4431
4567
  if (!this.isEchoSending) {
4432
4568
  await this.flushPendingEcho(channelId, delivery, context?.threadId);
4433
4569
  }
4434
4570
  let finalText = text;
4435
- this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
4571
+ const turnKey = `${channelId}\0${context?.threadId ?? ''}`;
4572
+ const seqInTurn = (this.sentCount.get(turnKey) || 0) + 1;
4573
+ this.sentCount.set(turnKey, seqInTurn);
4574
+ // Carry seq into context so deliverTextEntry can embed it in ext.
4575
+ routedContext = { ...routedContext, ext: { ...routedContext.ext, seqInTurn } };
4436
4576
  if (delivery.chatType === 'group') {
4437
4577
  const requestedMentions = Array.from(new Set((context?.mentionUserIds ?? [])
4438
4578
  .filter((id) => typeof id === 'string' && id.trim().length > 0)
@@ -4846,12 +4986,23 @@ export class AUNChannel {
4846
4986
  }
4847
4987
  // 从 context.metadata.source 读取 source,默认为 'daemon'
4848
4988
  const source = context?.metadata?.source ?? 'daemon';
4849
- // Put the label in the visible body without requiring client title support.
4850
- // Derive it from the durable context without mutating the stored text.
4851
- const title = delivery.chatType === 'private' ? context?.title?.trim() : undefined;
4852
- const wireText = title && finalText.split(/\r?\n/, 1)[0].trim() !== title
4853
- ? `${title}\n\n${finalText}` : finalText;
4854
- const payload = withAunAttention({ type: 'text', text: wireText }, context?.attention);
4989
+ // Merge ext hints from context into the wire payload.
4990
+ // isFinal: written by response-engine; turnMessageCount: total messages this turn (only on final).
4991
+ let ext = context?.ext
4992
+ ? { ...context.ext }
4993
+ : undefined;
4994
+ if (ext?.isFinal) {
4995
+ const turnKey = `${channelId}\0${context?.threadId ?? ''}`;
4996
+ const total = this.sentCount.get(turnKey) ?? 1;
4997
+ ext = { ...ext, turnMessageCount: total };
4998
+ }
4999
+ const payload = withAunAttention({ type: 'text', text: finalText }, context?.attention);
5000
+ if (ext && Object.keys(ext).length > 0)
5001
+ payload.ext = ext;
5002
+ // sentAt 在每次投递尝试时盖戳(outbox 重发取重发时刻)。服务端会用自己的时间覆盖
5003
+ // 顶层 timestamp,两者有毫秒级差异,sentAt 只代表本端时钟。
5004
+ if (context?.debug)
5005
+ payload.debug = { ...context.debug, sentAt: localTimestamp() };
4855
5006
  if (delivery.chatType === 'group') {
4856
5007
  const extracted = this.extractMentionEntriesFromText(finalText);
4857
5008
  if (extracted.length > 0)
@@ -4876,7 +5027,8 @@ export class AUNChannel {
4876
5027
  registerAunCausation(causationRef, this.config.aid, channelId, causation);
4877
5028
  }
4878
5029
  // 诊断日志:记录 payload 构造结果(含 task_id / thread_id / chatmode)
4879
- logger.info(`${this.logPrefix()} deliverTextEntry: channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} chatmode=${payload.chatmode ?? 'none'} source=${source} textLen=${wireText.length}`);
5030
+ logger.info(`${this.logPrefix()} deliverTextEntry: channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} chatmode=${payload.chatmode ?? 'none'} source=${source} textLen=${finalText.length}`);
5031
+ import('../utils/msg-diag.js').then(m => m.diagLog('aun_out', { aid: this.config.aid, from: this.config.aid, to: channelId, msgId: payload.task_id ?? '', taskId: payload.task_id, text: finalText })).catch(() => { });
4880
5032
  const isGroup = delivery.chatType === 'group';
4881
5033
  const targetAid = channelId;
4882
5034
  if (entry.deliveryReceipt) {
@@ -5348,6 +5500,9 @@ export class AUNChannel {
5348
5500
  if (isMenuPayload && typeof context?.metadata?.menuClientSdkVersion === 'string') {
5349
5501
  params.menu_client_sdk_version = context.metadata.menuClientSdkVersion;
5350
5502
  }
5503
+ if (isMenuPayload && context?.metadata?.menuClientVolatileV1 === true) {
5504
+ params.menu_client_volatile_v1 = true;
5505
+ }
5351
5506
  const rawHeaders = context?.metadata?.protectedHeaders;
5352
5507
  if (encrypt && rawHeaders && typeof rawHeaders === 'object' && !Array.isArray(rawHeaders)) {
5353
5508
  const protectedHeaders = {};
@@ -5427,8 +5582,8 @@ export class AUNChannel {
5427
5582
  logger.warn(`${this.logPrefix()} sendFile: file is empty`);
5428
5583
  return { status: 'failed', error: 'file is empty', code: 'EMPTY_FILE' };
5429
5584
  }
5430
- if (stat.size > 10 * 1024 * 1024) {
5431
- logger.warn(`${this.logPrefix()} sendFile: file too large (${formatSize(stat.size)}, max 10 MB)`);
5585
+ if (stat.size > MAX_AUN_ATTACHMENT_SIZE) {
5586
+ logger.warn(`${this.logPrefix()} sendFile: file too large (${formatSize(stat.size)}, max ${formatSize(MAX_AUN_ATTACHMENT_SIZE)})`);
5432
5587
  return { status: 'failed', error: `file too large (max ${formatSize(MAX_AUN_ATTACHMENT_SIZE)})`, code: 'FILE_TOO_LARGE' };
5433
5588
  }
5434
5589
  // Write-ahead: persist to outbox
@@ -5882,8 +6037,13 @@ export class AUNChannel {
5882
6037
  return;
5883
6038
  }
5884
6039
  context = this.withDelivery(context, delivery);
5885
- if (status === 'start')
5886
- this.sentCount.delete(channelId); // 新任务开始,重置计数
6040
+ if (status === 'start') {
6041
+ // 新任务开始,重置所有以该 channelId 开头的计数(不区分 threadId)
6042
+ for (const key of this.sentCount.keys()) {
6043
+ if (key.startsWith(`${channelId}\0`))
6044
+ this.sentCount.delete(key);
6045
+ }
6046
+ }
5887
6047
  if (!this.client || !this.connected)
5888
6048
  return;
5889
6049
  const requestedSeverity = extraMeta?.severity;
@@ -6252,7 +6412,7 @@ export class AUNChannelPlugin {
6252
6412
  if (replyCtx) {
6253
6413
  replyCtx = { ...replyCtx };
6254
6414
  delete replyCtx.attention;
6255
- // Assign titles per payload below, never inherit a final-reply title.
6415
+ // Do not inherit title from a previous payload's reply context.
6256
6416
  delete replyCtx.title;
6257
6417
  }
6258
6418
  if (attention !== undefined)