evolclaw 3.1.5 → 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 (46) hide show
  1. package/CHANGELOG.md +53 -3
  2. package/dist/agents/claude-runner.js +69 -24
  3. package/dist/agents/kit-renderer.js +15 -4
  4. package/dist/aun/aid/agentmd.js +10 -3
  5. package/dist/aun/msg/group.js +2 -2
  6. package/dist/channels/aun.js +98 -12
  7. package/dist/channels/dingtalk.js +1 -1
  8. package/dist/channels/feishu.js +31 -9
  9. package/dist/channels/qqbot.js +1 -1
  10. package/dist/channels/wechat.js +1 -1
  11. package/dist/channels/wecom.js +1 -1
  12. package/dist/cli/agent.js +10 -11
  13. package/dist/cli/bench.js +1 -5
  14. package/dist/cli/help.js +8 -0
  15. package/dist/cli/index.js +91 -128
  16. package/dist/cli/init.js +37 -21
  17. package/dist/cli/link-rules.js +1 -7
  18. package/dist/cli/model.js +231 -6
  19. package/dist/config-store.js +1 -22
  20. package/dist/core/command-handler.js +181 -48
  21. package/dist/core/evolagent.js +0 -18
  22. package/dist/core/message/im-renderer.js +9 -20
  23. package/dist/core/message/message-bridge.js +7 -3
  24. package/dist/core/message/message-processor.js +138 -35
  25. package/dist/core/relation/peer-identity.js +23 -11
  26. package/dist/core/trigger/parser.js +4 -4
  27. package/dist/core/trigger/scheduler.js +20 -6
  28. package/dist/index.js +55 -5
  29. package/dist/ipc.js +1 -1
  30. package/dist/utils/error-utils.js +6 -0
  31. package/dist/utils/process-introspect.js +7 -5
  32. package/kits/docs/INDEX.md +4 -8
  33. package/kits/docs/context-assembly.md +1 -0
  34. package/kits/docs/evolclaw/INDEX.md +43 -0
  35. package/kits/docs/evolclaw/group.md +13 -6
  36. package/kits/docs/evolclaw/model.md +51 -0
  37. package/kits/docs/evolclaw/msg.md +5 -0
  38. package/kits/docs/venues/group.md +13 -1
  39. package/kits/eck_manifest.json +9 -0
  40. package/kits/rules/06-channel.md +5 -1
  41. package/kits/templates/system-fragments/baseagent.md +7 -1
  42. package/kits/templates/system-fragments/channel.md +7 -5
  43. package/kits/templates/system-fragments/commands.md +19 -0
  44. package/kits/templates/system-fragments/session.md +9 -0
  45. package/kits/templates/system-fragments/venue.md +15 -0
  46. package/package.json +3 -3
@@ -1,5 +1,6 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs';
3
+ import os from 'os';
3
4
  import crypto from 'crypto';
4
5
  import { hasCompact } from '../../agents/claude-runner.js';
5
6
  import { IMRenderer } from './im-renderer.js';
@@ -14,6 +15,22 @@ import { normalizeBaseagent } from '../../agents/baseagent-normalize.js';
14
15
  import { renderActionAsText, renderCommandCardAsText } from '../interaction-router.js';
15
16
  import { formatPeerKey } from '../relation/peer-key.js';
