evolclaw 3.1.5 → 3.1.7

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 (51) hide show
  1. package/CHANGELOG.md +68 -3
  2. package/dist/agents/claude-runner.js +69 -24
  3. package/dist/agents/kit-renderer.js +78 -321
  4. package/dist/agents/manifest-engine.js +243 -0
  5. package/dist/agents/message-renderer.js +112 -0
  6. package/dist/aun/aid/agentmd.js +10 -3
  7. package/dist/aun/msg/group.js +2 -2
  8. package/dist/channels/aun.js +154 -18
  9. package/dist/channels/dingtalk.js +1 -1
  10. package/dist/channels/feishu.js +31 -9
  11. package/dist/channels/qqbot.js +1 -1
  12. package/dist/channels/wechat.js +1 -1
  13. package/dist/channels/wecom.js +1 -1
  14. package/dist/cli/agent.js +10 -11
  15. package/dist/cli/bench.js +1 -5
  16. package/dist/cli/help.js +8 -0
  17. package/dist/cli/index.js +91 -128
  18. package/dist/cli/init.js +37 -21
  19. package/dist/cli/link-rules.js +1 -7
  20. package/dist/cli/model.js +231 -6
  21. package/dist/config-store.js +1 -22
  22. package/dist/core/command-handler.js +181 -48
  23. package/dist/core/evolagent.js +0 -18
  24. package/dist/core/message/im-renderer.js +9 -20
  25. package/dist/core/message/message-bridge.js +9 -10
  26. package/dist/core/message/message-processor.js +188 -39
  27. package/dist/core/message/message-queue.js +15 -1
  28. package/dist/core/relation/peer-identity.js +23 -11
  29. package/dist/core/trigger/parser.js +4 -4
  30. package/dist/core/trigger/scheduler.js +43 -13
  31. package/dist/index.js +102 -52
  32. package/dist/ipc.js +1 -1
  33. package/dist/utils/error-utils.js +6 -0
  34. package/dist/utils/process-introspect.js +7 -5
  35. package/kits/docs/INDEX.md +4 -8
  36. package/kits/docs/context-assembly.md +1 -0
  37. package/kits/docs/evolclaw/INDEX.md +43 -0
  38. package/kits/docs/evolclaw/group.md +13 -6
  39. package/kits/docs/evolclaw/model.md +51 -0
  40. package/kits/docs/evolclaw/msg.md +5 -0
  41. package/kits/docs/venues/group.md +13 -1
  42. package/kits/eck_manifest.json +9 -0
  43. package/kits/eck_message_manifest.json +14 -0
  44. package/kits/rules/06-channel.md +5 -1
  45. package/kits/templates/message-fragments/item.md +2 -0
  46. package/kits/templates/system-fragments/baseagent.md +7 -1
  47. package/kits/templates/system-fragments/channel.md +7 -5
  48. package/kits/templates/system-fragments/commands.md +19 -0
  49. package/kits/templates/system-fragments/session.md +12 -0
  50. package/kits/templates/system-fragments/venue.md +15 -0
  51. package/package.json +3 -3
@@ -1,5 +1,6 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs';
3
+ import os from 'os';
3
4
  import crypto from 'crypto';
4
5
  import { hasCompact } from '../../agents/claude-runner.js';
5
6
  import { IMRenderer } from './im-renderer.js';
@@ -10,10 +11,41 @@ import { summarizeToolInput } from '../permission.js';
10
11
  import { DEFAULT_PERMISSION_MODE } from '../../types.js';
11
12
  import { getPackageRoot, resolveRoot } from '../../paths.js';
12
13
  import { renderKitSections } from '../../agents/kit-renderer.js';
14
+ import { renderMessageBody } from '../../agents/message-renderer.js';
13
15
  import { normalizeBaseagent } from '../../agents/baseagent-normalize.js';
14
16
  import { renderActionAsText, renderCommandCardAsText } from '../interaction-router.js';
15
17
  import { formatPeerKey } from '../relation/peer-key.js';
16
18
  import { resolveEffectiveModel } from '../model/model-scope.js';
