evolclaw 3.1.4 → 3.1.6

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 (99) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/agents/claude-runner.js +398 -161
  3. package/dist/agents/kit-renderer.js +191 -25
  4. package/dist/aun/aid/agentmd.js +75 -103
  5. package/dist/aun/aid/client.js +1 -29
  6. package/dist/aun/aid/identity.js +105 -64
  7. package/dist/aun/aid/index.js +2 -1
  8. package/dist/aun/aid/store.js +74 -0
  9. package/dist/aun/msg/group.js +2 -2
  10. package/dist/aun/msg/p2p.js +26 -2
  11. package/dist/aun/rpc/connection.js +23 -30
  12. package/dist/channels/aun.js +174 -99
  13. package/dist/channels/dingtalk.js +2 -1
  14. package/dist/channels/feishu.js +301 -199
  15. package/dist/channels/qqbot.js +2 -1
  16. package/dist/channels/wechat.js +2 -1
  17. package/dist/channels/wecom.js +2 -1
  18. package/dist/cli/agent.js +21 -16
  19. package/dist/cli/bench.js +41 -28
  20. package/dist/cli/help.js +8 -0
  21. package/dist/cli/index.js +176 -87
  22. package/dist/cli/init-channel.js +5 -1
  23. package/dist/cli/init.js +37 -21
  24. package/dist/cli/link-rules.js +1 -7
  25. package/dist/cli/model.js +549 -0
  26. package/dist/cli/net-check.js +133 -50
  27. package/dist/cli/watch-msg.js +7 -7
  28. package/dist/cli/watch-web/debug-log.js +18 -0
  29. package/dist/cli/watch-web/server.js +306 -0
  30. package/dist/cli/watch-web/sources/aid.js +63 -0
  31. package/dist/cli/watch-web/sources/msg.js +70 -0
  32. package/dist/cli/watch-web/sources/session.js +638 -0
  33. package/dist/cli/watch-web/sources/types.js +10 -0
  34. package/dist/cli/watch-web/static/app.js +546 -0
  35. package/dist/cli/watch-web/static/index.html +54 -0
  36. package/dist/cli/watch-web/static/style.css +247 -0
  37. package/dist/config-store.js +1 -22
  38. package/dist/core/channel-loader.js +7 -4
  39. package/dist/core/command-handler.js +261 -133
  40. package/dist/core/evolagent-registry.js +1 -1
  41. package/dist/core/evolagent.js +4 -22
  42. package/dist/core/interaction-router.js +59 -0
  43. package/dist/core/message/im-renderer.js +9 -20
  44. package/dist/core/message/message-bridge.js +13 -9
  45. package/dist/core/message/message-log.js +2 -2
  46. package/dist/core/message/message-processor.js +211 -123
  47. package/dist/core/message/stream-idle-monitor.js +21 -0
  48. package/dist/core/model/model-catalog.js +215 -0
  49. package/dist/core/model/model-scope.js +250 -0
  50. package/dist/core/relation/peer-identity.js +58 -55
  51. package/dist/core/relation/peer-key.js +16 -0
  52. package/dist/core/session/session-fs-store.js +34 -55
  53. package/dist/core/session/session-key.js +24 -0
  54. package/dist/core/session/session-manager.js +308 -251
  55. package/dist/core/session/session-mapper.js +9 -4
  56. package/dist/core/trigger/manager.js +3 -3
  57. package/dist/core/trigger/parser.js +4 -4
  58. package/dist/core/trigger/scheduler.js +22 -7
  59. package/dist/index.js +61 -7
  60. package/dist/ipc.js +23 -1
  61. package/dist/utils/error-utils.js +6 -0
  62. package/dist/utils/process-introspect.js +7 -5
  63. package/kits/docs/GUIDE.md +2 -2
  64. package/kits/docs/INDEX.md +8 -8
  65. package/kits/docs/channels/aun.md +56 -17
  66. package/kits/docs/channels/feishu.md +41 -12
  67. package/kits/docs/context-assembly.md +182 -0
  68. package/kits/docs/evolclaw/INDEX.md +43 -0
  69. package/kits/docs/evolclaw/agent.md +49 -0
  70. package/kits/docs/evolclaw/aid.md +49 -0
  71. package/kits/docs/evolclaw/ctl.md +46 -0
  72. package/kits/docs/evolclaw/group.md +89 -0
  73. package/kits/docs/evolclaw/model.md +51 -0
  74. package/kits/docs/evolclaw/msg.md +91 -0
  75. package/kits/docs/evolclaw/rpc.md +35 -0
  76. package/kits/docs/evolclaw/storage.md +49 -0
  77. package/kits/docs/venues/aun-group.md +10 -0
  78. package/kits/docs/venues/aun-private.md +10 -0
  79. package/kits/docs/venues/client-desktop.md +10 -0
  80. package/kits/docs/venues/client-mobile.md +10 -0
  81. package/kits/docs/venues/feishu-group.md +13 -0
  82. package/kits/docs/venues/feishu-private.md +9 -0
  83. package/kits/docs/venues/group.md +23 -0
  84. package/kits/docs/venues/private.md +10 -0
  85. package/kits/eck_manifest.json +81 -36
  86. package/kits/rules/01-overview.md +20 -10
  87. package/kits/rules/06-channel.md +34 -27
  88. package/kits/templates/system-fragments/baseagent.md +7 -1
  89. package/kits/templates/system-fragments/channel.md +7 -5
  90. package/kits/templates/system-fragments/commands.md +19 -0
  91. package/kits/templates/system-fragments/session.md +19 -3
  92. package/kits/templates/system-fragments/venue.md +24 -0
  93. package/package.json +10 -5
  94. package/dist/aun/aid/lifecycle-log.js +0 -33
  95. package/dist/utils/aid-lifecycle-log.js +0 -33
  96. package/kits/docs/evolclaw/AGENT_CMD.md +0 -31
  97. package/kits/docs/evolclaw/MSG_GROUP.md +0 -30
  98. package/kits/docs/evolclaw/MSG_PRIVATE.md +0 -72
  99. package/kits/docs/evolclaw/tools.md +0 -25
