evolclaw 3.0.0 → 3.1.1

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 (104) hide show
  1. package/README.md +1 -1
  2. package/bin/ec.js +29 -0
  3. package/dist/agents/baseagent-normalize.js +19 -0
  4. package/dist/agents/claude-runner.js +47 -12
  5. package/dist/agents/codex-runner.js +2 -0
  6. package/dist/agents/gemini-runner.js +9 -9
  7. package/dist/agents/kit-renderer.js +281 -0
  8. package/dist/aun/aid/identity.js +28 -0
  9. package/dist/aun/aid/index.js +1 -1
  10. package/dist/aun/aid/lifecycle-log.js +33 -0
  11. package/dist/aun/msg/group.js +3 -1
  12. package/dist/aun/msg/p2p.js +42 -1
  13. package/dist/channels/aun.js +427 -146
  14. package/dist/channels/dingtalk.js +3 -1
  15. package/dist/channels/feishu.js +128 -7
  16. package/dist/channels/qqbot.js +3 -1
  17. package/dist/channels/wechat.js +4 -1
  18. package/dist/channels/wecom.js +3 -1
  19. package/dist/cli/bench.js +1219 -0
  20. package/dist/cli/index.js +418 -40
  21. package/dist/cli/init.js +3 -4
  22. package/dist/cli/link-rules.js +245 -0
  23. package/dist/cli/net-check.js +640 -0
  24. package/dist/cli/watch-msg.js +666 -0
  25. package/dist/config-store.js +82 -5
  26. package/dist/core/channel-loader.js +23 -10
  27. package/dist/core/command-handler.js +127 -99
  28. package/dist/core/evolagent.js +5 -10
  29. package/dist/core/message/im-renderer.js +93 -48
  30. package/dist/core/message/items-formatter.js +11 -4
  31. package/dist/core/message/message-bridge.js +11 -2
  32. package/dist/core/message/message-log.js +8 -1
  33. package/dist/core/message/message-processor.js +194 -127
  34. package/dist/core/message/message-queue.js +10 -3
  35. package/dist/core/permission.js +95 -3
  36. package/dist/core/relation/peer-identity.js +161 -0
  37. package/dist/core/session/session-manager.js +103 -65
  38. package/dist/core/trigger/manager.js +16 -0
  39. package/dist/core/trigger/parser.js +110 -0
  40. package/dist/core/trigger/scheduler.js +7 -1
  41. package/dist/data/error-dict.json +118 -0
  42. package/dist/eck/baseagent-caps.js +18 -0
  43. package/dist/eck/detect.js +47 -0
  44. package/dist/eck/init.js +77 -0
  45. package/dist/eck/rules-loader.js +28 -0
  46. package/dist/index.js +186 -19
  47. package/dist/net-check.js +640 -0
  48. package/dist/paths.js +31 -40
  49. package/dist/utils/aid-lifecycle-log.js +33 -0
  50. package/dist/utils/atomic-write.js +10 -0
  51. package/dist/utils/cross-platform.js +17 -8
  52. package/dist/utils/error-utils.js +27 -15
  53. package/dist/utils/instance-registry.js +6 -5
  54. package/dist/utils/log-writer.js +2 -1
  55. package/dist/utils/logger.js +10 -0
  56. package/dist/utils/npm-ops.js +35 -3
  57. package/dist/utils/process-introspect.js +16 -38
  58. package/dist/utils/stats.js +216 -2
  59. package/dist/watch-msg.js +26 -11
  60. package/evolclaw-install-aun.md +14 -2
  61. package/kits/docs/GUIDE.md +20 -0
  62. package/kits/docs/INDEX.md +52 -0
  63. package/kits/docs/aun/CHEATSHEET.md +17 -0
  64. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
  65. package/kits/docs/channels/feishu.md +27 -0
  66. package/kits/docs/eck_templates/GUIDE.template.md +22 -0
  67. package/kits/docs/eck_templates/INDEX.template.md +28 -0
  68. package/kits/docs/eck_templates/path-registry.template.md +33 -0
  69. package/kits/docs/eck_templates/runtime.template.md +19 -0
  70. package/kits/docs/evolclaw/MSG_GROUP.md +30 -0
  71. package/kits/docs/evolclaw/MSG_PRIVATE.md +72 -0
  72. package/kits/docs/identity/AID_PROFILE_SPEC.md +27 -0
  73. package/kits/docs/identity/PATH_OPS.md +16 -0
  74. package/kits/docs/identity/ROLE_DETAIL.md +20 -0
  75. package/kits/docs/path-registry.md +43 -0
  76. package/kits/eck_manifest.json +95 -0
  77. package/kits/rules/01-overview.md +120 -0
  78. package/kits/rules/02-navigation.md +75 -0
  79. package/kits/rules/03-identity.md +34 -0
  80. package/kits/rules/04-relation.md +49 -0
  81. package/kits/rules/05-venue.md +45 -0
  82. package/kits/rules/06-channel.md +73 -0
  83. package/kits/templates/system-fragments/baseagent.md +2 -0
  84. package/kits/templates/system-fragments/channel.md +10 -0
  85. package/kits/templates/system-fragments/identity.md +12 -0
  86. package/kits/templates/system-fragments/relation.md +9 -0
  87. package/kits/templates/system-fragments/runtime.md +19 -0
  88. package/kits/templates/system-fragments/venue.md +5 -0
  89. package/package.json +7 -5
  90. package/dist/agents/templates.js +0 -122
  91. package/dist/data/prompts.md +0 -137
  92. package/kits/aun/meta.md +0 -25
  93. package/kits/aun/role.md +0 -25
  94. package/kits/templates/group.md +0 -20
  95. package/kits/templates/private.md +0 -9
  96. package/kits/templates/system-fragments/personal-context.md +0 -3
  97. package/kits/templates/system-fragments/self-intro.md +0 -5
  98. package/kits/templates/system-fragments/speaker-intro.md +0 -5
  99. package/kits/templates/system-fragments/venue-intro.md +0 -5
  100. /package/kits/{channels → docs/channels}/aun.md +0 -0
  101. /package/kits/{evolclaw/commands.md → docs/evolclaw/AGENT_CMD.md} +0 -0
  102. /package/kits/{evolclaw → docs/evolclaw}/self-summary.md +0 -0
  103. /package/kits/{evolclaw → docs/evolclaw}/tools.md +0 -0
  104. /package/kits/{evolclaw → docs/identity}/identity-tools.md +0 -0