19
+ /** OS 信息在进程生命周期内是常量,模块加载时算一次。例: "Windows 11 Pro (win32 10.0.26200)" */
20
+ const OS_INFO = (() => {
21
+ let label = '';
22
+ try {
23
+ label = os.version();
24
+ }
25
+ catch { /* 旧 Node 无 os.version */ }
26
+ return `${label ? label + ' ' : ''}(${os.platform()} ${os.release()})`;
27
+ })();
28
+ /** 当前 UTC 偏移,格式 +08:00 / -05:00。每条消息算(DST 安全)。 */
29
+ function currentTzOffset() {
30
+ const off = -new Date().getTimezoneOffset(); // 分钟,东区为正
31
+ const sign = off >= 0 ? '+' : '-';
32
+ const abs = Math.abs(off);
33
+ return `${sign}${String(Math.floor(abs / 60)).padStart(2, '0')}:${String(abs % 60).padStart(2, '0')}`;
34
+ }
35
+ /** 当前本地日期 YYYY-MM-DD(按运行环境时区)。系统提示词用,一天才变一次(缓存友好)。 */
36
+ function currentLocalDate() {
37
+ const d = new Date();
38
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
39
+ }
40
+ /** 当前本地星期几(中文,如「星期四」)。 */
41
+ function currentWeekday() {
42
+ try {
43
+ return new Intl.DateTimeFormat('zh-CN', { weekday: 'long' }).format(new Date());
44
+ }
45
+ catch {
46
+ return ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'][new Date().getDay()];
47
+ }
48
+ }
17
49
  function getContextTooLongHint(agent) {
18
50
  if (canCompactAgent(agent)) {
19
51
  return '上下文过长,请精简提问或使用 /compact 压缩上下文';
@@ -69,6 +101,8 @@ export class MessageProcessor {
69
101
  agentMap;
70
102
  primaryRunnerKey;
71
103
  interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
104
+ /** sessionId → 模型降级状态(带退避探测,进程重启清零) */
105
+ modelFallbackMap = new Map();
72
106
  interactionRouter;
73
107
  messageQueue;
74
108
  /** sessionId → 活跃的空闲监控器,用于等待用户交互期间暂停/恢复计时 */
@@ -208,15 +242,15 @@ export class MessageProcessor {
208
242
  }
209
243
  // 命令前缀列表(与 CommandHandler.quickCommandPrefixes 保持同步)
210
244
  static COMMAND_PREFIXES = [
211
- '/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart',
245
+ '/new', '/pwd', '/help', '/status', '/restart',
212
246
  '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
213
247
  '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check',
214
- '/p ', '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/chatmode',
248
+ '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/chatmode',
215
249
  '/aid', '/upgrade', '/evolagent',
216
250
  ];
217
251
  /** 判断消息内容是否为已知命令 */
218
252
  isKnownCommand(content) {
219
- return content === '/p' || content === '/s' ||
253
+ return content === '/s' ||
220
254
  MessageProcessor.COMMAND_PREFIXES.some(cmd => content.startsWith(cmd));
221
255
  }
222
256
  /**
@@ -227,6 +261,29 @@ export class MessageProcessor {
227
261
  // 先解析会话,再优先用 session.metadata.channelKey 精确定位实例级 adapter
228
262
  // message.channel 现在存实例名(channelName),可直接用于精确路由
229
263
  const { session, absoluteProjectPath } = await this.resolveSession(message);
264
+ // thread(feishu) pending strategy: inject replyContext so first reply creates the thread
265
+ if (message.triggerMeta?.pendingThread && message.triggerMeta?.rootMessageId) {
266
+ const triggerId = message.triggerMeta.triggerId;
267
+ const channelKeyForAgent = session.metadata?.channelKey || message.channel;
268
+ const trigMgr = this.agentRegistry?.resolveByChannel(channelKeyForAgent)?.triggerManager;
269
+ const onThreadCreated = trigMgr
270
+ ? (threadId) => {
271
+ try {
272
+ trigMgr.update(triggerId, { targetThreadId: threadId, pendingThread: false });
273
+ logger.info(`[MessageProcessor] Feishu thread created for trigger ${triggerId}: ${threadId}`);
274
+ }
275
+ catch (e) {
276
+ logger.warn(`[MessageProcessor] Failed to write back thread_id for trigger ${triggerId}: ${e}`);
277
+ }
278
+ }
279
+ : undefined;
280
+ message.replyContext = {
281
+ ...(message.replyContext ?? {}),
282
+ replyToMessageId: message.triggerMeta.rootMessageId,
283
+ replyInThread: true,
284
+ ...(onThreadCreated ? { metadata: { ...(message.replyContext?.metadata ?? {}), onThreadCreated } } : {}),
285
+ };
286
+ }
230
287
  const channelKey = session.metadata?.channelKey || message.channel;
231
288
  const channelInfo = this.resolveChannelInfo(channelKey);
232
289
  if (!channelInfo) {
@@ -388,7 +445,7 @@ export class MessageProcessor {
388
445
  };
389
446
  };
390
447
  const isProactive = session.sessionMode === 'proactive';
391
- const isAutonomous = session.sessionMode === 'autonomous' || message.triggerMeta?.silent === true;
448
+ const isAutonomous = session.sessionMode === 'autonomous';
392
449
  const envelope = buildEnvelope({
393
450
  taskId,
394
451
  channel: message.channel,
@@ -518,12 +575,18 @@ export class MessageProcessor {
518
575
  // 检查是否因新消息自动中断 — 包装 prompt 让 Agent 知道上下文
519
576
  const prevInterruptReason = this.interruptedSessions.get(session.id);
520
577
  this.interruptedSessions.delete(session.id);
521
- const effectivePrompt = prevInterruptReason === 'new_message' && session.agentSessionId
522
- ? `【新消息插入】\n\n${message.content}\n\n【请无视之前中断继续处理】`
523
- : message.content;
578
+ const wasInterrupted = prevInterruptReason === 'new_message' && !!session.agentSessionId;
579
+ const wrapPrompt = (body) => wasInterrupted
580
+ ? `【新消息插入】\n\n${body}\n\n【请无视之前中断继续处理】`
581
+ : body;
582
+ // 先用裸文本兜底;vars 构造完成后用消息渲染层重算(见下方 effectivePrompt 重赋值)。
583
+ let effectivePrompt = wrapPrompt(message.content);
524
584
  let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
525
585
  let effectiveSystemPrompt;
526
586
  let modelOverride;
587
+ let usedFallback = false;
588
+ let skipEvolclawModel = false;
589
+ let agentModel;
527
590
  try {
528
591
  // 动态构建运行时上下文提示
529
592
  const contextParts = [];
@@ -557,16 +620,35 @@ export class MessageProcessor {
557
620
  // 按 关系级 > agent级 > 全局 解析本次调用的模型/强度,作为 per-call 入参传入 runQuery。
558
621
  // 不缓存、不绑会话——改关系级/agent级后该范围所有会话的下条消息即时生效;
559
622
  // 多对端并发各自独立解析、各自传参,无共享状态可被污染。
560
- try {
561
- const resolved = resolveEffectiveModel({ self: selfAid || undefined, peerKey });
562
- if (resolved.model)
563
- modelOverride = { model: resolved.model, effort: resolved.effort };
623
+ let effectiveModel;
624
+ // 取降级状态,按退避策略决定是否跳过 evolclaw 作用域模型
625
+ const fbState = this.modelFallbackMap.get(session.id) ?? {
626
+ failCount: 0, fallbackActive: false,
627
+ messagesSinceFallback: 0, nextProbeAt: 2, hintShown: false,
628
+ };
629
+ // 退避期内递增消息计数,判断是否到探测点
630
+ if (fbState.fallbackActive) {
631
+ fbState.messagesSinceFallback++;
632
+ skipEvolclawModel = fbState.messagesSinceFallback < fbState.nextProbeAt;
633
+ this.modelFallbackMap.set(session.id, fbState);
564
634
  }
565
- catch (e) {
566
- logger.warn(`[MessageProcessor] resolveEffectiveModel failed: ${e instanceof Error ? e.message : String(e)}`);
635
+ // 非跳过时:尝试解析 evolclaw 作用域模型
636
+ let evolclawModelOverride;
637
+ if (!skipEvolclawModel) {
638
+ try {
639
+ const resolved = resolveEffectiveModel({ self: selfAid || undefined, peerKey });
640
+ if (resolved.model) {
641
+ evolclawModelOverride = { model: resolved.model, effort: resolved.effort };
642
+ effectiveModel = resolved.model;
643
+ }
644
+ }
645
+ catch (e) {
646
+ logger.warn(`[MessageProcessor] resolveEffectiveModel failed: ${e instanceof Error ? e.message : String(e)}`);
647
+ }
648
+ modelOverride = evolclawModelOverride;
567
649
  }
568
650
  const normalizedBaseagent = normalizeBaseagent(agent.name);
569
- const agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
651
+ agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
570
652
  // Kit renderer: 组装上下文
571
653
  const pkgRoot = getPackageRoot();
572
654
  const kitCtx = {
@@ -580,6 +662,9 @@ export class MessageProcessor {
580
662
  KITS_DOCS: path.join(pkgRoot, 'kits', 'docs'),
581
663
  KITS_TEMPLATES: path.join(pkgRoot, 'kits', 'templates'),
582
664
  KITS_FRAGMENTS: path.join(pkgRoot, 'kits', 'templates', 'system-fragments'),
665
+ KITS_MESSAGE_FRAGMENTS: path.join(pkgRoot, 'kits', 'templates', 'message-fragments'),
666
+ // evolclaw 运行模式:dev=源码仓库 | install=全局安装包
667
+ evolclawMode: fs.existsSync(path.join(pkgRoot, 'src', 'index.ts')) ? 'dev' : 'install',
583
668
  // 路径变量(用于 manifest 路径展开,resolvePath 用 ctx.vars 取真值)
584
669
  PERSONAL_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'personal') : undefined,
585
670
  RELATIONS_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'relations') : undefined,
@@ -593,6 +678,9 @@ export class MessageProcessor {
593
678
  peerName: peerName || undefined,
594
679
  peerRole: session.identity?.role || 'anonymous',
595
680
  peerType: message.peerType || undefined,
681
+ sameDevice: message.sameDevice || undefined,
682
+ sameNetwork: message.sameNetwork || undefined,
683
+ sameEgressIp: message.sameEgressIp || undefined,
596
684
  groupId: session.metadata?.groupId || undefined,
597
685
  chatType: session.chatType || null,
598
686
  channel: currentChannelType || null,
@@ -606,6 +694,12 @@ export class MessageProcessor {
606
694
  sessionId: session.id,
607
695
  sessionName: session.name || undefined,
608
696
  sessionCreatedAt: session.createdAt ? new Date(session.createdAt).toISOString() : undefined,
697
+ // 时区(把 ISO 时间戳转本地时间用)+ OS 环境
698
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || undefined,
699
+ tzOffset: currentTzOffset(),
700
+ localDate: currentLocalDate(),
701
+ weekday: currentWeekday(),
702
+ osInfo: OS_INFO,
609
703
  threadId: session.threadId || undefined,
610
704
  // Stage 3: sessionKey 持久化字段
611
705
  sessionKey: session.sessionKey,
@@ -614,6 +708,9 @@ export class MessageProcessor {
614
708
  baseAgent: normalizedBaseagent.canonical,
615
709
  baseAgentName: normalizedBaseagent.displayName,
616
710
  baseAgentModel: agentModel || undefined,
711
+ effectiveModel: effectiveModel || agentModel || undefined,
712
+ modelFallbackActive: (fbState.fallbackActive || skipEvolclawModel) ? true : undefined,
713
+ modelFallbackModel: (fbState.fallbackActive || skipEvolclawModel) ? (agentModel || undefined) : undefined,
617
714
  agentSessionId: session.agentSessionId || undefined,
618
715
  },
619
716
  sessionId: session.id,
@@ -622,22 +719,67 @@ export class MessageProcessor {
622
719
  if (kitContext)
623
720
  contextParts.push(kitContext);
624
721
  effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
722
+ // 消息渲染层:用 message manifest 逐条渲染(时间 + 群聊发送者),组装成最终正文。
723
+ // 单条消息构造单元素 items;批量合并的消息 message.items 已由队列填充。
724
+ let renderResult;
725
+ const hasContent = message.content.trim() || (message.items && message.items.length > 0);
726
+ if (hasContent) {
727
+ try {
728
+ const renderItems = message.items && message.items.length > 0
729
+ ? message.items
730
+ : [{
731
+ peerId: message.peerId, peerName: peerName || undefined,
732
+ peerType: message.peerType, content: message.content,
733
+ timestamp: message.timestamp,
734
+ images: message.images,
735
+ }];
736
+ renderResult = renderMessageBody(renderItems, kitCtx.vars, session.id);
737
+ if (renderResult.body.trim())
738
+ effectivePrompt = wrapPrompt(renderResult.body);
739
+ else
740
+ effectivePrompt = wrapPrompt(message.content);
741
+ }
742
+ catch (e) {
743
+ logger.warn(`[MessageProcessor] renderMessageBody failed, using raw content: ${e instanceof Error ? e.message : String(e)}`);
744
+ effectivePrompt = wrapPrompt(message.content);
745
+ }
746
+ }
625
747
  // 可重试错误(403/429/5xx)指数退避重试,最多 3 次
626
748
  const MAX_RETRIES = 3;
627
749
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
628
750
  let streamRegistered = false;
629
751
  try {
630
752
  logger.info(`[MessageProcessor] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${attempt}/${MAX_RETRIES} agentSessionId=${session.agentSessionId ?? 'none'}`);
631
- const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager, modelOverride);
753
+ const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, renderResult?.images.length ? renderResult.images : message.images, effectiveSystemPrompt, this.sessionManager, modelOverride);
632
754
  agent.registerStream(streamKey, stream);
633
755
  streamRegistered = true;
634
756
  streamResult = await this.processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress);
757
+ // 探测成功(退避期内到达探测点且用的是 evolclaw 模型)→ 清零降级状态
758
+ if (fbState.fallbackActive && !skipEvolclawModel && !usedFallback) {
759
+ this.modelFallbackMap.delete(session.id);
760
+ logger.info(`[MessageProcessor] Model probe succeeded, cleared fallback state for session=${session.id}`);
761
+ }
635
762
  break; // 成功,跳出重试循环
636
763
  }
637
764
  catch (retryError) {
638
765
  if (streamRegistered) {
639
766
  agent.cleanupStream(streamKey);
640
767
  }
768
+ // 模型不可用:累计计数,本次切换到 baseAgentModel 立即重试,不让用户看到失败
769
+ if (classifyError(retryError) === ErrorType.MODEL_UNAVAILABLE && evolclawModelOverride?.model) {
770
+ fbState.failCount++;
771
+ if (fbState.failCount >= 2) {
772
+ fbState.fallbackActive = true;
773
+ fbState.messagesSinceFallback = 0;
774
+ fbState.nextProbeAt = Math.min(Math.pow(2, fbState.failCount - 1), 8);
775
+ }
776
+ this.modelFallbackMap.set(session.id, fbState);
777
+ logger.warn(`[MessageProcessor] Model unavailable: ${evolclawModelOverride.model}, failCount=${fbState.failCount}, fallbackActive=${fbState.fallbackActive}`);
778
+ // 切换到 baseAgentModel 重试(清除 modelOverride,让 runQuery 使用 this.model)
779
+ modelOverride = undefined;
780
+ usedFallback = true;
781
+ continue;
782
+ }
641
783
  if (attempt < MAX_RETRIES && isRetryableError(retryError)) {
642
784
  const delay = Math.pow(2, attempt) * 1000; // 2s, 4s
643
785
  logger.warn(`[MessageProcessor] Retryable error (attempt ${attempt}/${MAX_RETRIES}), retrying in ${delay}ms:`, retryError);
@@ -657,9 +799,6 @@ export class MessageProcessor {
657
799
  await renderer.flush();
658
800
  const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
659
801
  if (compacted) {
660
- // compact 成功,清除第一次流中混入的错误文本,再重试
661
- const ctxErrPattern = /prompt is too long|input is too long|上下文过长/i;
662
- renderer.stripContextError(ctxErrPattern);
663
802
  renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
664
803
  const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride);
665
804
  agent.registerStream(streamKey, retryStream);
@@ -682,7 +821,6 @@ export class MessageProcessor {
682
821
  contextTooLongPattern.test(errorsText) ||
683
822
  contextTooLongPattern.test(streamResult.fullText));
684
823
  if (isPromptTooLong) {
685
- renderer.stripContextError(contextTooLongPattern);
686
824
  renderer.addNotice('上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
687
825
  await renderer.flush();
688
826
  const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
@@ -698,7 +836,6 @@ export class MessageProcessor {
698
836
  contextTooLongPattern.test(retryErrorsText) ||
699
837
  contextTooLongPattern.test(streamResult.fullText));
700
838
  if (retryStillTooLong) {
701
- renderer.stripContextError(contextTooLongPattern);
702
839
  renderer.addNotice(getContextTooLongHint(agent), 'warn', 'context-too-long', true);
703
840
  }
704
841
  }
@@ -815,6 +952,20 @@ export class MessageProcessor {
815
952
  logger.info(`[MessageProcessor] agent.cleanupStream ok: session=${session.id} task=${taskId}`);
816
953
  this.sessionManager.clearProcessing(session.id);
817
954
  logger.info(`[MessageProcessor] session ${session.id} processing cleared task=${taskId}`);
955
+ // 降级模型回复末尾追加标记(代码层硬注入,不依赖模型输出)
956
+ const usingFallback = usedFallback || (skipEvolclawModel && agentModel != null);
957
+ if (usingFallback && agentModel) {
958
+ const curFbState = this.modelFallbackMap.get(session.id);
959
+ const showHint = curFbState && curFbState.nextProbeAt >= 8 && !curFbState.hintShown;
960
+ const suffix = showHint
961
+ ? `\n\n---\n⚠️ [降级模型: ${agentModel} | 可告诉我"帮我检查可用模型"来诊断]`
962
+ : `\n\n---\n⚠️ [降级模型: ${agentModel}]`;
963
+ renderer.addText(suffix);
964
+ if (showHint && curFbState) {
965
+ curFbState.hintShown = true;
966
+ this.modelFallbackMap.set(session.id, curFbState);
967
+ }
968
+ }
818
969
  // 被用户中断(新消息打断)时跳过 flush — 新 task 已接管渠道,旧 task 的 flush 无意义且可能卡住
819
970
  const preFlushInterrupt = this.interruptedSessions.get(session.id);
820
971
  if (preFlushInterrupt === 'new_message' || preFlushInterrupt === 'stop' || preFlushInterrupt === 'recalled') {
@@ -877,7 +1028,7 @@ export class MessageProcessor {
877
1028
  adapter.send(envelope, { kind: 'status.interrupted', metadata: { reason: interruptReason } }).catch(() => { });
878
1029
  }
879
1030
  else {
880
- adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs, ttftMs: streamResult.ttftMs, numTurns: streamResult.numTurns, usage: streamResult.usage } }).catch(() => { });
1031
+ adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs, ttftMs: streamResult.ttftMs, numTurns: streamResult.numTurns, tokenUsage: streamResult.tokenUsage, contextUsage: streamResult.contextUsage } }).catch(() => { });
881
1032
  }
882
1033
  }
883
1034
  if (message.triggerMeta) {
@@ -887,10 +1038,6 @@ export class MessageProcessor {
887
1038
  else {
888
1039
  this.eventBus.publish({ type: 'trigger:completed', triggerId: message.triggerMeta.triggerId, messageId: messageId, durationMs });
889
1040
  }
890
- // Clean up autonomous sessions after completion to avoid accumulating orphaned sessions
891
- if (session.sessionMode === 'autonomous') {
892
- this.sessionManager.unbindSession(session.id).catch(() => { });
893
- }
894
1041
  }
895
1042
  await this.sessionManager.recordSuccess(session.id);
896
1043
  this.eventBus.publish({
@@ -1023,19 +1170,21 @@ export class MessageProcessor {
1023
1170
  ? { replyContext: message.replyContext }
1024
1171
  : undefined;
1025
1172
  const projectPath = this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd();
1026
- // --session silent 触发器:新建独立 autonomous 会话,与原会话历史隔离
1027
- if (message.triggerMeta?.silent) {
1028
- const prevActive = await this.sessionManager.getActiveSession(message.channel, message.channelId);
1029
- const session = await this.sessionManager.createNewSession(message.channel, message.channelId, projectPath, `trigger-${message.triggerMeta.triggerId.slice(0, 8)}`);
1030
- await this.sessionManager.updateSession(session.id, { sessionMode: 'autonomous' });
1031
- session.sessionMode = 'autonomous';
1032
- if (prevActive) {
1033
- await this.sessionManager.switchToSession(message.channel, message.channelId, prevActive.id);
1173
+ // current strategy: resume bound session, make it active so output is not suppressed
1174
+ if (message.triggerMeta?.boundSessionId) {
1175
+ const bound = await this.sessionManager.getSessionById(message.triggerMeta.boundSessionId);
1176
+ if (bound) {
1177
+ const switched = await this.sessionManager.switchToSession(bound.channel, bound.channelId, bound.id);
1178
+ if (switched) {
1179
+ const absoluteProjectPath = path.isAbsolute(switched.projectPath)
1180
+ ? switched.projectPath : path.resolve(process.cwd(), switched.projectPath);
1181
+ return { session: switched, absoluteProjectPath };
1182
+ }
1183
+ logger.warn(`[MessageProcessor] switchToSession failed for bound session ${bound.id}, falling back to latest`);
1184
+ }
1185
+ else {
1186
+ logger.warn(`[MessageProcessor] Bound session ${message.triggerMeta.boundSessionId} not found, falling back to latest`);
1034
1187
  }
1035
- const absoluteProjectPath = path.isAbsolute(session.projectPath)
1036
- ? session.projectPath
1037
- : path.resolve(process.cwd(), session.projectPath);
1038
- return { session, absoluteProjectPath };
1039
1188
  }
1040
1189
  const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId, message.chatType, undefined, message.selfAID, message.channelType, message.peerType);
1041
1190
  // 兜底纠正1:群聊强制 proactive
@@ -1240,7 +1389,7 @@ export class MessageProcessor {
1240
1389
  logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
1241
1390
  }
1242
1391
  // 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
1243
- completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, usage: event.usage };
1392
+ completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, tokenUsage: event.tokenUsage, contextUsage: event.contextUsage };
1244
1393
  // thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
1245
1394
  // 由那里按 LLM 输出的每个 text item 单独写一条,此处不再写。
1246
1395
  // 失败且无前置错误输出:显示 errors 摘要
@@ -1296,7 +1445,7 @@ export class MessageProcessor {
1296
1445
  logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
1297
1446
  }
1298
1447
  // 记录完成状态
1299
- completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, usage: event.usage };
1448
+ completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, tokenUsage: event.tokenUsage, contextUsage: event.contextUsage };
1300
1449
  if (event.subtype === 'success') {
1301
1450
  this.messageCache.addEvent(session.id, {
1302
1451
  type: 'completed',
@@ -184,7 +184,8 @@ export class MessageQueue {
184
184
  }
185
185
  /**
186
186
  * 合并多条同 peerId 消息:
187
- * - content: \n 连接
187
+ * - content: \n 连接(兜底用,渲染层优先用 items)
188
+ * - items: 保留每条子消息(含各自 peer/timestamp),供消息渲染层逐条渲染
188
189
  * - images / mentions: 扁平合并
189
190
  * - messageId: 取最新一条的 messageId(用于 thought 锚定与中断追踪)
190
191
  * - replyContext / peerName / 其余字段: 取最后一条
@@ -193,6 +194,7 @@ export class MessageQueue {
193
194
  const contents = [];
194
195
  const allImages = [];
195
196
  const allMentions = [];
197
+ const subMessages = [];
196
198
  for (const item of items) {
197
199
  const m = item.message;
198
200
  contents.push(m.content);
@@ -200,6 +202,17 @@ export class MessageQueue {
200
202
  allImages.push(...m.images);
201
203
  if (m.mentions)
202
204
  allMentions.push(...m.mentions);
205
+ // 逐条保留发送者、时刻、图片;若该条已自带 items(罕见),展开保留细粒度
206
+ if (m.items && m.items.length > 0) {
207
+ subMessages.push(...m.items);
208
+ }
209
+ else {
210
+ subMessages.push({
211
+ peerId: m.peerId, peerName: m.peerName, peerType: m.peerType,
212
+ content: m.content, timestamp: m.timestamp,
213
+ images: m.images && m.images.length > 0 ? m.images : undefined,
214
+ });
215
+ }
203
216
  }
204
217
  const last = items[items.length - 1];
205
218
  // 保留最新一条的 messageId(若最后一条无 ID 则回退到前面已有的 ID)
@@ -213,6 +226,7 @@ export class MessageQueue {
213
226
  const merged = {
214
227
  ...last.message,
215
228
  content: contents.join('\n'),
229
+ items: subMessages,
216
230
  images: allImages.length > 0 ? allImages : undefined,
217
231
  mentions: allMentions.length > 0 ? allMentions : undefined,
218
232
  messageId: latestMessageId,
@@ -56,8 +56,10 @@ export class PeerIdentityCache {
56
56
  }
57
57
  /**
58
58
  * 从 agent.md 更新身份信息
59
+ * @param source 'agentmd'(验签通过)或 'agentmd-unverified'(内容可解析但验签未过)
60
+ * @param verifiedAt 验签通过时间戳;未验签传 0
59
61
  */
60
- static updateFromAgentMd(channelType, peerId, agentDir, agentMd, verifiedAt) {
62
+ static updateFromAgentMd(channelType, peerId, agentDir, agentMd, verifiedAt, source = 'agentmd') {
61
63
  const typeMatch = agentMd.match(/^type:\s*["']?([^"'\n]+?)["']?\s*$/m);
62
64
  const nameMatch = agentMd.match(/^name:\s*["']?(.+?)["']?\s*$/m);
63
65
  const type = typeMatch?.[1] || 'unknown';
@@ -74,13 +76,13 @@ export class PeerIdentityCache {
74
76
  agentMdUpdatedAt: now,
75
77
  verifiedAt,
76
78
  lastCheckedAt: now,
77
- source: 'agentmd',
79
+ source,
78
80
  };
79
81
  const filePath = this.getFilePath(channelType, peerId, agentDir);
80
82
  try {
81
83
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
82
84
  fs.writeFileSync(filePath, JSON.stringify(identity, null, 2), 'utf-8');
83
- logger.debug(`[PeerIdentityCache] Updated: ${channelType}#${peerId} type=${type} isAgent=${isAgent}`);
85
+ logger.debug(`[PeerIdentityCache] Updated: ${channelType}#${peerId} type=${type} isAgent=${isAgent} source=${source}`);
84
86
  }
85
87
  catch (err) {
86
88
  logger.warn(`[PeerIdentityCache] Failed to write cache: ${filePath} err=${err}`);
@@ -151,27 +153,37 @@ export class PeerIdentityCache {
151
153
  if (!content) {
152
154
  throw new Error('agent.md content unavailable');
153
155
  }
154
- // 3. 比较 hash,仅在变化时重写 peer-identity.json
156
+ // 验签通过 可信(source=agentmd);否则 type 仍解析但标记未验证
157
+ const verified = result.verification?.status === 'verified';
158
+ const source = verified ? 'agentmd' : 'agentmd-unverified';
159
+ if (!verified) {
160
+ logger.info(`[PeerIdentityCache] agent.md unverified for ${peerId}: status=${result.verification?.status ?? 'unknown'} reason=${result.verification?.reason ?? '-'} (type 仍按声明解析)`);
161
+ }
162
+ // 3. 比较 hash,内容与可信级别都未变时仅 touch
155
163
  const newHash = 'sha256:' + crypto.createHash('sha256').update(content, 'utf-8').digest('hex');
156
164
  const cached = this.get(channelType, peerId, agentDir);
157
- if (cached && cached.agentMdHash === newHash && cached.source === 'agentmd') {
165
+ if (cached && cached.agentMdHash === newHash && cached.source === source) {
158
166
  return this.touchLastChecked(channelType, peerId, agentDir, cached);
159
167
  }
160
- return this.updateFromAgentMd(channelType, peerId, agentDir, content, Date.now());
168
+ return this.updateFromAgentMd(channelType, peerId, agentDir, content, verified ? Date.now() : 0, source);
161
169
  }
162
170
  catch (err) {
163
- // 4. 网络失败,fallback 本地文件
171
+ // 4. agentmdSync 抛错(非网络失败——网络失败时它内部已 fallback 返回本地内容;
172
+ // 这里通常是无内容或解析异常)。兜底读本地文件,但无法重新验签,
173
+ // 故沿用缓存里已有的可信级别,绝不凭空升级为已验签。
164
174
  const localPath = agentMdPath(peerId);
165
175
  try {
166
176
  if (fs.existsSync(localPath)) {
167
177
  const localContent = fs.readFileSync(localPath, 'utf-8');
168
- logger.info(`[PeerIdentityCache] Network failed, using local agent.md for ${peerId}`);
169
- const localHash = 'sha256:' + crypto.createHash('sha256').update(localContent, 'utf-8').digest('hex');
170
178
  const cached = this.get(channelType, peerId, agentDir);
171
- if (cached && cached.agentMdHash === localHash && cached.source === 'agentmd') {
179
+ logger.info(`[PeerIdentityCache] Using local agent.md for ${peerId} (cached source=${cached?.source ?? 'none'})`);
180
+ const localHash = 'sha256:' + crypto.createHash('sha256').update(localContent, 'utf-8').digest('hex');
181
+ if (cached && cached.agentMdHash === localHash && (cached.source === 'agentmd' || cached.source === 'agentmd-unverified')) {
172
182
  return this.touchLastChecked(channelType, peerId, agentDir, cached);
173
183
  }
174
- return this.updateFromAgentMd(channelType, peerId, agentDir, localContent, cached?.verifiedAt ?? 0);
184
+ // 无匹配缓存可信级别 本地内容未经本次验签,标记为未验证
185
+ const fallbackSource = cached?.source === 'agentmd' ? 'agentmd' : 'agentmd-unverified';
186
+ return this.updateFromAgentMd(channelType, peerId, agentDir, localContent, cached?.verifiedAt ?? 0, fallbackSource);
175
187
  }
176
188
  }
177
189
  catch { /* ignore fs errors */ }
@@ -127,8 +127,8 @@ export function parseTriggerUpdate(args) {
127
127
  }
128
128
  if (flags.has('session')) {
129
129
  const sv = flags.get('session');
130
- if (sv !== 'latest' && sv !== 'silent')
131
- return { ok: false, error: '--session 只接受 latest 或 silent' };
130
+ if (sv !== 'latest' && sv !== 'current' && sv !== 'thread')
131
+ return { ok: false, error: '--session 只接受 latest、currentthread' };
132
132
  result.targetSessionStrategy = sv;
133
133
  }
134
134
  if (flags.has('agent')) {
@@ -216,8 +216,8 @@ export function parseTriggerSet(args) {
216
216
  let targetSessionStrategy = 'latest';
217
217
  if (hasSession) {
218
218
  const sv = flags.get('session');
219
- if (sv !== 'latest' && sv !== 'silent') {
220
- return { ok: false, error: '--session 只接受 latest 或 silent' };
219
+ if (sv !== 'latest' && sv !== 'current' && sv !== 'thread') {
220
+ return { ok: false, error: '--session 只接受 latest、currentthread' };
221
221
  }
222
222
  targetSessionStrategy = sv;
223
223
  }