evolclaw 2.6.0 → 2.6.2

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.
@@ -103,7 +103,7 @@ function formatIdleTime(ms) {
103
103
  return '刚刚';
104
104
  }
105
105
  // 支持的命令列表
106
- const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/agentmd', '/chatmode'];
106
+ const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/aid', '/agentmd', '/chatmode'];
107
107
  // 命令别名映射
108
108
  const aliases = {
109
109
  '/p': '/project',
@@ -127,6 +127,7 @@ export class CommandHandler {
127
127
  permissionGateway;
128
128
  interactionRouter;
129
129
  statsCollector;
130
+ hotLoadChannel;
130
131
  agentMap;
131
132
  defaultAgentId;
132
133
  /** 按 agentId 获取 agent,回退到默认 */
@@ -266,6 +267,9 @@ export class CommandHandler {
266
267
  setMessageQueue(messageQueue) {
267
268
  this.messageQueue = messageQueue;
268
269
  }
270
+ setHotLoadChannel(fn) {
271
+ this.hotLoadChannel = fn;
272
+ }
269
273
  setPermissionGateway(gateway) {
270
274
  this.permissionGateway = gateway;
271
275
  }
@@ -407,6 +411,10 @@ export class CommandHandler {
407
411
  ] : []),
408
412
  ...(isOwner ? [
409
413
  { cmd: '/file', label: '发送项目内文件', desc: '将项目目录内的文件发送给用户' },
414
+ { cmd: '/aid', label: 'AID 管理', desc: '创建新 AID 并上线新 Agent 实例', next: { type: 'select', items: [
415
+ { value: 'list', label: '列表', desc: '列出所有 AUN 实例及连接状态' },
416
+ { value: 'new', label: '创建', desc: '创建新 AID 并热加载上线', next: { type: 'text' } },
417
+ ] } },
410
418
  { cmd: '/agentmd', label: '管理 agent.md', desc: '查看或更新 AUN 网络上的 agent.md 身份文件', next: { type: 'select', items: [
411
419
  { value: 'put', label: '上传当前', desc: '将本地 agent.md 上传到 AUN 网络' },
412
420
  { value: 'set', label: '直接设置', desc: '输入内容直接更新 agent.md', next: { type: 'text' } },
@@ -517,10 +525,10 @@ export class CommandHandler {
517
525
  const isAdmin = identity.role === 'owner' || identity.role === 'admin';
518
526
  const activeChatType = activeSession?.chatType || 'private';
519
527
  if (normalizedContent.startsWith('/')) {
520
- const guestGroupCommands = ['/status', '/help', '/check'];
528
+ const guestGroupCommands = ['/status', '/help', '/check', '/chatmode'];
521
529
  const userCommands = activeChatType === 'group' && !isAdmin
522
530
  ? guestGroupCommands
523
- : ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/del', '/s ', '/check'];
531
+ : ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/del', '/s ', '/check', '/chatmode'];
524
532
  const isUserCommand = userCommands.some(cmd => normalizedContent === cmd.trimEnd() || normalizedContent.startsWith(cmd));
525
533
  if (!isUserCommand && !isAdmin) {
526
534
  return activeChatType === 'group'
@@ -634,6 +642,7 @@ export class CommandHandler {
634
642
  ...(isOwner ? [
635
643
  ' /restart - 重启服务',
636
644
  ' /file [channel] <path> - 发送项目内文件',
645
+ ' /aid [list|new <aid>] - AID 管理',
637
646
  ' /agentmd [put|set <内容>] - 管理 agent.md',
638
647
  ] : []),
639
648
  '',
@@ -1139,6 +1148,94 @@ export class CommandHandler {
1139
1148
  }
1140
1149
  return `✓ 推理强度: ${newEffort}`;
1141
1150
  }
1151
+ // /aid 命令:AID 管理(list / new)
1152
+ if (normalizedContent === '/aid' || normalizedContent === '/aid list' || normalizedContent.startsWith('/aid ')) {
1153
+ if (!isOwner)
1154
+ return '❌ 无权限:此命令仅限 owner 使用';
1155
+ const adapter = this.adapters.get(channel);
1156
+ const channelType = this.channelTypeMap.get(channel);
1157
+ if (channelType !== 'aun')
1158
+ return '❌ 此命令仅在 AUN 通道中可用';
1159
+ const arg = normalizedContent.slice(4).trim();
1160
+ // /aid 或 /aid list — 列出所有 AUN 实例
1161
+ if (!arg || arg === 'list') {
1162
+ const { normalizeChannelInstances } = await import('../config.js');
1163
+ const instances = normalizeChannelInstances(this.config.channels?.aun, 'aun');
1164
+ if (instances.length === 0)
1165
+ return '暂无 AUN 实例';
1166
+ const lines = ['AUN 实例:'];
1167
+ for (const inst of instances) {
1168
+ if (inst.enabled === false || !inst.aid)
1169
+ continue;
1170
+ const channelObj = this.channelObjects.get(inst.name);
1171
+ const status = channelObj?.getStatus?.();
1172
+ const connected = status?.connected ?? false;
1173
+ const icon = connected ? '✓' : '✗';
1174
+ const state = connected ? '已连接' : '未连接';
1175
+ lines.push(` ${icon} ${inst.name} ${inst.aid} ${state}`);
1176
+ }
1177
+ return lines.join('\n');
1178
+ }
1179
+ // /aid new <aid> — 创建新 AID 并热加载
1180
+ if (arg.startsWith('new ')) {
1181
+ const rawName = arg.slice(4).trim();
1182
+ if (!rawName)
1183
+ return '用法: /aid new <aid>\n例: /aid new reviewer';
1184
+ if (!this.hotLoadChannel)
1185
+ return '❌ 热加载未就绪';
1186
+ // Derive full AID: if no dots, append domain from current AID
1187
+ const selfAid = typeof adapter._selfAid === 'function' ? adapter._selfAid() : '';
1188
+ let fullAid = rawName;
1189
+ if (!rawName.includes('.')) {
1190
+ const domain = selfAid.split('.').slice(1).join('.');
1191
+ if (!domain)
1192
+ return '❌ 无法推导 AID 域(当前实例未连接)';
1193
+ fullAid = `${rawName}.${domain}`;
1194
+ }
1195
+ // Validate AID format
1196
+ const { isValidAid } = await import('../utils/init-channel.js');
1197
+ if (!isValidAid(fullAid))
1198
+ return `❌ 无效 AID 格式: ${fullAid}`;
1199
+ // Check instance name conflict
1200
+ const instName = rawName.includes('.') ? rawName.split('.')[0] : rawName;
1201
+ const { normalizeChannelInstances } = await import('../config.js');
1202
+ const existing = normalizeChannelInstances(this.config.channels?.aun, 'aun');
1203
+ if (existing.some(e => e.name === instName)) {
1204
+ return `❌ 实例名 "${instName}" 已存在`;
1205
+ }
1206
+ if (existing.some(e => e.aid === fullAid)) {
1207
+ return `❌ AID ${fullAid} 已在配置中`;
1208
+ }
1209
+ // Create AID (reuse init-channel.ts silent logic)
1210
+ try {
1211
+ const { createAidSilent, appendAunInstance } = await import('../utils/init-channel.js');
1212
+ const createResult = await createAidSilent({ aid: fullAid, owner: selfAid });
1213
+ // Resolve owner from current AUN instance config
1214
+ const owner = this.config.channels?.aun
1215
+ ? (Array.isArray(this.config.channels.aun)
1216
+ ? this.config.channels.aun.find((a) => a.aid === selfAid)?.owner
1217
+ : this.config.channels.aun.owner)
1218
+ : undefined;
1219
+ // Hot-load: build and register new channel instance BEFORE writing config
1220
+ const { AUNChannelPlugin } = await import('../channels/aun.js');
1221
+ const plugin = new AUNChannelPlugin();
1222
+ const tempConfig = JSON.parse(JSON.stringify(this.config));
1223
+ tempConfig.channels.aun = [{ name: instName, enabled: true, aid: fullAid, owner }];
1224
+ const newInstances = await plugin.createChannels(tempConfig);
1225
+ if (newInstances.length === 0)
1226
+ return '❌ 通道实例创建失败';
1227
+ await this.hotLoadChannel(newInstances[0]);
1228
+ // Write config only after successful hot-load
1229
+ appendAunInstance(this.config, { name: instName, aid: fullAid, owner });
1230
+ const verb = createResult.alreadyExisted ? '已存在,现已上线' : '已创建并上线';
1231
+ return `✓ ${fullAid} ${verb}\n 实例名: ${instName}\n 可在 AUN 中搜索该 AID 开始对话`;
1232
+ }
1233
+ catch (e) {
1234
+ return `❌ 创建失败: ${String(e.message || e).slice(0, 200)}`;
1235
+ }
1236
+ }
1237
+ return '用法: /aid [list|new <aid>]';
1238
+ }
1142
1239
  // /activity 命令:控制中间输出显示模式
1143
1240
  if (normalizedContent === '/agentmd' || normalizedContent.startsWith('/agentmd ')) {
1144
1241
  if (!isOwner)
@@ -1289,9 +1386,9 @@ export class CommandHandler {
1289
1386
  return `✅ 中间输出模式: ${activityArg}(${label})`;
1290
1387
  }
1291
1388
  // /chatmode 命令:查看/切换 session 会话模式(interactive | proactive)
1389
+ // - 查看:所有人可用
1390
+ // - 设置:单聊任何角色可设置;群聊仅管理员可设置
1292
1391
  if (normalizedContent === '/chatmode' || normalizedContent.startsWith('/chatmode ')) {
1293
- if (!isAdmin)
1294
- return '❌ 无权限:此命令仅限管理员使用';
1295
1392
  if (!activeSession)
1296
1393
  return '❌ 当前无活跃会话';
1297
1394
  const lockedMode = getChannelSessionMode(this.config, channel);
@@ -1304,6 +1401,9 @@ export class CommandHandler {
1304
1401
  if (arg !== 'interactive' && arg !== 'proactive') {
1305
1402
  return `❌ 无效模式: ${arg}\n可选: interactive / proactive`;
1306
1403
  }
1404
+ if (activeChatType === 'group' && !isAdmin) {
1405
+ return '❌ 无权限:群聊中切换会话模式仅限管理员使用';
1406
+ }
1307
1407
  if (lockedMode) {
1308
1408
  return `❌ 会话模式由通道配置锁定为 ${lockedMode},无法切换`;
1309
1409
  }
@@ -1454,15 +1554,18 @@ export class CommandHandler {
1454
1554
  }
1455
1555
  }
1456
1556
  const lines = [];
1557
+ const sessionMode = session.sessionMode || 'interactive';
1558
+ const lockedMode = getChannelSessionMode(this.config, channel);
1559
+ const chatModeLine = `会话模式: ${sessionMode}${lockedMode ? '(通道锁定)' : ''}`;
1457
1560
  if (isAdmin) {
1458
- lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`);
1561
+ lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, chatModeLine, `会话轮数: ${sessionTurns}`);
1459
1562
  if (health.consecutiveErrors > 0) {
1460
1563
  lines.push(`异常计数: ${health.consecutiveErrors}`);
1461
1564
  }
1462
1565
  lines.push(`最后成功: ${timeStr}`, `${session.agentId}会话: ${session.agentSessionId || '(未初始化)'}`, `创建时间: ${new Date(session.createdAt).toLocaleString('zh-CN')}`, `更新时间: ${new Date(session.updatedAt).toLocaleString('zh-CN')}`);
1463
1566
  }
1464
1567
  else {
1465
- lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / ${session.agentId}会话`, `状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
1568
+ lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / ${session.agentId}会话`, `状态: ${sessionStatus}`, chatModeLine, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
1466
1569
  }
1467
1570
  if (health.lastError) {
1468
1571
  lines.push('');
@@ -2601,7 +2704,7 @@ export class CommandHandler {
2601
2704
  static CTL_COMMANDS = [
2602
2705
  '/help', '/status', '/check',
2603
2706
  '/model', '/effort', '/perm',
2604
- '/compact', '/activity', '/file', '/send', '/chatmode', '/restart', '/agentmd',
2707
+ '/compact', '/activity', '/file', '/send', '/chatmode', '/restart', '/agentmd', '/bind', '/aid',
2605
2708
  ];
2606
2709
  /**
2607
2710
  * 从 session 恢复 ReplyContext,用于 ctl send 主动发送文本时的路由
@@ -1,7 +1,9 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs';
3
+ import crypto from 'crypto';
3
4
  import { hasCompact } from '../../agents/claude-runner.js';
4
5
  import { StreamFlusher } from './stream-flusher.js';
6
+ import { ThoughtEmitter } from './thought-emitter.js';
5
7
  import { StreamIdleMonitor } from './stream-idle-monitor.js';
6
8
  import { logger } from '../../utils/logger.js';
7
9
  import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, prefixErrorType, isRetryableError } from '../../utils/error-utils.js';
@@ -27,7 +29,6 @@ export class MessageProcessor {
27
29
  interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
28
30
  interactionRouter;
29
31
  messageQueue;
30
- skillsHintDesc = undefined; // undefined=未加载, null=无模板, string=缓存描述
31
32
  skillsEnsured = false; // 全局 SKILLS.md 是否已确保
32
33
  /** 按 agentId 获取 agent,回退到默认 */
33
34
  getAgent(agentId) {
@@ -279,6 +280,8 @@ export class MessageProcessor {
279
280
  const { adapter, options } = channelInfo;
280
281
  const agent = this.getAgent(session.agentId);
281
282
  const streamKey = session.id;
283
+ // 为本次任务处理生成唯一 task_id(客户端生成,格式 task-{10hex})
284
+ const taskId = `task-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`;
282
285
  try {
283
286
  const isBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
284
287
  // 记录收到消息
@@ -302,7 +305,7 @@ export class MessageProcessor {
302
305
  logger.info(`[MessageProcessor] session=${session.id} chatType=${session.chatType} sessionMode=${session.sessionMode} agentId=${session.agentId} msgChatType=${message.chatType ?? 'n/a'}`);
303
306
  // 记录开始处理
304
307
  this.eventBus.publish({ type: 'message:processing', sessionId: session.id });
305
- adapter.sendProcessingStatus?.(message.channelId, 'start', session.id, this.getReplyContext(message));
308
+ adapter.sendProcessingStatus?.(message.channelId, 'start', session.id, taskId, this.getReplyContext(message));
306
309
  logger.message({
307
310
  msgId: messageId,
308
311
  sessionId: session.id,
@@ -338,6 +341,12 @@ export class MessageProcessor {
338
341
  }, (options?.flushDelay ?? this.config.flushDelay ?? 3) * 1000, options?.fileMarkerPattern, this.config.debug?.flusherDiag, isProactive);
339
342
  // 保存当前 flusher,用于 compact 事件
340
343
  this.currentFlusher = flusher;
344
+ // Proactive 模式可观测:创建 ThoughtEmitter,将静默的流式事件转发为 thought
345
+ // selector: context = { type: 'task', id: taskId }
346
+ let thoughtEmitter = null;
347
+ if (isProactive && adapter.putThought) {
348
+ thoughtEmitter = new ThoughtEmitter(adapter, message.channelId, taskId);
349
+ }
341
350
  // 调用 AgentRunner(含上下文过长自动 compact 重试)
342
351
  // 捕获当前消息的上下文(闭包),避免后续消息处理时串台
343
352
  const capturedChannelId = message.channelId;
@@ -380,15 +389,28 @@ export class MessageProcessor {
380
389
  const peerLabel = session.identity?.role || 'unknown';
381
390
  const peerName = message.peerName || session.metadata?.peerName;
382
391
  const peerType = message.peerType;
392
+ const peerId = message.peerId;
393
+ const adapterAny = channelInfo.adapter;
394
+ const selfAid = typeof adapterAny._selfAid === 'function' ? adapterAny._selfAid() : undefined;
395
+ const selfName = typeof adapterAny._selfName === 'function' ? adapterAny._selfName() : undefined;
396
+ const formatIdentity = (name, id) => {
397
+ if (name && id)
398
+ return `${name} (${id})`;
399
+ return name || id || undefined;
400
+ };
401
+ const selfIdentity = formatIdentity(selfName, selfAid);
402
+ const peerIdentity = formatIdentity(peerName, peerId);
383
403
  const envParts = [
384
404
  `会话通道: ${currentChannelType}`,
385
405
  `当前项目: ${path.basename(absoluteProjectPath)}`,
386
406
  ];
387
407
  if (session.name)
388
408
  envParts.push(`会话名称: ${session.name}`);
409
+ if (selfIdentity)
410
+ envParts.push(`当前名称: ${selfIdentity}`);
389
411
  envParts.push(`对端身份: ${peerLabel}`);
390
- if (peerName)
391
- envParts.push(`对端名称: ${peerName}`);
412
+ if (peerIdentity)
413
+ envParts.push(`对端名称: ${peerIdentity}`);
392
414
  if (peerType && peerType !== 'unknown')
393
415
  envParts.push(`对端类型: ${peerType}`);
394
416
  if (session.chatType)
@@ -439,14 +461,16 @@ export class MessageProcessor {
439
461
  contextParts.push(`[群聊回复规则] 回复时必须在开头添加 @${message.peerId} 来通知对方`);
440
462
  }
441
463
  // 5. Agent ctl 自管理指令提示 + SKILLS.md 生成
442
- if (!this.skillsEnsured) {
443
- this.ensureSkillsFile();
444
- this.skillsEnsured = true;
445
- }
446
- const skillsHint = this.getSkillsHint();
447
- if (skillsHint) {
448
- contextParts.push(`[EvolClaw 自管理] ${skillsHint}`);
449
- }
464
+ // 暂时注释:排查 proactive 模式下 agent 未调用 Bash 工具的问题,
465
+ // 怀疑此段与 [Proactive 模式] 语义重合稀释了后者的权重
466
+ // if (!this.skillsEnsured) {
467
+ // this.ensureSkillsFile();
468
+ // this.skillsEnsured = true;
469
+ // }
470
+ // const skillsHint = this.getSkillsHint();
471
+ // if (skillsHint) {
472
+ // contextParts.push(`[EvolClaw 自管理] ${skillsHint}`);
473
+ // }
450
474
  // 6. Proactive 模式提示词:agent 的输出不会自动发送,必须主动调用 ctl send/file
451
475
  if (isProactive) {
452
476
  contextParts.push('[Proactive 模式] 本次对话中你的流式输出不会自动发送给用户,必须通过以下命令主动发送:\n' +
@@ -463,7 +487,7 @@ export class MessageProcessor {
463
487
  const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager);
464
488
  agent.registerStream(streamKey, stream);
465
489
  streamRegistered = true;
466
- streamResult = await this.processEventStream(stream, session, flusher, resetTimer, shouldSuppress);
490
+ streamResult = await this.processEventStream(stream, session, flusher, resetTimer, shouldSuppress, thoughtEmitter);
467
491
  break; // 成功,跳出重试循环
468
492
  }
469
493
  catch (retryError) {
@@ -493,7 +517,7 @@ export class MessageProcessor {
493
517
  flusher.addActivity('\u2705 压缩完成,正在重试...');
494
518
  const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, options?.systemPromptAppend, this.sessionManager);
495
519
  agent.registerStream(streamKey, retryStream);
496
- streamResult = await this.processEventStream(retryStream, session, flusher, resetTimer, shouldSuppress);
520
+ streamResult = await this.processEventStream(retryStream, session, flusher, resetTimer, shouldSuppress, thoughtEmitter);
497
521
  }
498
522
  else {
499
523
  throw new Error('CONTEXT_COMPACT_FAILED');
@@ -607,7 +631,7 @@ export class MessageProcessor {
607
631
  const errorSummary = streamResult.errors?.join('; ') || '任务执行失败';
608
632
  const rawSubtype = streamResult.subtype || 'agent_error';
609
633
  const errorType = prefixErrorType(ERROR_PREFIX.AGENT, rawSubtype);
610
- adapter.sendProcessingStatus?.(message.channelId, 'error', session.id, this.getReplyContext(message));
634
+ adapter.sendProcessingStatus?.(message.channelId, 'error', session.id, taskId, this.getReplyContext(message));
611
635
  this.eventBus.publish({
612
636
  type: 'message:error',
613
637
  sessionId: session.id,
@@ -635,7 +659,7 @@ export class MessageProcessor {
635
659
  }
636
660
  else {
637
661
  // 真正的成功
638
- adapter.sendProcessingStatus?.(message.channelId, interruptReason ? 'interrupted' : 'done', session.id, this.getReplyContext(message));
662
+ adapter.sendProcessingStatus?.(message.channelId, interruptReason ? 'interrupted' : 'done', session.id, taskId, this.getReplyContext(message));
639
663
  await this.sessionManager.recordSuccess(session.id);
640
664
  this.eventBus.publish({
641
665
  type: 'message:completed',
@@ -688,7 +712,7 @@ export class MessageProcessor {
688
712
  // 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送中断/错误提示
689
713
  if (!isUserInterrupt) {
690
714
  try {
691
- adapter.sendProcessingStatus?.(message.channelId, procStatus, session.id, this.getReplyContext(message));
715
+ adapter.sendProcessingStatus?.(message.channelId, procStatus, session.id, taskId, this.getReplyContext(message));
692
716
  }
693
717
  catch { }
694
718
  }
@@ -764,7 +788,7 @@ export class MessageProcessor {
764
788
  * 此方法只消费标准 AgentEvent 类型,不引用任何 SDK 特有事件。
765
789
  * SDK 事件 → AgentEvent 的转换在 AgentRunner.transformStream() 中完成。
766
790
  */
767
- async processEventStream(stream, session, flusher, resetTimer, shouldSuppress) {
791
+ async processEventStream(stream, session, flusher, resetTimer, shouldSuppress, thoughtEmitter) {
768
792
  let hasReceivedText = false;
769
793
  let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
770
794
  let completeResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
@@ -777,6 +801,10 @@ export class MessageProcessor {
777
801
  resetTimer(event.type, toolName);
778
802
  // 记录所有事件类型
779
803
  logger.info(`[MessageProcessor] Event: type=${event.type}`);
804
+ // Proactive 可观测:将事件实时透传为 thought(fire-and-forget)
805
+ if (thoughtEmitter) {
806
+ thoughtEmitter.emit(event).catch(() => { });
807
+ }
780
808
  // session_id 已在 AgentRunner.transformStream 中处理,此处仅记录
781
809
  if (event.type === 'session_id') {
782
810
  logger.info(`[MessageProcessor] Session ID updated: ${event.sessionId} for session: ${session.id}`);
@@ -1077,42 +1105,28 @@ export class MessageProcessor {
1077
1105
  return 0;
1078
1106
  }
1079
1107
  /**
1080
- * 从模板 frontmatter 缓存提示(懒加载,整个进程只读一次模板文件)
1108
+ * data/SKILLS.md 读取 frontmatter 并生成提示。
1109
+ * 不缓存:每次读取保证用户编辑立即生效。
1110
+ * 调用前应确保 ensureSkillsFile() 已执行过(首次落盘)。
1081
1111
  */
1082
1112
  getSkillsHint() {
1083
- if (this.skillsHintDesc === undefined) {
1084
- this.skillsHintDesc = this.loadSkillsHint();
1085
- }
1086
- return this.skillsHintDesc;
1087
- }
1088
- /**
1089
- * 从包模板源读取 frontmatter 并生成提示(仅执行一次)
1090
- */
1091
- loadSkillsHint() {
1092
1113
  try {
1093
- const candidates = [
1094
- path.join(getPackageRoot(), 'src', 'templates', 'skills.md'),
1095
- path.join(getPackageRoot(), 'dist', 'templates', 'skills.md'),
1114
+ const skillsPath = path.join(resolveRoot(), 'data', 'SKILLS.md');
1115
+ if (!fs.existsSync(skillsPath))
1116
+ return null;
1117
+ const content = fs.readFileSync(skillsPath, 'utf-8');
1118
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
1119
+ if (!frontmatterMatch)
1120
+ return null;
1121
+ const fm = frontmatterMatch[1];
1122
+ const desc = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() || 'EvolClaw 运行时管理指令';
1123
+ const trigger = fm.match(/^trigger:\s*(.+)$/m)?.[1]?.trim() || '';
1124
+ const parts = [
1125
+ `可通过 Bash 指令管理运行时,${desc}。`,
1126
+ trigger ? `触发时机:${trigger}。` : '',
1127
+ `完整文档见 ${skillsPath}`,
1096
1128
  ];
1097
- for (const templatePath of candidates) {
1098
- if (!fs.existsSync(templatePath))
1099
- continue;
1100
- const content = fs.readFileSync(templatePath, 'utf-8');
1101
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
1102
- if (!frontmatterMatch)
1103
- continue;
1104
- const fm = frontmatterMatch[1];
1105
- const desc = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() || 'EvolClaw 运行时管理指令';
1106
- const trigger = fm.match(/^trigger:\s*(.+)$/m)?.[1]?.trim() || '';
1107
- const skillsPath = path.join(resolveRoot(), 'data', 'SKILLS.md');
1108
- const parts = [
1109
- `可通过 Bash 执行 \`evolclaw ctl <cmd>\` 管理运行时:${desc}`,
1110
- trigger ? `触发时机:${trigger}` : '',
1111
- `完整文档见 ${skillsPath}`,
1112
- ];
1113
- return parts.filter(Boolean).join('\n');
1114
- }
1115
- return null;
1129
+ return parts.filter(Boolean).join('');
1116
1130
  }
1117
1131
  catch {
1118
1132
  return null;
@@ -169,7 +169,7 @@ export class MessageQueue {
169
169
  * 合并多条同 peerId 消息:
170
170
  * - content: \n 连接
171
171
  * - images / mentions: 扁平合并
172
- * - messageId: 置空(合并后不代表某一条具体消息)
172
+ * - messageId: 取最新一条的 messageId(用于 thought 锚定与中断追踪)
173
173
  * - replyContext / peerName / 其余字段: 取最后一条
174
174
  */
175
175
  mergeItems(items) {
@@ -185,12 +185,20 @@ export class MessageQueue {
185
185
  allMentions.push(...m.mentions);
186
186
  }
187
187
  const last = items[items.length - 1];
188
+ // 保留最新一条的 messageId(若最后一条无 ID 则回退到前面已有的 ID)
189
+ let latestMessageId;
190
+ for (let i = items.length - 1; i >= 0; i--) {
191
+ if (items[i].message.messageId) {
192
+ latestMessageId = items[i].message.messageId;
193
+ break;
194
+ }
195
+ }
188
196
  const merged = {
189
197
  ...last.message,
190
198
  content: contents.join('\n'),
191
199
  images: allImages.length > 0 ? allImages : undefined,
192
200
  mentions: allMentions.length > 0 ? allMentions : undefined,
193
- messageId: undefined,
201
+ messageId: latestMessageId,
194
202
  };
195
203
  return {
196
204
  message: merged,
@@ -95,13 +95,21 @@ export class StreamDebouncer {
95
95
  allMentions.push(...e.mentions);
96
96
  }
97
97
  const last = entries[entries.length - 1];
98
+ // 合并后保留最新一条的 messageId(用于 thought 锚定与中断追踪)
99
+ let latestMessageId;
100
+ for (let i = entries.length - 1; i >= 0; i--) {
101
+ if (entries[i].messageId) {
102
+ latestMessageId = entries[i].messageId;
103
+ break;
104
+ }
105
+ }
98
106
  const merged = {
99
107
  ...last.rest,
100
108
  content: contents.join('\n'),
101
109
  images: allImages.length > 0 ? allImages : undefined,
102
110
  mentions: allMentions.length > 0 ? allMentions : undefined,
103
111
  replyContext: last.replyContext,
104
- messageId: entries.length > 1 ? undefined : last.messageId,
112
+ messageId: latestMessageId,
105
113
  };
106
114
  const resolves = entries.map(e => e.resolve);
107
115
  const rejects = entries.map(e => e.reject);
@@ -0,0 +1,153 @@
1
+ import { logger } from '../../utils/logger.js';
2
+ /**
3
+ * ThoughtEmitter — 将 Proactive 模式下的流式 AgentEvent 实时发送为 thought
4
+ *
5
+ * 设计特点:
6
+ * - 不做聚合/batching,逐事件调用 adapter.putThought()
7
+ * - 不感知 group vs P2P,通道差异由 adapter 内部处理
8
+ * - taskId 映射为 context: { type: 'task', id: taskId }(协议 selector)
9
+ * - fire-and-forget:调用方不 await emit(),错误被内部捕获
10
+ */
11
+ export class ThoughtEmitter {
12
+ adapter;
13
+ channelId;
14
+ taskId;
15
+ hasEmittedText = false;
16
+ constructor(adapter, channelId, taskId) {
17
+ if (!taskId) {
18
+ throw new Error('[ThoughtEmitter] taskId is required at construction');
19
+ }
20
+ this.adapter = adapter;
21
+ this.channelId = channelId;
22
+ this.taskId = taskId;
23
+ }
24
+ async emit(event) {
25
+ // 对齐 interactive 的 dedup:流式 text 已推过时,complete.result 不再重复发 summary
26
+ if (event.type === 'complete' &&
27
+ !event.isError &&
28
+ event.result &&
29
+ this.hasEmittedText) {
30
+ return;
31
+ }
32
+ const payload = this.mapEventToPayload(event);
33
+ if (!payload)
34
+ return;
35
+ if (!this.adapter.putThought)
36
+ return;
37
+ if (payload.stage === 'thinking') {
38
+ this.hasEmittedText = true;
39
+ }
40
+ try {
41
+ await this.adapter.putThought(this.channelId, this.taskId, payload);
42
+ }
43
+ catch (err) {
44
+ logger.debug(`[ThoughtEmitter] putThought failed: ${err.message}`);
45
+ }
46
+ }
47
+ mapEventToPayload(event) {
48
+ switch (event.type) {
49
+ case 'text':
50
+ if (!event.text)
51
+ return null;
52
+ return { type: 'thought', text: event.text, stage: 'thinking' };
53
+ case 'tool_use': {
54
+ const desc = this.summarizeInput(event.input, event.name);
55
+ return {
56
+ type: 'thought',
57
+ text: desc ? `🔧 ${event.name}: ${desc}` : `🔧 ${event.name}`,
58
+ stage: 'tool',
59
+ metadata: { tool: event.name, input: desc },
60
+ };
61
+ }
62
+ case 'tool_result':
63
+ if (event.isError) {
64
+ return {
65
+ type: 'thought',
66
+ text: `⚠️ ${event.name}: ${event.error || '执行失败'}`,
67
+ stage: 'tool',
68
+ metadata: { tool: event.name, ok: false },
69
+ };
70
+ }
71
+ {
72
+ const resultText = this.truncate(this.stringifyResult(event.result), 200);
73
+ return {
74
+ type: 'thought',
75
+ text: resultText ? `✅ ${event.name}: ${resultText}` : `✅ ${event.name}`,
76
+ stage: 'tool',
77
+ metadata: { tool: event.name, ok: true },
78
+ };
79
+ }
80
+ case 'compact':
81
+ return {
82
+ type: 'thought',
83
+ text: `💡 会话压缩完成 (压缩前 tokens: ${event.preTokens})`,
84
+ stage: 'system',
85
+ };
86
+ case 'task_progress': {
87
+ const stats = this.formatTaskStats(event);
88
+ const text = event.summary
89
+ ? `⏳ 子任务: ${event.summary}${stats ? ` (${stats})` : ''}`
90
+ : `⏳ 子任务进行中${stats ? `: ${stats}` : ''}`;
91
+ return { type: 'thought', text, stage: 'planning' };
92
+ }
93
+ case 'error':
94
+ return { type: 'thought', text: `❌ ${event.error}`, stage: 'error' };
95
+ case 'complete':
96
+ if (event.isError) {
97
+ const errText = event.errors?.join('; ') || event.result || '任务失败';
98
+ return { type: 'thought', text: `❌ ${errText}`, stage: 'error' };
99
+ }
100
+ if (event.result) {
101
+ return { type: 'thought', text: event.result, stage: 'summary' };
102
+ }
103
+ return null;
104
+ case 'session_id':
105
+ case 'state_changed':
106
+ case 'status':
107
+ return null;
108
+ default:
109
+ return null;
110
+ }
111
+ }
112
+ summarizeInput(input, toolName) {
113
+ if (!input || typeof input !== 'object')
114
+ return '';
115
+ // Bash + ctl send/file: 显示完整命令内容(含发送的消息正文)
116
+ if (toolName === 'Bash' && typeof input.command === 'string') {
117
+ const cmd = input.command;
118
+ if (cmd.includes('evolclaw ctl send') || cmd.includes('evolclaw ctl file')) {
119
+ return cmd;
120
+ }
121
+ }
122
+ return (input.description ||
123
+ input.file_path ||
124
+ input.pattern ||
125
+ (typeof input.command === 'string' ? input.command.substring(0, 80) : '') ||
126
+ (typeof input.prompt === 'string' ? input.prompt.substring(0, 80) : '') ||
127
+ (typeof input.query === 'string' ? input.query.substring(0, 80) : '') ||
128
+ '');
129
+ }
130
+ stringifyResult(result) {
131
+ if (result === null || result === undefined)
132
+ return '';
133
+ if (typeof result === 'string')
134
+ return result;
135
+ try {
136
+ return JSON.stringify(result);
137
+ }
138
+ catch {
139
+ return String(result);
140
+ }
141
+ }
142
+ truncate(text, maxLen) {
143
+ return text.length > maxLen ? text.substring(0, maxLen) + '...' : text;
144
+ }
145
+ formatTaskStats(event) {
146
+ const parts = [];
147
+ if (event.toolUses)
148
+ parts.push(`${event.toolUses} tools`);
149
+ if (event.durationMs)
150
+ parts.push(`${Math.round(event.durationMs / 1000)}s`);
151
+ return parts.join(', ');
152
+ }
153
+ }