@@ -10,7 +10,8 @@ import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError,
10
10
  import { summarizeToolInput } from '../permission.js';
11
11
  import { DEFAULT_PERMISSION_MODE } from '../../types.js';
12
12
  import { getPackageRoot, resolveRoot } from '../../paths.js';
13
- import { renderPromptSection } from '../../agents/templates.js';
13
+ import { renderKitSections } from '../../agents/kit-renderer.js';
14
+ import { normalizeBaseagent } from '../../agents/baseagent-normalize.js';
14
15
  import { renderActionAsText, renderCommandCardAsText } from '../interaction-router.js';
15
16
  /**
16
17
  * 构造 OutboundEnvelope —— 出站三件套的信封部分。
@@ -124,7 +125,8 @@ export class MessageProcessor {
124
125
  const agent = this.agentRegistry.resolveByChannel(channelName);
125
126
  if (!agent)
126
127
  return null;
127
- const globalCm = this.agentRegistry?.resolveByChannel(channelName)?.config?.chatmode;
128
+ // chatmode 解析优先级:agent.config.chatmode > globalSettings.chatmode
129
+ const globalCm = agent.config?.chatmode ?? this.globalSettings.chatmode;
128
130
  return agent.getContext(channelName, chatType, globalCm);
129
131
  }
130
132
  /**
@@ -182,7 +184,7 @@ export class MessageProcessor {
182
184
  '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
183
185
  '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check',
184
186
  '/p ', '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/chatmode',
185
- '/aid', '/agentmd',
187
+ '/aid', '/agentmd', '/upgrade',
186
188
  ];
187
189
  /** 判断消息内容是否为已知命令 */