@@ -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';
@@ -12,6 +13,24 @@ import { getPackageRoot, resolveRoot } from '../../paths.js';
12
13
  import { renderKitSections } from '../../agents/kit-renderer.js';
13
14
  import { normalizeBaseagent } from '../../agents/baseagent-normalize.js';
14
15
  import { renderActionAsText, renderCommandCardAsText } from '../interaction-router.js';
16
+ import { formatPeerKey } from '../relation/peer-key.js';
17
+ import { resolveEffectiveModel } from '../model/model-scope.js';
18
+ /** OS 信息在进程生命周期内是常量,模块加载时算一次。例: "Windows 11 Pro (win32 10.0.26200)" */
19
+ const OS_INFO = (() => {
20
+ let label = '';
21
+ try {
22
+ label = os.version();
23
+ }
24
+ catch { /* 旧 Node 无 os.version */ }
25
+ return `${label ? label + ' ' : ''}(${os.platform()} ${os.release()})`;
26
+ })();
27
+ /** 当前 UTC 偏移,格式 +08:00 / -05:00。每条消息算(DST 安全)。 */
28
+ function currentTzOffset() {
29
+ const off = -new Date().getTimezoneOffset(); // 分钟,东区为正
30
+ const sign = off >= 0 ? '+' : '-';
31
+ const abs = Math.abs(off);
32
+ return `${sign}${String(Math.floor(abs / 60)).padStart(2, '0')}:${String(abs % 60).padStart(2, '0')}`;
33
+ }
15
34
  function getContextTooLongHint(agent) {
16
35
  if (canCompactAgent(agent)) {
17
36
  return '上下文过长,请精简提问或使用 /compact 压缩上下文';
@@ -67,9 +86,12 @@ export class MessageProcessor {
67
86
  agentMap;
68
87
  primaryRunnerKey;
69
88
  interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
89
+ /** sessionId → 模型降级状态(带退避探测,进程重启清零) */
90
+ modelFallbackMap = new Map();
70
91
  interactionRouter;
71
92
  messageQueue;
72
- skillsEnsured = false; // 全局 SKILLS.md 是否已确保
93
+ /** sessionId 活跃的空闲监控器,用于等待用户交互期间暂停/恢复计时 */
94
+ activeMonitors = new Map();
73
95
  /**
74
96
  * Get the runner for a given (channel, baseagent) pair.
75
97
  *
@@ -99,7 +121,7 @@ export class MessageProcessor {
99
121
  if (session.threadId)
100
122
  return false;
101
123
  // 使用 session 自身的 channelType 精确定位 active.json,避免扫描误匹配
102
- const active = this.sessionManager.getActiveSessionSync(session.channel, session.channelId, session.channelType, session.selfId);
124
+ const active = this.sessionManager.getActiveSessionSync(session.channel, session.channelId, session.channelType, session.selfAID);
103
125
  return active ? session.id !== active.id : false;
104
126
  }
105
127
  constructor(agentRunnerOrMap, sessionManager, globalSettings, messageCache, eventBus, commandHandler, primaryRunnerKey) {
@@ -126,6 +148,16 @@ export class MessageProcessor {
126
148
  }
127
149
  setInteractionRouter(router) {
128
150
  this.interactionRouter = router;
151
+ // 等待用户交互期间暂停 idle 监控,应答/取消/超时后恢复——
152
+ // 避免把「正在等用户点按钮」误判为「任务卡死」而中断任务。
153
+ router.setWaitHooks({
154
+ onWaitStart: (sessionId) => {
155
+ this.activeMonitors.get(sessionId)?.pause();
156
+ },
157
+ onWaitEnd: (sessionId) => {
158
+ this.activeMonitors.get(sessionId)?.resume();
159
+ },
160
+ });
129
161
  }
130
162
  setMessageQueue(queue) {
131
163
  this.messageQueue = queue;
@@ -195,15 +227,15 @@ export class MessageProcessor {
195
227
  }
196
228
  // 命令前缀列表(与 CommandHandler.quickCommandPrefixes 保持同步)
197
229
  static COMMAND_PREFIXES = [
198
- '/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart',
230
+ '/new', '/pwd', '/help', '/status', '/restart',
199
231
  '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
200
232
  '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check',
201
- '/p ', '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/chatmode',
233
+ '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/chatmode',
202
234
  '/aid', '/upgrade', '/evolagent',
203
235
  ];
204
236
  /** 判断消息内容是否为已知命令 */
205
237
  isKnownCommand(content) {
206
- return content === '/p' || content === '/s' ||
238
+ return content === '/s' ||
207
239
  MessageProcessor.COMMAND_PREFIXES.some(cmd => content.startsWith(cmd));
208
240
  }
209
241
  /**
@@ -211,10 +243,33 @@ export class MessageProcessor {
211
243
  */
212
244
  async processMessage(message) {
213
245
  const idleMs = (this.globalSettings.idleMonitor?.timeout ?? 120) * 1000;
214
- // 先解析会话,再优先用 session.metadata.channelName 精确定位实例级 adapter
246
+ // 先解析会话,再优先用 session.metadata.channelKey 精确定位实例级 adapter
215
247
  // message.channel 现在存实例名(channelName),可直接用于精确路由
216
248
  const { session, absoluteProjectPath } = await this.resolveSession(message);
217
- const channelKey = session.metadata?.channelName || message.channel;
249
+ // thread(feishu) pending strategy: inject replyContext so first reply creates the thread
250
+ if (message.triggerMeta?.pendingThread && message.triggerMeta?.rootMessageId) {
251
+ const triggerId = message.triggerMeta.triggerId;
252
+ const channelKeyForAgent = session.metadata?.channelKey || message.channel;
253
+ const trigMgr = this.agentRegistry?.resolveByChannel(channelKeyForAgent)?.triggerManager;
254
+ const onThreadCreated = trigMgr
255
+ ? (threadId) => {
256
+ try {
257
+ trigMgr.update(triggerId, { targetThreadId: threadId, pendingThread: false });
258
+ logger.info(`[MessageProcessor] Feishu thread created for trigger ${triggerId}: ${threadId}`);
259
+ }
260
+ catch (e) {
261
+ logger.warn(`[MessageProcessor] Failed to write back thread_id for trigger ${triggerId}: ${e}`);
262
+ }
263
+ }
264
+ : undefined;
265
+ message.replyContext = {
266
+ ...(message.replyContext ?? {}),
267
+ replyToMessageId: message.triggerMeta.rootMessageId,
268
+ replyInThread: true,
269
+ ...(onThreadCreated ? { metadata: { ...(message.replyContext?.metadata ?? {}), onThreadCreated } } : {}),
270
+ };
271
+ }
272
+ const channelKey = session.metadata?.channelKey || message.channel;
218
273
  const channelInfo = this.resolveChannelInfo(channelKey);
219
274
  if (!channelInfo) {
220
275
  logger.error(`[MessageProcessor] Unknown channel: ${channelKey}`);
@@ -252,6 +307,7 @@ export class MessageProcessor {
252
307
  if (!monitorEnabled)
253
308
  return;
254
309
  monitor = new StreamIdleMonitor(idleMs);
310
+ this.activeMonitors.set(streamKey, monitor);
255
311
  monitorInterval = setInterval(() => {
256
312
  // Drain all pending levels in one tick
257
313
  let result = monitor.check();
@@ -332,6 +388,7 @@ export class MessageProcessor {
332
388
  finally {
333
389
  if (monitorInterval)
334
390
  clearInterval(monitorInterval);
391
+ this.activeMonitors.delete(streamKey);
335
392
  }
336
393
  }
337
394
  /** 获取回复上下文(跟着任务走) */
@@ -341,7 +398,7 @@ export class MessageProcessor {
341
398
  /** 自动安全模式已禁用:仅保留错误计数,不再自动切换状态 */
342
399
  async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress) {
343
400
  const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
344
- const channelKey = session.metadata?.channelName || message.channel;
401
+ const channelKey = session.metadata?.channelKey || message.channel;
345
402
  const channelInfo = this.resolveChannelInfo(channelKey);
346
403
  // Per-method agent name for stats bucketing (agent.name or '<unknown>')
347
404
  const agentNameForStats = this.agentRegistry?.resolveByChannel(channelKey)?.name ?? '<unknown>';
@@ -373,7 +430,7 @@ export class MessageProcessor {
373
430
  };
374
431
  };
375
432
  const isProactive = session.sessionMode === 'proactive';
376
- const isAutonomous = session.sessionMode === 'autonomous' || message.triggerMeta?.silent === true;
433
+ const isAutonomous = session.sessionMode === 'autonomous';
377
434
  const envelope = buildEnvelope({
378
435
  taskId,
379
436
  channel: message.channel,
@@ -448,7 +505,7 @@ export class MessageProcessor {
448
505
  if (baseReplyCtx) {
449
506
  Object.assign(opts, baseReplyCtx);
450
507
  }
451
- else if (firstReply && message.messageId) {
508
+ else if (firstReply && message.messageId && message.source !== 'trigger') {
452
509
  if (payload.kind === 'result.text' && payload.text) {
453
510
  opts.replyToMessageId = message.messageId;
454
511
  firstReply = false;
@@ -508,6 +565,10 @@ export class MessageProcessor {
508
565
  : message.content;
509
566
  let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
510
567
  let effectiveSystemPrompt;
568
+ let modelOverride;
569
+ let usedFallback = false;
570
+ let skipEvolclawModel = false;
571
+ let agentModel;
511
572
  try {
512
573
  // 动态构建运行时上下文提示
513
574
  const contextParts = [];
@@ -533,19 +594,62 @@ export class MessageProcessor {
533
594
  contextParts.push(persona);
534
595
  if (working)
535
596
  contextParts.push(`[当前关注]\n${working}`);
536
- // 计算 peerKey: <channel>#<urlEncode(peerId)>
597
+ // 计算 peerKey: <channelType>#<urlEncode(peerId)>
537
598
  const peerIdRaw = message.peerId;
538
599
  const peerKey = (currentChannelType && peerIdRaw)
539
- ? `${currentChannelType}#${encodeURIComponent(peerIdRaw)}`
600
+ ? formatPeerKey(currentChannelType, peerIdRaw)
540
601
  : undefined;
602
+ // 按 关系级 > agent级 > 全局 解析本次调用的模型/强度,作为 per-call 入参传入 runQuery。
603
+ // 不缓存、不绑会话——改关系级/agent级后该范围所有会话的下条消息即时生效;
604
+ // 多对端并发各自独立解析、各自传参,无共享状态可被污染。
605
+ let effectiveModel;
606
+ // 取降级状态,按退避策略决定是否跳过 evolclaw 作用域模型
607
+ const fbState = this.modelFallbackMap.get(session.id) ?? {
608
+ failCount: 0, fallbackActive: false,
609
+ messagesSinceFallback: 0, nextProbeAt: 2, hintShown: false,
610
+ };
611
+ // 退避期内递增消息计数,判断是否到探测点
612
+ if (fbState.fallbackActive) {
613
+ fbState.messagesSinceFallback++;
614
+ skipEvolclawModel = fbState.messagesSinceFallback < fbState.nextProbeAt;
615
+ this.modelFallbackMap.set(session.id, fbState);
616
+ }
617
+ // 非跳过时:尝试解析 evolclaw 作用域模型
618
+ let evolclawModelOverride;
619
+ if (!skipEvolclawModel) {
620
+ try {
621
+ const resolved = resolveEffectiveModel({ self: selfAid || undefined, peerKey });
622
+ if (resolved.model) {
623
+ evolclawModelOverride = { model: resolved.model, effort: resolved.effort };
624
+ effectiveModel = resolved.model;
625
+ }
626
+ }
627
+ catch (e) {
628
+ logger.warn(`[MessageProcessor] resolveEffectiveModel failed: ${e instanceof Error ? e.message : String(e)}`);
629
+ }
630
+ modelOverride = evolclawModelOverride;
631
+ }
541
632
  const normalizedBaseagent = normalizeBaseagent(agent.name);
542
- const agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
633
+ agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
543
634
  // Kit renderer: 组装上下文
635
+ const pkgRoot = getPackageRoot();
544
636
  const kitCtx = {
545
637
  vars: {
546
638
  EVOLCLAW_HOME: resolveRoot(),
547
- PACKAGE_ROOT: getPackageRoot(),
639
+ PACKAGE_ROOT: pkgRoot,
548
640
  CURRENT_PROJECT: absoluteProjectPath,
641
+ // ECK 派生路径(manifest 引用时需要展开)
642
+ KITS: path.join(pkgRoot, 'kits'),
643
+ KITS_RULES: path.join(pkgRoot, 'kits', 'rules'),
644
+ KITS_DOCS: path.join(pkgRoot, 'kits', 'docs'),
645
+ KITS_TEMPLATES: path.join(pkgRoot, 'kits', 'templates'),
646
+ KITS_FRAGMENTS: path.join(pkgRoot, 'kits', 'templates', 'system-fragments'),
647
+ // evolclaw 运行模式:dev=源码仓库 | install=全局安装包
648
+ evolclawMode: fs.existsSync(path.join(pkgRoot, 'src', 'index.ts')) ? 'dev' : 'install',
649
+ // 路径变量(用于 manifest 路径展开,resolvePath 用 ctx.vars 取真值)
650
+ PERSONAL_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'personal') : undefined,
651
+ RELATIONS_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'relations') : undefined,
652
+ VENUES_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'venues') : undefined,
549
653
  selfAid: selfAid || undefined,
550
654
  selfName: selfName || undefined,
551
655
  hasPersona: !!persona,
@@ -553,23 +657,39 @@ export class MessageProcessor {
553
657
  peerId: peerIdRaw || undefined,
554
658
  peerKey,
555
659
  peerName: peerName || undefined,
556
- peerRole: session.identity?.role || 'unknown',
660
+ peerRole: session.identity?.role || 'anonymous',
557
661
  peerType: message.peerType || undefined,
662
+ sameDevice: message.sameDevice || undefined,
663
+ sameNetwork: message.sameNetwork || undefined,
664
+ sameEgressIp: message.sameEgressIp || undefined,
558
665
  groupId: session.metadata?.groupId || undefined,
559
666
  chatType: session.chatType || null,
560
667
  channel: currentChannelType || null,
561
668
  venueUid: undefined,
669
+ // 群分发模式 / 客户端类型 / 权限模式
670
+ dispatch: session.metadata?.dispatchMode || undefined,
671
+ clientType: message.clientType || undefined,
672
+ permissionMode: session.metadata?.permissionMode || 'auto',
562
673
  capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
563
674
  project: path.basename(absoluteProjectPath),
564
675
  sessionId: session.id,
565
676
  sessionName: session.name || undefined,
566
677
  sessionCreatedAt: session.createdAt ? new Date(session.createdAt).toISOString() : undefined,
678
+ // 时区(把 ISO 时间戳转本地时间用)+ OS 环境
679
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || undefined,
680
+ tzOffset: currentTzOffset(),
681
+ osInfo: OS_INFO,
567
682
  threadId: session.threadId || undefined,
683
+ // Stage 3: sessionKey 持久化字段
684
+ sessionKey: session.sessionKey,
568
685
  chatMode: isProactive ? 'proactive' : 'interactive',
569
686
  readonly: session.metadata?.permissionMode === 'readonly',
570
687
  baseAgent: normalizedBaseagent.canonical,
571
688
  baseAgentName: normalizedBaseagent.displayName,
572
689
  baseAgentModel: agentModel || undefined,
690
+ effectiveModel: effectiveModel || agentModel || undefined,
691
+ modelFallbackActive: (fbState.fallbackActive || skipEvolclawModel) ? true : undefined,
692
+ modelFallbackModel: (fbState.fallbackActive || skipEvolclawModel) ? (agentModel || undefined) : undefined,
573
693
  agentSessionId: session.agentSessionId || undefined,
574
694
  },
575
695
  sessionId: session.id,
@@ -584,16 +704,36 @@ export class MessageProcessor {
584
704
  let streamRegistered = false;
585
705
  try {
586
706
  logger.info(`[MessageProcessor] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${attempt}/${MAX_RETRIES} agentSessionId=${session.agentSessionId ?? 'none'}`);
587
- const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager);
707
+ const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager, modelOverride);
588
708
  agent.registerStream(streamKey, stream);
589
709
  streamRegistered = true;
590
710
  streamResult = await this.processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress);
711
+ // 探测成功(退避期内到达探测点且用的是 evolclaw 模型)→ 清零降级状态
712
+ if (fbState.fallbackActive && !skipEvolclawModel && !usedFallback) {
713
+ this.modelFallbackMap.delete(session.id);
714
+ logger.info(`[MessageProcessor] Model probe succeeded, cleared fallback state for session=${session.id}`);
715
+ }
591
716
  break; // 成功,跳出重试循环
592
717
  }
593
718
  catch (retryError) {
594
719
  if (streamRegistered) {
595
720
  agent.cleanupStream(streamKey);
596
721
  }
722
+ // 模型不可用:累计计数,本次切换到 baseAgentModel 立即重试,不让用户看到失败
723
+ if (classifyError(retryError) === ErrorType.MODEL_UNAVAILABLE && evolclawModelOverride?.model) {
724
+ fbState.failCount++;
725
+ if (fbState.failCount >= 2) {
726
+ fbState.fallbackActive = true;
727
+ fbState.messagesSinceFallback = 0;
728
+ fbState.nextProbeAt = Math.min(Math.pow(2, fbState.failCount - 1), 8);
729
+ }
730
+ this.modelFallbackMap.set(session.id, fbState);
731
+ logger.warn(`[MessageProcessor] Model unavailable: ${evolclawModelOverride.model}, failCount=${fbState.failCount}, fallbackActive=${fbState.fallbackActive}`);
732
+ // 切换到 baseAgentModel 重试(清除 modelOverride,让 runQuery 使用 this.model)
733
+ modelOverride = undefined;
734
+ usedFallback = true;
735
+ continue;
736
+ }
597
737
  if (attempt < MAX_RETRIES && isRetryableError(retryError)) {
598
738
  const delay = Math.pow(2, attempt) * 1000; // 2s, 4s
599
739
  logger.warn(`[MessageProcessor] Retryable error (attempt ${attempt}/${MAX_RETRIES}), retrying in ${delay}ms:`, retryError);
@@ -613,9 +753,8 @@ export class MessageProcessor {
613
753
  await renderer.flush();
614
754
  const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
615
755
  if (compacted) {
616
- // compact 成功,带 resume 重试(不重复原始消息,让 Agent 继续未完成的工作)
617
756
  renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
618
- const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
757
+ const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride);
619
758
  agent.registerStream(streamKey, retryStream);
620
759
  streamResult = await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress);
621
760
  }
@@ -641,7 +780,7 @@ export class MessageProcessor {
641
780
  const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
642
781
  if (compacted) {
643
782
  renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
644
- const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
783
+ const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride);
645
784
  agent.registerStream(streamKey, retryStream);
646
785
  streamResult = await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress);
647
786
  // 重试后仍然 prompt_too_long:清理 renderer 中可能混入的错误文本,显示友好提示
@@ -651,7 +790,6 @@ export class MessageProcessor {
651
790
  contextTooLongPattern.test(retryErrorsText) ||
652
791
  contextTooLongPattern.test(streamResult.fullText));
653
792
  if (retryStillTooLong) {
654
- renderer.stripContextError(contextTooLongPattern);
655
793
  renderer.addNotice(getContextTooLongHint(agent), 'warn', 'context-too-long', true);
656
794
  }
657
795
  }
@@ -768,6 +906,20 @@ export class MessageProcessor {
768
906
  logger.info(`[MessageProcessor] agent.cleanupStream ok: session=${session.id} task=${taskId}`);
769
907
  this.sessionManager.clearProcessing(session.id);
770
908
  logger.info(`[MessageProcessor] session ${session.id} processing cleared task=${taskId}`);
909
+ // 降级模型回复末尾追加标记(代码层硬注入,不依赖模型输出)
910
+ const usingFallback = usedFallback || (skipEvolclawModel && agentModel != null);
911
+ if (usingFallback && agentModel) {
912
+ const curFbState = this.modelFallbackMap.get(session.id);
913
+ const showHint = curFbState && curFbState.nextProbeAt >= 8 && !curFbState.hintShown;
914
+ const suffix = showHint
915
+ ? `\n\n---\n⚠️ [降级模型: ${agentModel} | 可告诉我"帮我检查可用模型"来诊断]`
916
+ : `\n\n---\n⚠️ [降级模型: ${agentModel}]`;
917
+ renderer.addText(suffix);
918
+ if (showHint && curFbState) {
919
+ curFbState.hintShown = true;
920
+ this.modelFallbackMap.set(session.id, curFbState);
921
+ }
922
+ }
771
923
  // 被用户中断(新消息打断)时跳过 flush — 新 task 已接管渠道,旧 task 的 flush 无意义且可能卡住
772
924
  const preFlushInterrupt = this.interruptedSessions.get(session.id);
773
925
  if (preFlushInterrupt === 'new_message' || preFlushInterrupt === 'stop' || preFlushInterrupt === 'recalled') {
@@ -830,7 +982,7 @@ export class MessageProcessor {
830
982
  adapter.send(envelope, { kind: 'status.interrupted', metadata: { reason: interruptReason } }).catch(() => { });
831
983
  }
832
984
  else {
833
- adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs, numTurns: streamResult.numTurns, usage: streamResult.usage } }).catch(() => { });
985
+ adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs, ttftMs: streamResult.ttftMs, numTurns: streamResult.numTurns, tokenUsage: streamResult.tokenUsage, contextUsage: streamResult.contextUsage } }).catch(() => { });
834
986
  }
835
987
  }
836
988
  if (message.triggerMeta) {
@@ -840,10 +992,6 @@ export class MessageProcessor {
840
992
  else {
841
993
  this.eventBus.publish({ type: 'trigger:completed', triggerId: message.triggerMeta.triggerId, messageId: messageId, durationMs });
842
994
  }
843
- // Clean up autonomous sessions after completion to avoid accumulating orphaned sessions
844
- if (session.sessionMode === 'autonomous') {
845
- this.sessionManager.unbindSession(session.id).catch(() => { });
846
- }
847
995
  }
848
996
  await this.sessionManager.recordSuccess(session.id);
849
997
  this.eventBus.publish({
@@ -953,7 +1101,7 @@ export class MessageProcessor {
953
1101
  // 获取 session 用于话题回复(如果 resolveSession 已执行)
954
1102
  let sendOpts;
955
1103
  try {
956
- await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd(), message.threadId, undefined, undefined, message.peerId, message.chatType, undefined, message.selfId, message.channelType, message.peerType);
1104
+ await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd(), message.threadId, undefined, undefined, message.peerId, message.chatType, undefined, message.selfAID, message.channelType, message.peerType);
957
1105
  sendOpts = this.getReplyContext(message);
958
1106
  }
959
1107
  catch { }
@@ -976,21 +1124,23 @@ export class MessageProcessor {
976
1124
  ? { replyContext: message.replyContext }
977
1125
  : undefined;
978
1126
  const projectPath = this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd();
979
- // --session silent 触发器:新建独立 autonomous 会话,与原会话历史隔离
980
- if (message.triggerMeta?.silent) {
981
- const prevActive = await this.sessionManager.getActiveSession(message.channel, message.channelId);
982
- const session = await this.sessionManager.createNewSession(message.channel, message.channelId, projectPath, `trigger-${message.triggerMeta.triggerId.slice(0, 8)}`);
983
- await this.sessionManager.updateSession(session.id, { sessionMode: 'autonomous' });
984
- session.sessionMode = 'autonomous';
985
- if (prevActive) {
986
- await this.sessionManager.switchToSession(message.channel, message.channelId, prevActive.id);
1127
+ // current strategy: resume bound session, make it active so output is not suppressed
1128
+ if (message.triggerMeta?.boundSessionId) {
1129
+ const bound = await this.sessionManager.getSessionById(message.triggerMeta.boundSessionId);
1130
+ if (bound) {
1131
+ const switched = await this.sessionManager.switchToSession(bound.channel, bound.channelId, bound.id);
1132
+ if (switched) {
1133
+ const absoluteProjectPath = path.isAbsolute(switched.projectPath)
1134
+ ? switched.projectPath : path.resolve(process.cwd(), switched.projectPath);
1135
+ return { session: switched, absoluteProjectPath };
1136
+ }
1137
+ logger.warn(`[MessageProcessor] switchToSession failed for bound session ${bound.id}, falling back to latest`);
1138
+ }
1139
+ else {
1140
+ logger.warn(`[MessageProcessor] Bound session ${message.triggerMeta.boundSessionId} not found, falling back to latest`);
987
1141
  }
988
- const absoluteProjectPath = path.isAbsolute(session.projectPath)
989
- ? session.projectPath
990
- : path.resolve(process.cwd(), session.projectPath);
991
- return { session, absoluteProjectPath };
992
1142
  }
993
- const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId, message.chatType, undefined, message.selfId, message.channelType, message.peerType);
1143
+ 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);
994
1144
  // 兜底纠正1:群聊强制 proactive
995
1145
  if (message.chatType === 'group' && session.sessionMode !== 'proactive') {
996
1146
  logger.info(`[MessageProcessor] group proactive upgrade: sessionId=${session.id} ${session.sessionMode} -> proactive`);
@@ -1004,6 +1154,13 @@ export class MessageProcessor {
1004
1154
  session.sessionMode = 'proactive';
1005
1155
  await this.sessionManager.updateSession(session.id, { sessionMode: 'proactive' });
1006
1156
  }
1157
+ // Proactive→Interactive 模式切换提示:上一轮 proactive 使用了标志位,本轮已切换为 interactive
1158
+ if (session.sessionMode === 'interactive' && session.metadata?.lastProactiveFlag) {
1159
+ message.content = '本轮会话已切换为 interactive 模式,无需调用工具发送消息。\n\n' + message.content;
1160
+ delete session.metadata.lastProactiveFlag;
1161
+ await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
1162
+ logger.info(`[MessageProcessor] Injected interactive mode hint for session ${session.id}`);
1163
+ }
1007
1164
  // replyContext 不再写入 session.metadata(跟着 message 走,避免群聊多人覆盖)
1008
1165
  const absoluteProjectPath = path.isAbsolute(session.projectPath)
1009
1166
  ? session.projectPath
@@ -1018,7 +1175,7 @@ export class MessageProcessor {
1018
1175
  */
1019
1176
  async processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress) {
1020
1177
  // Per-session agent name for stats bucketing
1021
- const agentNameForStats = this.agentRegistry?.resolveByChannel(session.metadata?.channelName || session.channel)?.name ?? '<unknown>';
1178
+ const agentNameForStats = this.agentRegistry?.resolveByChannel(session.metadata?.channelKey || session.channel)?.name ?? '<unknown>';
1022
1179
  let hasReceivedText = false;
1023
1180
  let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
1024
1181
  let completeResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
@@ -1100,7 +1257,7 @@ export class MessageProcessor {
1100
1257
  if (event.type === 'compact') {
1101
1258
  this.eventBus.publish({ type: 'runner:compact-complete', sessionId: session.id, preTokens: event.preTokens });
1102
1259
  if (!shouldSuppress()) {
1103
- renderer.addNotice(`\ud83d\udca1 会话压缩完成,继续执行...(压缩前 tokens: ${event.preTokens})`, 'info', 'compact');
1260
+ renderer.addNotice(`\ud83d\udca1 会话压缩完成,继续执行...)`, 'info', 'compact');
1104
1261
  }
1105
1262
  }
1106
1263
  // 子任务进度
@@ -1178,14 +1335,15 @@ export class MessageProcessor {
1178
1335
  // SDK 可能产生多个 complete 事件(如 subagent 或 auto-compact 二次查询),
1179
1336
  // 仅记录状态,最终 flush(true) 在流结束后统一执行
1180
1337
  if (event.type === 'complete') {
1181
- logger.info(`[MessageProcessor] complete event: isError=${event.isError} terminalReason=${event.terminalReason ?? 'none'} subtype=${event.subtype ?? 'none'} hasReceivedText=${hasReceivedText}`);
1338
+ const isAbort = event.terminalReason === 'aborted_streaming' || event.terminalReason === 'aborted_tools';
1339
+ logger.info(`[MessageProcessor] ${isAbort ? 'task interrupted' : 'complete event'}: isError=${event.isError} terminalReason=${event.terminalReason ?? 'none'} subtype=${event.subtype ?? 'none'} hasReceivedText=${hasReceivedText}`);
1182
1340
  // 自动回填会话名称
1183
1341
  if (event.sessionTitle && session.name === '默认会话') {
1184
1342
  await this.sessionManager.renameSession(session.id, event.sessionTitle);
1185
1343
  logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
1186
1344
  }
1187
1345
  // 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
1188
- completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, usage: event.usage };
1346
+ 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 };
1189
1347
  // thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
1190
1348
  // 由那里按 LLM 输出的每个 text item 单独写一条,此处不再写。
1191
1349
  // 失败且无前置错误输出:显示 errors 摘要
@@ -1213,6 +1371,15 @@ export class MessageProcessor {
1213
1371
  if (renderer.hasContent()) {
1214
1372
  await renderer.flushActivitiesOnly();
1215
1373
  }
1374
+ // 检测 proactive 标志位,设置 lastProactiveFlag 供模式切换提示使用
1375
+ if (session.sessionMode === 'proactive' && lastReplyText) {
1376
+ if (/\[PROACTIVE:REPLY_CONFIRMED_(SENT|NONE)\]/.test(lastReplyText)) {
1377
+ session.metadata = session.metadata || {};
1378
+ session.metadata.lastProactiveFlag = true;
1379
+ await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
1380
+ logger.debug(`[MessageProcessor] Set lastProactiveFlag for session ${session.id}`);
1381
+ }
1382
+ }
1216
1383
  }
1217
1384
  continue;
1218
1385
  }
@@ -1232,7 +1399,7 @@ export class MessageProcessor {
1232
1399
  logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
1233
1400
  }
1234
1401
  // 记录完成状态
1235
- completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, usage: event.usage };
1402
+ 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 };
1236
1403
  if (event.subtype === 'success') {
1237
1404
  this.messageCache.addEvent(session.id, {
1238
1405
  type: 'completed',
@@ -1345,85 +1512,6 @@ export class MessageProcessor {
1345
1512
  // 都找不到,返回项目根目录路径
1346
1513
  return rootPath;
1347
1514
  }
1348
- /**
1349
- * 确保全局数据目录下有最新版本的 SKILLS.md
1350
- * 目标:{EVOLCLAW_HOME}/data/SKILLS.md
1351
- */
1352
- ensureSkillsFile() {
1353
- try {
1354
- const targetDir = path.join(resolveRoot(), 'data');
1355
- const targetPath = path.join(targetDir, 'SKILLS.md');
1356
- const templatePath = path.join(getPackageRoot(), 'src', 'templates', 'skills.md');
1357
- // 模板不存在则跳过(构建环境可能没有 src/)
1358
- if (!fs.existsSync(templatePath)) {
1359
- // 尝试 dist/templates/skills.md
1360
- const distTemplatePath = path.join(getPackageRoot(), 'dist', 'templates', 'skills.md');
1361
- if (!fs.existsSync(distTemplatePath))
1362
- return;
1363
- this.copySkillsIfNeeded(distTemplatePath, targetDir, targetPath);
1364
- return;
1365
- }
1366
- this.copySkillsIfNeeded(templatePath, targetDir, targetPath);
1367
- }
1368
- catch {
1369
- // 静默失败,不影响正常消息处理
1370
- }
1371
- }
1372
- copySkillsIfNeeded(templatePath, targetDir, targetPath) {
1373
- const templateContent = fs.readFileSync(templatePath, 'utf-8');
1374
- const templateVersion = templateContent.match(/^version:\s*(.+)$/m)?.[1]?.trim() || '0';
1375
- if (fs.existsSync(targetPath)) {
1376
- const existing = fs.readFileSync(targetPath, 'utf-8');
1377
- const existingVersion = existing.match(/^version:\s*(.+)$/m)?.[1]?.trim() || '0';
1378
- if (this.compareSemver(existingVersion, templateVersion) >= 0)
1379
- return; // 已是最新
1380
- }
1381
- if (!fs.existsSync(targetDir)) {
1382
- fs.mkdirSync(targetDir, { recursive: true });
1383
- }
1384
- fs.writeFileSync(targetPath, templateContent, 'utf-8');
1385
- }
1386
- /** 简易 semver 比较:支持 "1", "1.0", "1.0.0" 等格式,返回 -1/0/1 */
1387
- compareSemver(a, b) {
1388
- const pa = a.split('.').map(Number);
1389
- const pb = b.split('.').map(Number);
1390
- const len = Math.max(pa.length, pb.length);
1391
- for (let i = 0; i < len; i++) {
1392
- const na = pa[i] || 0;
1393
- const nb = pb[i] || 0;
1394
- if (na !== nb)
1395
- return na > nb ? 1 : -1;
1396
- }
1397
- return 0;
1398
- }
1399
- /**
1400
- * 从 data/SKILLS.md 读取 frontmatter 并生成提示。
1401
- * 不缓存:每次读取保证用户编辑立即生效。
1402
- * 调用前应确保 ensureSkillsFile() 已执行过(首次落盘)。
1403
- */
1404
- getSkillsHint() {
1405
- try {
1406
- const skillsPath = path.join(resolveRoot(), 'data', 'SKILLS.md');
1407
- if (!fs.existsSync(skillsPath))
1408
- return null;
1409
- const content = fs.readFileSync(skillsPath, 'utf-8');
1410
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
1411
- if (!frontmatterMatch)
1412
- return null;
1413
- const fm = frontmatterMatch[1];
1414
- const desc = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() || 'EvolClaw 运行时管理指令';
1415
- const trigger = fm.match(/^trigger:\s*(.+)$/m)?.[1]?.trim() || '';
1416
- const parts = [
1417
- `可通过 Bash 指令管理运行时,${desc}。`,
1418
- trigger ? `触发时机:${trigger}。` : '',
1419
- `完整文档见 ${skillsPath}`,
1420
- ];
1421
- return parts.filter(Boolean).join('');
1422
- }
1423
- catch {
1424
- return null;
1425
- }
1426
- }
1427
1515
  /**
1428
1516
  * 判断文件路径是否为占位符/示例文本
1429
1517
  * 用于过滤大模型在说明文字中误写的 [SEND_FILE:...] 标记
@@ -10,6 +10,7 @@ export class StreamIdleMonitor {
10
10
  state;
11
11
  triggeredLevels = new Set();
12
12
  idleMs;
13
+ paused = false;
13
14
  constructor(idleMs) {
14
15
  this.idleMs = idleMs;
15
16
  this.state = {
@@ -22,6 +23,24 @@ export class StreamIdleMonitor {
22
23
  hasReceivedText: false,
23
24
  };
24
25
  }
26
+ /**
27
+ * 暂停空闲计时(等待用户交互期间调用,如权限确认 / AskUserQuestion / PlanMode)。
28
+ * 暂停期间 check() 始终返回 null,等待时长不计入 idle。
29
+ */
30
+ pause() {
31
+ this.paused = true;
32
+ }
33
+ /**
34
+ * 恢复空闲计时。从恢复时刻重新起算 idle,并清空已触发级别——
35
+ * 用户应答后是一次全新的执行周期,不应继承等待前的 idle 状态。
36
+ */
37
+ resume() {
38
+ if (!this.paused)
39
+ return;
40
+ this.paused = false;
41
+ this.state.lastEventTime = Date.now();
42
+ this.triggeredLevels.clear();
43
+ }
25
44
  /**
26
45
  * 记录 SDK 事件,更新状态并重置空闲计时
27
46
  */
@@ -44,6 +63,8 @@ export class StreamIdleMonitor {
44
63
  * 检查空闲状态,返回 null(未空闲)或分级结果
45
64
  */
46
65
  check() {
66
+ if (this.paused)
67
+ return null;
47
68
  const now = Date.now();
48
69
  const idleDuration = now - this.state.lastEventTime;
49
70
  const notifyThreshold = this.idleMs * NOTIFY_MULTIPLIER;