16
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
+ }
17
34
  function getContextTooLongHint(agent) {
18
35
  if (canCompactAgent(agent)) {
19
36
  return '上下文过长,请精简提问或使用 /compact 压缩上下文';
@@ -69,6 +86,8 @@ export class MessageProcessor {
69
86
  agentMap;
70
87
  primaryRunnerKey;
71
88
  interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
89
+ /** sessionId → 模型降级状态(带退避探测,进程重启清零) */
90
+ modelFallbackMap = new Map();
72
91
  interactionRouter;
73
92
  messageQueue;
74
93
  /** sessionId → 活跃的空闲监控器,用于等待用户交互期间暂停/恢复计时 */
@@ -208,15 +227,15 @@ export class MessageProcessor {
208
227
  }
209
228
  // 命令前缀列表(与 CommandHandler.quickCommandPrefixes 保持同步)
210
229
  static COMMAND_PREFIXES = [
211
- '/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart',
230
+ '/new', '/pwd', '/help', '/status', '/restart',
212
231
  '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
213
232
  '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check',
214
- '/p ', '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/chatmode',
233
+ '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/chatmode',
215
234
  '/aid', '/upgrade', '/evolagent',
216
235
  ];
217
236
  /** 判断消息内容是否为已知命令 */
218
237
  isKnownCommand(content) {
219
- return content === '/p' || content === '/s' ||
238
+ return content === '/s' ||
220
239
  MessageProcessor.COMMAND_PREFIXES.some(cmd => content.startsWith(cmd));
221
240
  }
222
241
  /**
@@ -227,6 +246,29 @@ export class MessageProcessor {
227
246
  // 先解析会话,再优先用 session.metadata.channelKey 精确定位实例级 adapter
228
247
  // message.channel 现在存实例名(channelName),可直接用于精确路由
229
248
  const { session, absoluteProjectPath } = await this.resolveSession(message);
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
+ }
230
272
  const channelKey = session.metadata?.channelKey || message.channel;
231
273
  const channelInfo = this.resolveChannelInfo(channelKey);
232
274
  if (!channelInfo) {
@@ -388,7 +430,7 @@ export class MessageProcessor {
388
430
  };
389
431
  };
390
432
  const isProactive = session.sessionMode === 'proactive';
391
- const isAutonomous = session.sessionMode === 'autonomous' || message.triggerMeta?.silent === true;
433
+ const isAutonomous = session.sessionMode === 'autonomous';
392
434
  const envelope = buildEnvelope({
393
435
  taskId,
394
436
  channel: message.channel,
@@ -524,6 +566,9 @@ export class MessageProcessor {
524
566
  let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
525
567
  let effectiveSystemPrompt;
526
568
  let modelOverride;
569
+ let usedFallback = false;
570
+ let skipEvolclawModel = false;
571
+ let agentModel;
527
572
  try {
528
573
  // 动态构建运行时上下文提示
529
574
  const contextParts = [];
@@ -557,16 +602,35 @@ export class MessageProcessor {
557
602
  // 按 关系级 > agent级 > 全局 解析本次调用的模型/强度,作为 per-call 入参传入 runQuery。
558
603
  // 不缓存、不绑会话——改关系级/agent级后该范围所有会话的下条消息即时生效;
559
604
  // 多对端并发各自独立解析、各自传参,无共享状态可被污染。
560
- try {
561
- const resolved = resolveEffectiveModel({ self: selfAid || undefined, peerKey });
562
- if (resolved.model)
563
- modelOverride = { model: resolved.model, effort: resolved.effort };
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);
564
616
  }
565
- catch (e) {
566
- logger.warn(`[MessageProcessor] resolveEffectiveModel failed: ${e instanceof Error ? e.message : String(e)}`);
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;
567
631
  }
568
632
  const normalizedBaseagent = normalizeBaseagent(agent.name);
569
- const agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
633
+ agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
570
634
  // Kit renderer: 组装上下文
571
635
  const pkgRoot = getPackageRoot();
572
636
  const kitCtx = {
@@ -580,6 +644,8 @@ export class MessageProcessor {
580
644
  KITS_DOCS: path.join(pkgRoot, 'kits', 'docs'),
581
645
  KITS_TEMPLATES: path.join(pkgRoot, 'kits', 'templates'),
582
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',
583
649
  // 路径变量(用于 manifest 路径展开,resolvePath 用 ctx.vars 取真值)
584
650
  PERSONAL_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'personal') : undefined,
585
651
  RELATIONS_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'relations') : undefined,
@@ -593,6 +659,9 @@ export class MessageProcessor {
593
659
  peerName: peerName || undefined,
594
660
  peerRole: session.identity?.role || 'anonymous',
595
661
  peerType: message.peerType || undefined,
662
+ sameDevice: message.sameDevice || undefined,
663
+ sameNetwork: message.sameNetwork || undefined,
664
+ sameEgressIp: message.sameEgressIp || undefined,
596
665
  groupId: session.metadata?.groupId || undefined,
597
666
  chatType: session.chatType || null,
598
667
  channel: currentChannelType || null,
@@ -606,6 +675,10 @@ export class MessageProcessor {
606
675
  sessionId: session.id,
607
676
  sessionName: session.name || undefined,
608
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,
609
682
  threadId: session.threadId || undefined,
610
683
  // Stage 3: sessionKey 持久化字段
611
684
  sessionKey: session.sessionKey,
@@ -614,6 +687,9 @@ export class MessageProcessor {
614
687
  baseAgent: normalizedBaseagent.canonical,
615
688
  baseAgentName: normalizedBaseagent.displayName,
616
689
  baseAgentModel: agentModel || undefined,
690
+ effectiveModel: effectiveModel || agentModel || undefined,
691
+ modelFallbackActive: (fbState.fallbackActive || skipEvolclawModel) ? true : undefined,
692
+ modelFallbackModel: (fbState.fallbackActive || skipEvolclawModel) ? (agentModel || undefined) : undefined,
617
693
  agentSessionId: session.agentSessionId || undefined,
618
694
  },
619
695
  sessionId: session.id,
@@ -632,12 +708,32 @@ export class MessageProcessor {
632
708
  agent.registerStream(streamKey, stream);
633
709
  streamRegistered = true;
634
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
+ }
635
716
  break; // 成功,跳出重试循环
636
717
  }
637
718
  catch (retryError) {
638
719
  if (streamRegistered) {
639
720
  agent.cleanupStream(streamKey);
640
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
+ }
641
737
  if (attempt < MAX_RETRIES && isRetryableError(retryError)) {
642
738
  const delay = Math.pow(2, attempt) * 1000; // 2s, 4s
643
739
  logger.warn(`[MessageProcessor] Retryable error (attempt ${attempt}/${MAX_RETRIES}), retrying in ${delay}ms:`, retryError);
@@ -657,9 +753,6 @@ export class MessageProcessor {
657
753
  await renderer.flush();
658
754
  const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
659
755
  if (compacted) {
660
- // compact 成功,清除第一次流中混入的错误文本,再重试
661
- const ctxErrPattern = /prompt is too long|input is too long|上下文过长/i;
662
- renderer.stripContextError(ctxErrPattern);
663
756
  renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
664
757
  const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride);
665
758
  agent.registerStream(streamKey, retryStream);
@@ -682,7 +775,6 @@ export class MessageProcessor {
682
775
  contextTooLongPattern.test(errorsText) ||
683
776
  contextTooLongPattern.test(streamResult.fullText));
684
777
  if (isPromptTooLong) {
685
- renderer.stripContextError(contextTooLongPattern);
686
778
  renderer.addNotice('上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
687
779
  await renderer.flush();
688
780
  const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
@@ -698,7 +790,6 @@ export class MessageProcessor {
698
790
  contextTooLongPattern.test(retryErrorsText) ||
699
791
  contextTooLongPattern.test(streamResult.fullText));
700
792
  if (retryStillTooLong) {
701
- renderer.stripContextError(contextTooLongPattern);
702
793
  renderer.addNotice(getContextTooLongHint(agent), 'warn', 'context-too-long', true);
703
794
  }
704
795
  }
@@ -815,6 +906,20 @@ export class MessageProcessor {
815
906
  logger.info(`[MessageProcessor] agent.cleanupStream ok: session=${session.id} task=${taskId}`);
816
907
  this.sessionManager.clearProcessing(session.id);
817
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
+ }
818
923
  // 被用户中断(新消息打断)时跳过 flush — 新 task 已接管渠道,旧 task 的 flush 无意义且可能卡住
819
924
  const preFlushInterrupt = this.interruptedSessions.get(session.id);
820
925
  if (preFlushInterrupt === 'new_message' || preFlushInterrupt === 'stop' || preFlushInterrupt === 'recalled') {
@@ -877,7 +982,7 @@ export class MessageProcessor {
877
982
  adapter.send(envelope, { kind: 'status.interrupted', metadata: { reason: interruptReason } }).catch(() => { });
878
983
  }
879
984
  else {
880
- adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs, ttftMs: streamResult.ttftMs, 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(() => { });
881
986
  }
882
987
  }
883
988
  if (message.triggerMeta) {
@@ -887,10 +992,6 @@ export class MessageProcessor {
887
992
  else {
888
993
  this.eventBus.publish({ type: 'trigger:completed', triggerId: message.triggerMeta.triggerId, messageId: messageId, durationMs });
889
994
  }
890
- // Clean up autonomous sessions after completion to avoid accumulating orphaned sessions
891
- if (session.sessionMode === 'autonomous') {
892
- this.sessionManager.unbindSession(session.id).catch(() => { });
893
- }
894
995
  }
895
996
  await this.sessionManager.recordSuccess(session.id);
896
997
  this.eventBus.publish({
@@ -1023,19 +1124,21 @@ export class MessageProcessor {
1023
1124
  ? { replyContext: message.replyContext }
1024
1125
  : undefined;
1025
1126
  const projectPath = this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd();
1026
- // --session silent 触发器:新建独立 autonomous 会话,与原会话历史隔离
1027
- if (message.triggerMeta?.silent) {
1028
- const prevActive = await this.sessionManager.getActiveSession(message.channel, message.channelId);
1029
- const session = await this.sessionManager.createNewSession(message.channel, message.channelId, projectPath, `trigger-${message.triggerMeta.triggerId.slice(0, 8)}`);
1030
- await this.sessionManager.updateSession(session.id, { sessionMode: 'autonomous' });
1031
- session.sessionMode = 'autonomous';
1032
- if (prevActive) {
1033
- await this.sessionManager.switchToSession(message.channel, message.channelId, prevActive.id);
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`);
1034
1141
  }
1035
- const absoluteProjectPath = path.isAbsolute(session.projectPath)
1036
- ? session.projectPath
1037
- : path.resolve(process.cwd(), session.projectPath);
1038
- return { session, absoluteProjectPath };
1039
1142
  }
1040
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);
1041
1144
  // 兜底纠正1:群聊强制 proactive
@@ -1240,7 +1343,7 @@ export class MessageProcessor {
1240
1343
  logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
1241
1344
  }
1242
1345
  // 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
1243
- completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, usage: event.usage };
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 };
1244
1347
  // thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
1245
1348
  // 由那里按 LLM 输出的每个 text item 单独写一条,此处不再写。
1246
1349
  // 失败且无前置错误输出:显示 errors 摘要
@@ -1296,7 +1399,7 @@ export class MessageProcessor {
1296
1399
  logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
1297
1400
  }
1298
1401
  // 记录完成状态
1299
- completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, usage: event.usage };
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 };
1300
1403
  if (event.subtype === 'success') {
1301
1404
  this.messageCache.addEvent(session.id, {
1302
1405
  type: 'completed',
@@ -56,8 +56,10 @@ export class PeerIdentityCache {
56
56
  }
57
57
  /**
58
58
  * 从 agent.md 更新身份信息
59
+ * @param source 'agentmd'(验签通过)或 'agentmd-unverified'(内容可解析但验签未过)
60
+ * @param verifiedAt 验签通过时间戳;未验签传 0
59
61
  */
60
- static updateFromAgentMd(channelType, peerId, agentDir, agentMd, verifiedAt) {
62
+ static updateFromAgentMd(channelType, peerId, agentDir, agentMd, verifiedAt, source = 'agentmd') {
61
63
  const typeMatch = agentMd.match(/^type:\s*["']?([^"'\n]+?)["']?\s*$/m);
62
64
  const nameMatch = agentMd.match(/^name:\s*["']?(.+?)["']?\s*$/m);
63
65
  const type = typeMatch?.[1] || 'unknown';
@@ -74,13 +76,13 @@ export class PeerIdentityCache {
74
76
  agentMdUpdatedAt: now,
75
77
  verifiedAt,
76
78
  lastCheckedAt: now,
77
- source: 'agentmd',
79
+ source,
78
80
  };
79
81
  const filePath = this.getFilePath(channelType, peerId, agentDir);
80
82
  try {
81
83
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
82
84
  fs.writeFileSync(filePath, JSON.stringify(identity, null, 2), 'utf-8');
83
- logger.debug(`[PeerIdentityCache] Updated: ${channelType}#${peerId} type=${type} isAgent=${isAgent}`);
85
+ logger.debug(`[PeerIdentityCache] Updated: ${channelType}#${peerId} type=${type} isAgent=${isAgent} source=${source}`);
84
86
  }
85
87
  catch (err) {
86
88
  logger.warn(`[PeerIdentityCache] Failed to write cache: ${filePath} err=${err}`);
@@ -151,27 +153,37 @@ export class PeerIdentityCache {
151
153
  if (!content) {
152
154
  throw new Error('agent.md content unavailable');
153
155
  }
154
- // 3. 比较 hash,仅在变化时重写 peer-identity.json
156
+ // 验签通过 可信(source=agentmd);否则 type 仍解析但标记未验证
157
+ const verified = result.verification?.status === 'verified';
158
+ const source = verified ? 'agentmd' : 'agentmd-unverified';
159
+ if (!verified) {
160
+ logger.info(`[PeerIdentityCache] agent.md unverified for ${peerId}: status=${result.verification?.status ?? 'unknown'} reason=${result.verification?.reason ?? '-'} (type 仍按声明解析)`);
161
+ }
162
+ // 3. 比较 hash,内容与可信级别都未变时仅 touch
155
163
  const newHash = 'sha256:' + crypto.createHash('sha256').update(content, 'utf-8').digest('hex');
156
164
  const cached = this.get(channelType, peerId, agentDir);
157
- if (cached && cached.agentMdHash === newHash && cached.source === 'agentmd') {
165
+ if (cached && cached.agentMdHash === newHash && cached.source === source) {
158
166
  return this.touchLastChecked(channelType, peerId, agentDir, cached);
159
167
  }
160
- return this.updateFromAgentMd(channelType, peerId, agentDir, content, Date.now());
168
+ return this.updateFromAgentMd(channelType, peerId, agentDir, content, verified ? Date.now() : 0, source);
161
169
  }
162
170
  catch (err) {
163
- // 4. 网络失败,fallback 本地文件
171
+ // 4. agentmdSync 抛错(非网络失败——网络失败时它内部已 fallback 返回本地内容;
172
+ // 这里通常是无内容或解析异常)。兜底读本地文件,但无法重新验签,
173
+ // 故沿用缓存里已有的可信级别,绝不凭空升级为已验签。
164
174
  const localPath = agentMdPath(peerId);
165
175
  try {
166
176
  if (fs.existsSync(localPath)) {
167
177
  const localContent = fs.readFileSync(localPath, 'utf-8');
168
- logger.info(`[PeerIdentityCache] Network failed, using local agent.md for ${peerId}`);
169
- const localHash = 'sha256:' + crypto.createHash('sha256').update(localContent, 'utf-8').digest('hex');
170
178
  const cached = this.get(channelType, peerId, agentDir);
171
- if (cached && cached.agentMdHash === localHash && cached.source === 'agentmd') {
179
+ logger.info(`[PeerIdentityCache] Using local agent.md for ${peerId} (cached source=${cached?.source ?? 'none'})`);
180
+ const localHash = 'sha256:' + crypto.createHash('sha256').update(localContent, 'utf-8').digest('hex');
181
+ if (cached && cached.agentMdHash === localHash && (cached.source === 'agentmd' || cached.source === 'agentmd-unverified')) {
172
182
  return this.touchLastChecked(channelType, peerId, agentDir, cached);
173
183
  }
174
- return this.updateFromAgentMd(channelType, peerId, agentDir, localContent, cached?.verifiedAt ?? 0);
184
+ // 无匹配缓存可信级别 本地内容未经本次验签,标记为未验证
185
+ const fallbackSource = cached?.source === 'agentmd' ? 'agentmd' : 'agentmd-unverified';
186
+ return this.updateFromAgentMd(channelType, peerId, agentDir, localContent, cached?.verifiedAt ?? 0, fallbackSource);
175
187
  }
176
188
  }
177
189
  catch { /* ignore fs errors */ }
@@ -127,8 +127,8 @@ export function parseTriggerUpdate(args) {
127
127
  }
128
128
  if (flags.has('session')) {
129
129
  const sv = flags.get('session');
130
- if (sv !== 'latest' && sv !== 'silent')
131
- return { ok: false, error: '--session 只接受 latest 或 silent' };
130
+ if (sv !== 'latest' && sv !== 'current' && sv !== 'thread')
131
+ return { ok: false, error: '--session 只接受 latest、currentthread' };
132
132
  result.targetSessionStrategy = sv;
133
133
  }
134
134
  if (flags.has('agent')) {
@@ -216,8 +216,8 @@ export function parseTriggerSet(args) {
216
216
  let targetSessionStrategy = 'latest';
217
217
  if (hasSession) {
218
218
  const sv = flags.get('session');
219
- if (sv !== 'latest' && sv !== 'silent') {
220
- return { ok: false, error: '--session 只接受 latest 或 silent' };
219
+ if (sv !== 'latest' && sv !== 'current' && sv !== 'thread') {
220
+ return { ok: false, error: '--session 只接受 latest、currentthread' };
221
221
  }
222
222
  targetSessionStrategy = sv;
223
223
  }
@@ -209,12 +209,12 @@ export class TriggerScheduler {
209
209
  this.inflightCron.delete(triggerId);
210
210
  }
211
211
  buildSyntheticMessage(trigger, messageId) {
212
- return {
212
+ const base = {
213
213
  channel: trigger.targetChannel,
214
214
  channelType: trigger.targetChannelType,
215
215
  channelId: trigger.targetChannelId,
216
216
  selfAID: this.aid,
217
- threadId: trigger.targetThreadId ?? '',
217
+ threadId: '',
218
218
  agentId: trigger.agentId,
219
219
  chatType: 'private',
220
220
  peerId: `__trigger__:${trigger.id}`, // unique per trigger to prevent greedy merge
@@ -222,10 +222,24 @@ export class TriggerScheduler {
222
222
  messageId,
223
223
  timestamp: Date.now(),
224
224
  source: 'trigger',
225
- triggerMeta: {
226
- triggerId: trigger.id,
227
- silent: trigger.targetSessionStrategy === 'silent',
228
- },
229
225
  };
226
+ if (trigger.targetSessionStrategy === 'current') {
227
+ base.triggerMeta = { triggerId: trigger.id, boundSessionId: trigger.boundSessionId };
228
+ }
229
+ else if (trigger.targetSessionStrategy === 'thread') {
230
+ if (trigger.threadKind === 'feishu' && trigger.pendingThread) {
231
+ base.triggerMeta = { triggerId: trigger.id, pendingThread: true, rootMessageId: trigger.rootMessageId };
232
+ // threadId intentionally empty — first fire builds the thread via reply_in_thread
233
+ }
234
+ else {
235
+ base.threadId = trigger.targetThreadId ?? '';
236
+ base.triggerMeta = { triggerId: trigger.id };
237
+ }
238
+ }
239
+ else {
240
+ // latest
241
+ base.triggerMeta = { triggerId: trigger.id };
242
+ }
243
+ return base;
230
244
  }
231
245
  }
package/dist/index.js CHANGED
@@ -452,11 +452,58 @@ async function main() {
452
452
  continue;
453
453
  const scheduler = agent.triggerScheduler;
454
454
  const primaryProjectPath = agent.config.projects?.defaultPath ?? primaryAgent.projectPath;
455
+ function scheduleRetryWhenIdle(boundId, msg, trigger) {
456
+ let done = false;
457
+ const handler = (ev) => {
458
+ if (ev.sessionId !== boundId || done)
459
+ return;
460
+ done = true;
461
+ clearTimeout(timer);
462
+ eventBus.unsubscribe('task:completed', handler);
463
+ retry();
464
+ };
465
+ const timer = setTimeout(() => {
466
+ if (done)
467
+ return;
468
+ done = true;
469
+ eventBus.unsubscribe('task:completed', handler);
470
+ retry();
471
+ }, 30_000);
472
+ eventBus.subscribe('task:completed', handler);
473
+ function retry() {
474
+ if (messageQueue.isProcessing(boundId)) {
475
+ scheduleRetryWhenIdle(boundId, msg, trigger);
476
+ return;
477
+ }
478
+ sessionManager.getSessionById(boundId).then(bound => {
479
+ if (!bound) {
480
+ logger.warn(`[Trigger] Bound session ${boundId} deleted, aborting`);
481
+ return;
482
+ }
483
+ messageQueue.enqueue(boundId, msg, bound.projectPath, { interruptible: false })
484
+ .catch(err => logger.error(`[Trigger] Retry failed ${trigger.id}: ${err}`));
485
+ });
486
+ }
487
+ }
455
488
  scheduler.setFireCallback((msg, trigger) => {
456
- const sessionKey = `${msg.channel}:${msg.channelId}`;
457
- messageQueue.enqueue(sessionKey, msg, primaryProjectPath, { interruptible: false }).catch(err => {
458
- logger.error(`[Trigger] Failed to enqueue trigger ${trigger.id}: ${err}`);
459
- });
489
+ if (trigger.targetSessionStrategy === 'current' && trigger.boundSessionId) {
490
+ const boundId = trigger.boundSessionId;
491
+ if (messageQueue.isProcessing(boundId)) {
492
+ scheduleRetryWhenIdle(boundId, msg, trigger);
493
+ return;
494
+ }
495
+ sessionManager.getSessionById(boundId).then(bound => {
496
+ if (!bound) {
497
+ logger.warn(`[Trigger] Bound session ${boundId} not found`);
498
+ return;
499
+ }
500
+ messageQueue.enqueue(boundId, msg, bound.projectPath, { interruptible: false })
501
+ .catch(err => logger.error(`[Trigger] Enqueue failed ${trigger.id}: ${err}`));
502
+ });
503
+ return;
504
+ }
505
+ messageQueue.enqueue(`${msg.channel}:${msg.channelId}`, msg, primaryProjectPath, { interruptible: false })
506
+ .catch(err => logger.error(`[Trigger] Enqueue failed ${trigger.id}: ${err}`));
460
507
  });
461
508
  // Subscribe to trigger:completed/failed/skipped to update cron inflight state
462
509
  eventBus.subscribe('trigger:completed', (ev) => scheduler.onTriggerComplete(ev.triggerId, 'completed'));
@@ -504,7 +551,7 @@ async function main() {
504
551
  nextFireAt: calcNextFireAt('cron', cronExpr),
505
552
  targetChannel: firstChannel,
506
553
  targetChannelId: '__system__',
507
- targetSessionStrategy: 'silent',
554
+ targetSessionStrategy: 'latest',
508
555
  prompt: '检查 evolclaw 是否有新版本可用。执行 `npm view evolclaw version` 获取最新版本,与当前版本(执行 `evolclaw --version`)对比。如果有新版本,执行 /restart 进行升级。如果已是最新版本,无需任何操作。',
509
556
  createdByPeerId: '__system__',
510
557
  createdByChannel: '__system__',
@@ -756,8 +803,11 @@ async function main() {
756
803
  continue;
757
804
  }
758
805
  logger.info(`[Resume] Resuming session: ${session.id} (agent: ${evolName}::${baseagentName})`);
806
+ const parsedResumeKey = tryParseChannelKey(session.channel);
759
807
  const resumeMessage = {
760
808
  channel: session.channel,
809
+ channelType: session.channelType || parsedResumeKey?.type,
810
+ selfAID: parsedResumeKey?.selfAID,
761
811
  channelId: session.channelId,
762
812
  content: '服务已重启,请继续之前未完成的任务。',
763
813
  timestamp: Date.now(),
package/dist/ipc.js CHANGED
@@ -93,7 +93,7 @@ export class IpcServer {
93
93
  case 'status':
94
94
  return this.getStatus();
95
95
  case 'ping':
96
- return { pong: true, pid: process.pid };
96
+ return { pong: true, pid: process.pid, protocolVersion: 1 };
97
97
  case 'aun-aids': {
98
98
  const aids = this.aunAidProvider ? this.aunAidProvider() : [];
99
99
  return { ok: true, aids };
@@ -10,6 +10,7 @@ export var ErrorType;
10
10
  ErrorType["FILE_CORRUPT"] = "file_corrupt";
11
11
  ErrorType["STREAM_ERROR"] = "stream_error";
12
12
  ErrorType["CONTEXT_TOO_LONG"] = "context_too_long";
13
+ ErrorType["MODEL_UNAVAILABLE"] = "model_unavailable";
13
14
  ErrorType["UNKNOWN"] = "unknown";
14
15
  })(ErrorType || (ErrorType = {}));
15
16
  /**
@@ -210,6 +211,11 @@ export function classifyError(error) {
210
211
  || msg.includes('上下文过长')) {
211
212
  return ErrorType.CONTEXT_TOO_LONG;
212
213
  }
214
+ if (msg.includes('invalid_model') || msg.includes('model_not_found')
215
+ || msg.includes('no such model') || msg.includes('unknown model')
216
+ || /api error: 404\b/.test(msg)) {
217
+ return ErrorType.MODEL_UNAVAILABLE;
218
+ }
213
219
  if (msg.includes('401') || msg.includes('authentication_error')) {
214
220
  return ErrorType.AUTH_ERROR;
215
221
  }
@@ -51,18 +51,20 @@ function getStartTimeLinux(pid) {
51
51
  const starttimeJiffies = parseInt(fields[19], 10);
52
52
  if (isNaN(starttimeJiffies))
53
53
  return null;
54
- let uptimeSec;
54
+ let btimeSec;
55
55
  try {
56
- uptimeSec = parseFloat(fs.readFileSync('/proc/uptime', 'utf-8').split(' ')[0]);
56
+ const m = fs.readFileSync('/proc/stat', 'utf-8').match(/^btime (\d+)/m);
57
+ if (!m)
58
+ return null;
59
+ btimeSec = parseInt(m[1], 10);
57
60
  }
58
61
  catch {
59
62
  return null;
60
63
  }
61
- if (isNaN(uptimeSec))
64
+ if (isNaN(btimeSec))
62
65
  return null;
63
66
  const clkTck = 100;
64
- const bootTimeMs = Date.now() - uptimeSec * 1000;
65
- return bootTimeMs + (starttimeJiffies / clkTck) * 1000;
67
+ return (btimeSec + starttimeJiffies / clkTck) * 1000;
66
68
  }
67
69
  // ── macOS ──
68
70
  function getStartTimeMacOS(pid) {