188
190
  isKnownCommand(content) {
@@ -345,6 +347,8 @@ export class MessageProcessor {
345
347
  // 为本次任务处理生成唯一 task_id(客户端生成,格式 task-{10hex})
346
348
  const taskId = `task-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`;
347
349
  const chatmode = session.sessionMode ?? 'interactive';
350
+ // 诊断日志:记录 inbound message_id 和生成的 task_id 的对应关系
351
+ logger.info(`[MessageProcessor] Task created: inboundMsgId=${message.messageId ?? 'none'} taskId=${taskId} sessionId=${session.id} chatmode=${chatmode}`);
348
352
  // 构建带 taskId/chatmode 的 ReplyContext(本次任务所有出站消息共用)
349
353
  const taskReplyContext = () => {
350
354
  const base = this.getReplyContext(message);
@@ -392,7 +396,8 @@ export class MessageProcessor {
392
396
  const peerLabel = peerName && peerName !== peerShort ? `${peerShort}(${peerName})` : peerShort;
393
397
  logger.info(`[MessageProcessor] session=${session.id} task=${taskId} peer=${peerLabel} chatType=${session.chatType} sessionMode=${session.sessionMode} agentId=${session.agentId} msgChatType=${message.chatType ?? 'n/a'}`);
394
398
  // 记录开始处理
395
- this.eventBus.publish({ type: 'task:started', sessionId: session.id });
399
+ const taskEncrypt = message.replyContext?.metadata?.encrypted != null ? !!(message.replyContext.metadata.encrypted) : undefined;
400
+ this.eventBus.publish({ type: 'task:started', sessionId: session.id, agentName: agentNameForStats, encrypt: taskEncrypt, chatmode: session.sessionMode || 'interactive' });
396
401
  // 触发器消息不发 processing status(无需通知用户)
397
402
  if (message.source !== 'trigger') {
398
403
  adapter.send(envelope, { kind: 'status.started' }).catch(() => { });
@@ -416,6 +421,10 @@ export class MessageProcessor {
416
421
  send: async (payload) => {
417
422
  if (isAutonomous)
418
423
  return; // autonomous session: never send to channel
424
+ // proactive 模式:activity.batch 是 thought 协议内容,只发给支持 thought 的 channel
425
+ // (不支持 thought 的 channel 静默丢弃,避免降级为普通消息)
426
+ if (isProactive && payload.kind === 'activity.batch' && !adapter.capabilities?.thought)
427
+ return;
419
428
  const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
420
429
  if (isCurrentlyBackground)
421
430
  return;
@@ -431,7 +440,7 @@ export class MessageProcessor {
431
440
  }
432
441
  }
433
442
  if (payload.kind === 'result.text' && payload.isFinal) {
434
- opts.title = '\u2713 \u6700\u7ec8\u56de\u590d:';
443
+ opts.title = '\u2705 \u6700\u7ec8\u56de\u590d:';
435
444
  }
436
445
  opts.metadata = { ...(opts.metadata ?? {}), taskId, chatmode };
437
446
  const enrichedEnvelope = { ...envelope, replyContext: opts };
@@ -488,32 +497,15 @@ export class MessageProcessor {
488
497
  // 动态构建运行时上下文提示
489
498
  const contextParts = [];
490
499
  const currentChannelType = options?.channelType || message.channel;
491
- // 1. 构建模板变量并渲染 runtime 段
492
- const peerName = message.peerName || session.metadata?.peerName;
493
- const peerType = message.peerType;
494
- const peerId = message.peerId;
500
+ // 提取 self 信息
495
501
  const adapterAny = channelInfo.adapter;
496
502
  const selfAid = typeof adapterAny._selfAid === 'function' ? adapterAny._selfAid() : undefined;
497
503
  const selfName = typeof adapterAny._selfName === 'function' ? adapterAny._selfName() : undefined;
498
- const formatIdentity = (name, id) => {
499
- if (name && id)
500
- return `${name} (${id})`;
501
- return name || id || undefined;
502
- };
503
- const selfIdentity = formatIdentity(selfName, selfAid);
504
- const peerIdentity = formatIdentity(peerName, peerId);
505
- // 文件发送能力(按 channelType 去重)
506
- let crossChannelTypes = [];
504
+ const peerName = message.peerName || session.metadata?.peerName;
505
+ // 文件发送能力
507
506
  let currentCanSend = false;
508
507
  if (!isProactive) {
509
- const fileChannelTypes = new Set();
510
508
  currentCanSend = !!(channelInfo.adapter.capabilities?.file);
511
- for (const [, info] of this.channels) {
512
- if (info.adapter.capabilities?.file) {
513
- fileChannelTypes.add(info.options?.channelType || info.adapter.channelName);
514
- }
515
- }
516
- crossChannelTypes = [...fileChannelTypes].filter(t => t !== currentChannelType);
517
509
  }
518
510
  // 通道能力
519
511
  const capParts = [];
@@ -523,49 +515,53 @@ export class MessageProcessor {
523
515
  capParts.push('图片输出');
524
516
  if (channelInfo.adapter.capabilities?.file)
525
517
  capParts.push('文件发送');
526
- // Personal layer: persona.md + working memory 注入
518
+ // Personal layer
527
519
  const owningAgent = this.agentRegistry?.resolveByChannel(channelKey);
528
- if (owningAgent) {
529
- const persona = owningAgent.getPersona?.();
530
- if (persona)
531
- contextParts.push(persona);
532
- const working = owningAgent.getWorkingMemory?.();
533
- if (working)
534
- contextParts.push(`[当前关注]\n${working}`);
535
- }
536
- contextParts.push(renderPromptSection('runtime', {
537
- channel: currentChannelType,
538
- project: path.basename(absoluteProjectPath),
539
- sessionName: session.name || '',
540
- selfIdentity: selfIdentity || '',
541
- peerRole: session.identity?.role || 'unknown',
542
- peerIdentity: peerIdentity || '',
543
- peerType: peerType && peerType !== 'unknown' ? peerType : '',
544
- chatType: session.chatType || '',
545
- agent: session.agentId && session.agentId !== 'claude' ? session.agentId : '',
546
- readonly: session.metadata?.permissionMode === 'readonly',
547
- readonlySendHint: isProactive ? '使用 evolclaw ctl file 发送' : '使用 [SEND_FILE:] 发送',
548
- fileSendCurrent: !isProactive && currentCanSend,
549
- fileSendCross: !isProactive && crossChannelTypes.length > 0,
550
- crossPrimary: crossChannelTypes[0] || '',
551
- crossTypes: crossChannelTypes.join('/'),
552
- capability: capParts.length > 0,
553
- capabilities: capParts.join(''),
554
- }));
555
- // 2. 群聊 @ 规则
556
- if (message.chatType === 'group' && message.peerId) {
557
- contextParts.push(renderPromptSection('group', { peerId: message.peerId }));
558
- }
559
- // 3. Proactive 模式提示词
560
- if (isProactive) {
561
- contextParts.push(renderPromptSection('proactive', {}));
562
- }
563
- // 4. 触发器功能提示词(非触发器消息时注入,让 AI 知道可以使用触发器)
564
- if (message.source !== 'trigger') {
565
- const triggerSection = renderPromptSection('trigger', {});
566
- if (triggerSection)
567
- contextParts.push(triggerSection);
568
- }
520
+ const persona = owningAgent?.getPersona?.() || undefined;
521
+ const working = owningAgent?.getWorkingMemory?.() || undefined;
522
+ if (persona)
523
+ contextParts.push(persona);
524
+ if (working)
525
+ contextParts.push(`[当前关注]\n${working}`);
526
+ // 计算 peerKey: <channel>#<urlEncode(peerId)>
527
+ const peerIdRaw = message.peerId;
528
+ const peerKey = (currentChannelType && peerIdRaw)
529
+ ? `${currentChannelType}#${encodeURIComponent(peerIdRaw)}`
530
+ : undefined;
531
+ const normalizedBaseagent = normalizeBaseagent(agent.name);
532
+ // Kit renderer: 组装上下文
533
+ const kitCtx = {
534
+ vars: {
535
+ EVOLCLAW_HOME: resolveRoot(),
536
+ PACKAGE_ROOT: getPackageRoot(),
537
+ CURRENT_PROJECT: absoluteProjectPath,
538
+ selfAid: selfAid || undefined,
539
+ selfName: selfName || undefined,
540
+ hasPersona: !!persona,
541
+ hasWorkingMemory: !!working,
542
+ peerId: peerIdRaw || undefined,
543
+ peerKey,
544
+ peerName: peerName || undefined,
545
+ peerRole: session.identity?.role || 'unknown',
546
+ groupId: session.metadata?.groupId || undefined,
547
+ scene: session.chatType ? (session.chatType === 'group' ? 'group' : 'private') : 'coding',
548
+ chatType: session.chatType || null,
549
+ channel: currentChannelType || null,
550
+ venueUid: undefined,
551
+ project: path.basename(absoluteProjectPath),
552
+ sessionName: session.name || undefined,
553
+ sessionMode: isProactive ? 'proactive' : 'interactive',
554
+ readonly: session.metadata?.permissionMode === 'readonly',
555
+ canSendFile: !isProactive && currentCanSend,
556
+ capabilities: capParts.length > 0 ? capParts.join('') : undefined,
557
+ baseAgent: normalizedBaseagent.canonical,
558
+ baseAgentName: normalizedBaseagent.displayName,
559
+ },
560
+ sessionId: session.id,
561
+ };
562
+ const kitContext = renderKitSections(kitCtx);
563
+ if (kitContext)
564
+ contextParts.push(kitContext);
569
565
  effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
570
566
  // 可重试错误(403/429/5xx)指数退避重试,最多 3 次
571
567
  const MAX_RETRIES = 3;
@@ -586,7 +582,7 @@ export class MessageProcessor {
586
582
  if (attempt < MAX_RETRIES && isRetryableError(retryError)) {
587
583
  const delay = Math.pow(2, attempt) * 1000; // 2s, 4s
588
584
  logger.warn(`[MessageProcessor] Retryable error (attempt ${attempt}/${MAX_RETRIES}), retrying in ${delay}ms:`, retryError);
589
- renderer.addNotice(`⚠️ API 暂时不可用,${delay / 1000}秒后重试 (${attempt}/${MAX_RETRIES})...`, 'warn', 'retry', true);
585
+ renderer.addNotice(`API 暂时不可用,${delay / 1000}秒后重试 (${attempt}/${MAX_RETRIES})...`, 'warn', 'retry', true);
590
586
  await renderer.flush();
591
587
  await new Promise(resolve => setTimeout(resolve, delay));
592
588
  continue;
@@ -598,12 +594,12 @@ export class MessageProcessor {
598
594
  catch (error) {
599
595
  if (classifyError(error) === ErrorType.CONTEXT_TOO_LONG && session.agentSessionId && hasCompact(agent)) {
600
596
  // 尝试 compact 压缩会话
601
- renderer.addNotice('\u26a0\ufe0f 上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
597
+ renderer.addNotice('上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
602
598
  await renderer.flush();
603
599
  const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
604
600
  if (compacted) {
605
601
  // compact 成功,带 resume 重试(不重复原始消息,让 Agent 继续未完成的工作)
606
- renderer.addNotice('\u2705 压缩完成,正在重试...', 'info', 'compact-retry', true);
602
+ renderer.addNotice(' 压缩完成,继续处理...', 'info', 'compact-retry', true);
607
603
  const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
608
604
  agent.registerStream(streamKey, retryStream);
609
605
  streamResult = await this.processEventStream(retryStream, session, renderer, resetTimer, shouldSuppress);
@@ -616,6 +612,35 @@ export class MessageProcessor {
616
612
  throw error;
617
613
  }
618
614
  }
615
+ // prompt_too_long:SDK 以 complete 事件(非异常)返回,需在此处触发 compact
616
+ // 检测条件:terminalReason 明确为 prompt_too_long,或文本/errors 包含相关错误文本
617
+ const contextTooLongPattern = /prompt is too long|input is too long|上下文过长/i;
618
+ const errorsText = streamResult.errors?.join(' ') || '';
619
+ const isPromptTooLong = streamResult.isError && session.agentSessionId && hasCompact(agent) && (streamResult.terminalReason === 'prompt_too_long' ||
620
+ contextTooLongPattern.test(streamResult.lastReplyText) ||
621
+ contextTooLongPattern.test(errorsText) ||
622
+ contextTooLongPattern.test(streamResult.fullText));
623
+ if (isPromptTooLong) {
624
+ renderer.addNotice('上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
625
+ await renderer.flush();
626
+ const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
627
+ if (compacted) {
628
+ renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
629
+ const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
630
+ agent.registerStream(streamKey, retryStream);
631
+ streamResult = await this.processEventStream(retryStream, session, renderer, resetTimer, shouldSuppress);
632
+ }
633
+ else {
634
+ throw new Error('CONTEXT_COMPACT_FAILED');
635
+ }
636
+ }
637
+ else if (streamResult.isError && !isPromptTooLong && (streamResult.terminalReason === 'prompt_too_long' ||
638
+ contextTooLongPattern.test(streamResult.lastReplyText) ||
639
+ contextTooLongPattern.test(errorsText) ||
640
+ contextTooLongPattern.test(streamResult.fullText))) {
641
+ // 上下文过长但无法 auto-compact(无 session ID 或 agent 不支持),显示友好提示
642
+ renderer.addNotice('上下文过长,请精简提问或使用 /compact 压缩上下文', 'warn', 'context-too-long', true);
643
+ }
619
644
  // 处理文件标记 - 支持 [SEND_FILE:path] 和 [SEND_FILE:channel:path]
620
645
  // 注意:始终扫描全部文本(含中间轮),因为文件标记可能出现在任意轮次
621
646
  // suppressed 模式下 renderer 只有最后一轮文本,需要用 streamResult.fullText(SDK 全文)兜底
@@ -698,43 +723,35 @@ export class MessageProcessor {
698
723
  }
699
724
  }
700
725
  } // end of !isProactive
701
- // 最终回复文本添加到 renderer(统一在流结束后处理,避免多 complete 事件重复发送)
702
- // suppressed 模式:中间流式文本未推送,使用最后一轮回复(回退到全文)
703
- // 非 suppressed 且无流式文本:同上
704
- // 非 suppressed 且有流式文本:已经逐步推送过了,不重复添加
705
- // 但如果 renderer 既未发送过内容也没有 pending 内容(如 text 事件全为空),仍需兜底
726
+ // 最终回复文本:suppressed 模式或无 text 事件时需要兜底添加
706
727
  const finalReplyText = streamResult.lastReplyText || streamResult.fullText;
707
- // 识别 Claude SDK 本地预处理兜底(如 "Unknown skill: xxx"):
708
- // 特征:无流式 text + complete.result 匹配已知模式
709
- // 这类输出不是 agent 的回复意图,而是 SDK 本地拦截到的"未知斜杠命令"提示。
710
- // Proactive 模式下 renderer silent,需要兜底发出以告知用户,否则用户完全无反馈。
711
- const isSdkFallbackMessage = !!finalReplyText
712
- && !streamResult.hasReceivedText
713
- && /^Unknown skill:\s+\S+/i.test(finalReplyText.trim());
714
728
  if (finalReplyText) {
715
- if (isProactive && isSdkFallbackMessage) {
716
- // Proactive 模式 + SDK 本地兜底:直接 sendText 绕过 silent renderer
729
+ if (isProactive && !streamResult.hasReceivedText && /^Unknown skill:\s+\S+/i.test(finalReplyText.trim())) {
730
+ // Proactive 模式 + SDK 本地兜底:直接发送绕过 silent renderer
717
731
  const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
718
732
  if (!isCurrentlyBackground) {
719
733
  await adapter.send({ ...envelope, replyContext: capturedReplyContext }, { kind: 'result.text', text: finalReplyText, isFinal: true });
720
734
  logger.info(`[MessageProcessor] proactive SDK fallback replied task=${taskId} text="${finalReplyText.slice(0, 60)}"`);
721
735
  }
722
736
  }
723
- else if (shouldSuppress()) {
724
- renderer.addText(finalReplyText);
725
- }
726
- else if (!streamResult.hasReceivedText || (!renderer.hasSentContent() && !renderer.hasContent())) {
737
+ else if (shouldSuppress() || !streamResult.hasReceivedText) {
727
738
  renderer.addText(finalReplyText);
728
739
  }
729
740
  }
730
- // Flush 剩余内容(文件标记已在 flush 时自动移除)
731
- await renderer.flush(true);
732
- // 清理 activeStreams(正常完成)
741
+ // 先清理流和处理中状态(保证即使 flush 卡住,session 也不会永久处于"处理中")
733
742
  agent.cleanupStream(streamKey);
734
743
  logger.info(`[MessageProcessor] agent.cleanupStream ok: session=${session.id} task=${taskId}`);
735
- // 清除处理中状态
736
744
  this.sessionManager.clearProcessing(session.id);
737
745
  logger.info(`[MessageProcessor] session ${session.id} processing cleared task=${taskId}`);
746
+ // 被用户中断(新消息打断)时跳过 flush — 新 task 已接管渠道,旧 task 的 flush 无意义且可能卡住
747
+ const preFlushInterrupt = this.interruptedSessions.get(session.id);
748
+ if (preFlushInterrupt === 'new_message' || preFlushInterrupt === 'stop' || preFlushInterrupt === 'recalled') {
749
+ logger.info(`[MessageProcessor] Skipping flush for interrupted task=${taskId} reason=${preFlushInterrupt}`);
750
+ }
751
+ else {
752
+ // Flush 剩余内容(文件标记已在 flush 时自动移除)
753
+ await renderer.flush(true);
754
+ }
738
755
  // 更新 EvolAgent.lastActivity
739
756
  if (this.agentRegistry) {
740
757
  const owningAgent = this.agentRegistry.resolveByChannel(channelKey);
@@ -788,7 +805,7 @@ export class MessageProcessor {
788
805
  adapter.send(envelope, { kind: 'status.interrupted', metadata: { reason: interruptReason } }).catch(() => { });
789
806
  }
790
807
  else {
791
- adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs } }).catch(() => { });
808
+ adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs, numTurns: streamResult.numTurns, usage: streamResult.usage } }).catch(() => { });
792
809
  }
793
810
  }
794
811
  if (message.triggerMeta) {
@@ -813,6 +830,7 @@ export class MessageProcessor {
813
830
  finalText: streamResult.lastReplyText || undefined,
814
831
  durationMs: Date.now() - startTime,
815
832
  agentName: agentNameForStats,
833
+ numTurns: streamResult.numTurns,
816
834
  timestamp: Date.now()
817
835
  });
818
836
  // 记录处理完成
@@ -823,22 +841,8 @@ export class MessageProcessor {
823
841
  status: 'completed',
824
842
  duration: Date.now() - startTime
825
843
  });
826
- // 写入消息记录(出方向)
827
- if (streamResult.lastReplyText || streamResult.fullText) {
828
- const chatDir = this.sessionManager.getChatDir(session);
829
- appendMessageLog(chatDir, buildOutboundEntry({
830
- from: message.selfId || session.selfId || 'self',
831
- to: message.peerId || message.channelId,
832
- chatType: (message.chatType || session.chatType || 'private'),
833
- groupId: session.metadata?.groupId ?? null,
834
- msgId: `${messageId}_reply`,
835
- content: streamResult.lastReplyText || streamResult.fullText,
836
- replyTo: message.messageId ?? null,
837
- agent: session.agentId || null,
838
- model: agent.getModel?.() || null,
839
- durationMs: Date.now() - startTime,
840
- }));
841
- }
844
+ // 写入消息记录(出方向)已下沉到 aun.ts:deliverTextEntry,
845
+ // 所有 message.send 成功后统一写入 messages.jsonl,此处不再重复写入。
842
846
  }
843
847
  const isFinallyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
844
848
  if (isFinallyBackground && session.sessionMode !== 'autonomous') {
@@ -961,7 +965,20 @@ export class MessageProcessor {
961
965
  : path.resolve(process.cwd(), session.projectPath);
962
966
  return { session, absoluteProjectPath };
963
967
  }
964
- const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId);
968
+ const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId, undefined, undefined, undefined, undefined, message.peerType);
969
+ // 兜底纠正1:群聊强制 proactive
970
+ if (message.chatType === 'group' && session.sessionMode !== 'proactive') {
971
+ logger.info(`[MessageProcessor] group proactive upgrade: sessionId=${session.id} ${session.sessionMode} -> proactive`);
972
+ session.sessionMode = 'proactive';
973
+ await this.sessionManager.updateSession(session.id, { sessionMode: 'proactive' });
974
+ }
975
+ // 兜底纠正2:旧 session 创建时没传 peerType(建为 interactive),后续非 human 消息进来时升级为 proactive。
976
+ // 新建场景已由 getOrCreateSession 内部 resolveDefaultSessionMode 处理,这里只兜底历史会话。
977
+ if (message.peerType && message.peerType !== 'human' && message.peerType !== 'unknown' && session.sessionMode !== 'proactive') {
978
+ logger.info(`[MessageProcessor] proactive upgrade: sessionId=${session.id} ${session.sessionMode} -> proactive (peerType=${message.peerType})`);
979
+ session.sessionMode = 'proactive';
980
+ await this.sessionManager.updateSession(session.id, { sessionMode: 'proactive' });
981
+ }
965
982
  // replyContext 不再写入 session.metadata(跟着 message 走,避免群聊多人覆盖)
966
983
  const absoluteProjectPath = path.isAbsolute(session.projectPath)
967
984
  ? session.projectPath
@@ -982,6 +999,8 @@ export class MessageProcessor {
982
999
  let completeResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
983
1000
  // 追踪最后一轮 assistant 回复文本(tool_use 之后的纯文本)
984
1001
  let lastReplyText = '';
1002
+ // callId → description 映射,用于 tool_result 回显描述
1003
+ const toolDescByCallId = new Map();
985
1004
  try {
986
1005
  for await (const event of stream) {
987
1006
  // 每收到事件重置空闲超时
@@ -1049,7 +1068,7 @@ export class MessageProcessor {
1049
1068
  lastReplyText += event.text;
1050
1069
  this.eventBus.publish({ type: 'message:text', sessionId: session.id, text: event.text, isFinal: false });
1051
1070
  if (!shouldSuppress()) {
1052
- renderer.addText(event.text);
1071
+ renderer.addText(event.text, event.outputTokens, event.turn);
1053
1072
  }
1054
1073
  }
1055
1074
  // compact 完成
@@ -1073,7 +1092,11 @@ export class MessageProcessor {
1073
1092
  }
1074
1093
  // 工具调用
1075
1094
  if (event.type === 'tool_use') {
1076
- // 工具调用意味着当前文本是中间轮,重置最后回复追踪
1095
+ // 工具调用意味着当前 turn 结束,flush 已累积的文本作为独立消息
1096
+ if (renderer.hasTextPending()) {
1097
+ await renderer.flushText();
1098
+ }
1099
+ // 重置最后回复追踪
1077
1100
  lastReplyText = '';
1078
1101
  this.eventBus.publish({
1079
1102
  type: 'tool:use',
@@ -1084,7 +1107,10 @@ export class MessageProcessor {
1084
1107
  });
1085
1108
  if (!shouldSuppress()) {
1086
1109
  const desc = summarizeToolInput(event.name, event.input || {});
1087
- renderer.addToolCall(event.name, event.input, event.callId, desc);
1110
+ if (event.callId) {
1111
+ toolDescByCallId.set(event.callId, desc);
1112
+ }
1113
+ renderer.addToolCall(event.name, event.input, event.callId, desc, event.turn, event.outputTokens);
1088
1114
  }
1089
1115
  }
1090
1116
  // 工具结果
@@ -1098,47 +1124,80 @@ export class MessageProcessor {
1098
1124
  agentName: agentNameForStats,
1099
1125
  timestamp: Date.now()
1100
1126
  });
1127
+ // 从 tool_use 阶段缓存的描述中回溯
1128
+ const cachedDesc = event.callId ? toolDescByCallId.get(event.callId) : undefined;
1101
1129
  if (event.isError && !shouldSuppress()) {
1102
1130
  hasErrorResult = true;
1103
1131
  let errorMsg = event.error || (typeof event.result === 'string' ? event.result : JSON.stringify(event.result)) || '\u6267\u884c\u5931\u8d25';
1104
1132
  // 移除 XML 风格的错误标签
1105
1133
  errorMsg = errorMsg.replace(/<tool_use_error>(.*?)<\/tool_use_error>/gs, '$1');
1106
- renderer.addToolResult(event.name || '\u5de5\u5177', false, undefined, errorMsg, event.callId);
1134
+ renderer.addToolResult(event.name || '\u5de5\u5177', false, undefined, errorMsg, event.callId, undefined, cachedDesc);
1107
1135
  }
1108
1136
  else if (!event.isError && !shouldSuppress()) {
1109
- renderer.addToolResult(event.name || '\u5de5\u5177', true, event.result, undefined, event.callId);
1137
+ renderer.addToolResult(event.name || '\u5de5\u5177', true, event.result, undefined, event.callId, undefined, cachedDesc);
1110
1138
  }
1111
1139
  }
1112
1140
  // 运行时错误(Codex: turn.failed / item error)
1113
1141
  if (event.type === 'error') {
1114
1142
  logger.warn(`[MessageProcessor] error event: ${event.errorType}: ${event.error}`);
1115
- if (!hasErrorResult && !shouldSuppress()) {
1143
+ // 记录错误文本到 lastReplyText,供后续 isPromptTooLong 检测
1144
+ lastReplyText += event.error || '';
1145
+ // 上下文过长的错误不在此处输出 notice,留给外层 isPromptTooLong 触发 auto-compact
1146
+ const isContextError = /prompt is too long|input is too long|上下文过长/i.test(event.error || '');
1147
+ if (!isContextError && !hasErrorResult && !shouldSuppress()) {
1116
1148
  hasErrorResult = true;
1117
- renderer.addNotice(`\u274c ${event.error}`, 'warn', 'runtime-error', true);
1149
+ renderer.addNotice(`${event.error}`, 'warn', 'runtime-error', true);
1118
1150
  }
1119
1151
  }
1120
1152
  // 完成事件
1121
1153
  // SDK 可能产生多个 complete 事件(如 subagent 或 auto-compact 二次查询),
1122
1154
  // 仅记录状态,最终 flush(true) 在流结束后统一执行
1123
1155
  if (event.type === 'complete') {
1124
- logger.debug(`[MessageProcessor] complete event: hasReceivedText=${hasReceivedText}, isError=${event.isError}, shouldSuppress=${shouldSuppress()}`);
1156
+ logger.info(`[MessageProcessor] complete event: isError=${event.isError} terminalReason=${event.terminalReason ?? 'none'} subtype=${event.subtype ?? 'none'} hasReceivedText=${hasReceivedText}`);
1125
1157
  // 自动回填会话名称
1126
1158
  if (event.sessionTitle && session.name === '默认会话') {
1127
1159
  await this.sessionManager.renameSession(session.id, event.sessionTitle);
1128
1160
  logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
1129
1161
  }
1130
1162
  // 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
1131
- completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText };
1163
+ completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, usage: event.usage };
1164
+ // proactive 模式:每轮 LLM 调用完成后写一条 thought 到 messages.jsonl
1165
+ // 这样 thought 数 = LLM 调用轮数,而不是 chunk 数
1166
+ if (session.sessionMode === 'proactive' && lastReplyText) {
1167
+ try {
1168
+ const chatDir = this.sessionManager.getChatDir(session);
1169
+ const sessionEncrypt = this.sessionManager.getSessionEncrypt(session.id);
1170
+ appendMessageLog(chatDir, buildOutboundEntry({
1171
+ from: session.selfId || 'self',
1172
+ to: session.metadata?.peerId ?? session.channelId,
1173
+ chatType: (session.chatType ?? 'private'),
1174
+ groupId: session.metadata?.groupId ?? null,
1175
+ msgId: `thought-${session.id}-${Date.now()}`,
1176
+ content: lastReplyText,
1177
+ agent: session.agentId || null,
1178
+ model: null,
1179
+ durationMs: null,
1180
+ encrypt: sessionEncrypt ?? undefined,
1181
+ chatmode: 'proactive',
1182
+ msgType: 'thought',
1183
+ }));
1184
+ }
1185
+ catch { }
1186
+ }
1132
1187
  // 失败且无前置错误输出:显示 errors 摘要
1133
1188
  // 但用户主动中断(新消息打断 或 /stop 命令)时不显示错误提示
1189
+ // 上下文过长的错误留给外层 isPromptTooLong 触发 auto-compact,不在此处输出
1134
1190
  const interruptReason = this.interruptedSessions.get(session.id);
1135
1191
  const isUserInterrupt = interruptReason === 'new_message' || interruptReason === 'stop' || interruptReason === 'recalled';
1136
- if (event.isError && !hasErrorResult && !shouldSuppress() && !isUserInterrupt) {
1137
- const errorSummary = event.errors?.join('; ') || '\u4efb\u52a1\u6267\u884c\u5931\u8d25';
1138
- // 使用 terminalReason 提供更友好的错误提示
1192
+ const isContextTooLong = event.terminalReason === 'prompt_too_long'
1193
+ || /prompt is too long|input is too long|上下文过长/i.test(event.errors?.join(' ') || '')
1194
+ || /prompt is too long|input is too long|上下文过长/i.test(lastReplyText);
1195
+ if (event.isError && !hasErrorResult && !shouldSuppress() && !isUserInterrupt && !isContextTooLong) {
1196
+ const errorSummary = event.errors?.join('; ') || '任务执行失败';
1197
+ // 使用 terminalReason 提供更友好的错误提示(不带 emoji,由 formatter 统一加)
1139
1198
  const userFriendlyMessage = event.terminalReason
1140
- ? getErrorMessage(null, event.terminalReason)
1141
- : `\u274c ${errorSummary}`;
1199
+ ? getErrorMessage(null, event.terminalReason, false)
1200
+ : errorSummary;
1142
1201
  renderer.addNotice(userFriendlyMessage, 'warn', 'task-error', true);
1143
1202
  }
1144
1203
  // 中间 complete:flush 掉已有 activities(不带 isFinal),让中间结果及时显示
@@ -1165,7 +1224,7 @@ export class MessageProcessor {
1165
1224
  logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
1166
1225
  }
1167
1226
  // 记录完成状态
1168
- completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText };
1227
+ completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, usage: event.usage };
1169
1228
  if (event.subtype === 'success') {
1170
1229
  this.messageCache.addEvent(session.id, {
1171
1230
  type: 'completed',
@@ -1185,6 +1244,7 @@ export class MessageProcessor {
1185
1244
  finalText: lastReplyText || event.result || undefined,
1186
1245
  durationMs: event.durationMs,
1187
1246
  agentName: agentNameForStats,
1247
+ numTurns: event.numTurns,
1188
1248
  timestamp: Date.now()
1189
1249
  });
1190
1250
  }
@@ -1212,9 +1272,16 @@ export class MessageProcessor {
1212
1272
  catch (error) {
1213
1273
  // User interrupt (AbortError) is expected, log at info level
1214
1274
  const catchInterruptReason = this.interruptedSessions.get(session.id);
1215
- const catchIsUserInterrupt = catchInterruptReason === 'new_message' || catchInterruptReason === 'stop';
1275
+ const catchIsUserInterrupt = catchInterruptReason === 'new_message' || catchInterruptReason === 'stop' || catchInterruptReason === 'recalled';
1216
1276
  if (error instanceof Error && error.name === 'AbortError') {
1217
1277
  logger.info('[MessageProcessor] Stream interrupted (AbortError)');
1278
+ // User-initiated interrupt: skip flush — new task takes over the channel,
1279
+ // flushing here would send a spurious "最终回复" before the new task's output
1280
+ if (catchIsUserInterrupt) {
1281
+ completeResult.isError = false;
1282
+ completeResult.hasReceivedText = hasReceivedText;
1283
+ return completeResult;
1284
+ }
1218
1285
  }
1219
1286
  else if (catchIsUserInterrupt) {
1220
1287
  // SDK telemetry noise after user-initiated interrupt — not a real error
@@ -1231,7 +1298,7 @@ export class MessageProcessor {
1231
1298
  logger.error('[MessageProcessor] Stream processing error:', error);
1232
1299
  }
1233
1300
  if (error instanceof Error && error.message.includes('process exited')) {
1234
- renderer.addNotice('\u274c Claude Code \u8fdb\u7a0b\u5f02\u5e38\u9000\u51fa\uff0c\u8bf7\u91cd\u8bd5', 'warn', 'process-exit', true);
1301
+ renderer.addNotice('Claude Code 进程异常退出,请重试', 'warn', 'process-exit', true);
1235
1302
  }
1236
1303
  // Flush any pending error activities before re-throwing,
1237
1304
  // and mark the error so outer catch won't send a duplicate message
@@ -80,7 +80,8 @@ export class MessageQueue {
80
80
  }
81
81
  const queueKey = this.getQueueKey(sessionKey, projectPath);
82
82
  const agentName = options?.agentName || DEFAULT_AGENT_NAME;
83
- logger.debug(`[Queue] Enqueuing message for ${queueKey} (agent=${agentName})`);
83
+ const isProcessing = this.processing.has(queueKey);
84
+ logger.info(`[Queue] enqueue: key=${queueKey} processing=${isProcessing} queueLen=${this.queues.get(queueKey)?.length ?? 0} agent=${agentName}`);
84
85
  return new Promise((resolve, reject) => {
85
86
  if (!this.queues.has(queueKey)) {
86
87
  this.queues.set(queueKey, []);
@@ -104,6 +105,12 @@ export class MessageQueue {
104
105
  else {
105
106
  // 群聊:FIFO,不打断
106
107
  logger.debug(`[Queue] ${queueKey} is processing, message queued (FIFO)`);
108
+ this.eventBus?.publish({
109
+ type: 'task:queued',
110
+ channel: message.channel,
111
+ channelId: message.channelId,
112
+ replyContext: message.replyContext,
113
+ });
107
114
  }
108
115
  }
109
116
  else {
@@ -114,7 +121,7 @@ export class MessageQueue {
114
121
  }
115
122
  async processNext(queueKey) {
116
123
  this.processing.add(queueKey);
117
- logger.debug(`[Queue] Processing queue ${queueKey}`);
124
+ logger.info(`[Queue] processNext: start key=${queueKey}`);
118
125
  while (true) {
119
126
  // 等待外部锁释放(/compact, /clear 等快速命令)
120
127
  const lock = this.getExternalLock(queueKey);
@@ -124,7 +131,7 @@ export class MessageQueue {
124
131
  }
125
132
  const queue = this.queues.get(queueKey);
126
133
  if (!queue || queue.length === 0) {
127
- logger.debug(`[Queue] Queue ${queueKey} is empty, stopping`);
134
+ logger.info(`[Queue] processNext: queue empty, releasing key=${queueKey}`);
128
135
  this.processing.delete(queueKey);
129
136
  this.processingAgent.delete(queueKey);
130
137
  this.currentSessionKey = undefined;