evolclaw 3.3.0 → 3.4.0
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.
- package/CHANGELOG.md +36 -0
- package/README.md +7 -3
- package/dist/agents/claude-runner.js +23 -27
- package/dist/agents/codex-runner.js +90 -6
- package/dist/agents/runner-types.js +30 -0
- package/dist/aun/outbox.js +14 -2
- package/dist/channels/aun.js +506 -108
- package/dist/channels/feishu.js +29 -5
- package/dist/cli/agent-command.js +591 -0
- package/dist/cli/agent.js +15 -3
- package/dist/cli/aun-commands.js +1444 -0
- package/dist/cli/ctl-command.js +78 -0
- package/dist/cli/daemon-commands.js +2707 -0
- package/dist/cli/index.js +12 -5027
- package/dist/cli/restart-monitor.js +539 -0
- package/dist/cli/watch-logs.js +33 -0
- package/dist/core/channel-loader.js +4 -1
- package/dist/core/command/command-handler.js +1189 -0
- package/dist/core/command/menu-handler.js +1478 -0
- package/dist/core/command/slash-gate.js +142 -0
- package/dist/core/command/slash-handler.js +2090 -0
- package/dist/core/evolagent-registry.js +81 -0
- package/dist/core/evolagent.js +16 -0
- package/dist/core/message/im-renderer.js +67 -49
- package/dist/core/message/message-bridge.js +30 -9
- package/dist/core/message/message-processor.js +200 -122
- package/dist/core/message/message-queue.js +68 -0
- package/dist/core/permission.js +16 -0
- package/dist/core/session/session-manager.js +59 -13
- package/dist/core/stats/db.js +20 -0
- package/dist/core/stats/writer.js +3 -3
- package/dist/data/error-dict.json +7 -0
- package/dist/index.js +49 -6
- package/dist/ipc.js +99 -0
- package/dist/utils/cross-platform.js +35 -0
- package/dist/utils/ecweb-launch.js +49 -0
- package/dist/utils/error-utils.js +18 -5
- package/dist/utils/npm-ops.js +38 -8
- package/dist/utils/stats.js +63 -6
- package/kits/eck_manifest.json +0 -12
- package/package.json +2 -3
- package/dist/core/command-handler.js +0 -4235
- package/dist/core/message/response-depth.js +0 -56
- package/kits/templates/system-fragments/response-depth.md +0 -16
|
@@ -2,18 +2,17 @@ import path from 'path';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import crypto from 'crypto';
|
|
5
|
-
import { hasCompact } from '../../agents/runner-types.js';
|
|
5
|
+
import { hasCompact, autoCompactWindowForModel, isClaudeContextUsageModel } from '../../agents/runner-types.js';
|
|
6
6
|
import { IMRenderer } from './im-renderer.js';
|
|
7
7
|
import { StreamIdleMonitor } from './stream-idle-monitor.js';
|
|
8
8
|
import { logger } from '../../utils/logger.js';
|
|
9
|
-
import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, prefixErrorType, isRetryableError } from '../../utils/error-utils.js';
|
|
9
|
+
import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, prefixErrorType, isRetryableError, isContextTooLongText } from '../../utils/error-utils.js';
|
|
10
10
|
import { summarizeToolInput } from '../permission.js';
|
|
11
11
|
import { DEFAULT_PERMISSION_MODE } from '../../types.js';
|
|
12
12
|
import { getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
|
|
13
13
|
import { renderKitSections } from '../../eck/kit-renderer.js';
|
|
14
14
|
import { renderMessageBody } from '../../eck/message-renderer.js';
|
|
15
15
|
import { consumeHints, hintsToSubMessages, composeHintFallback } from './pending-hints.js';
|
|
16
|
-
import { resolveResponseDepth as computeResponseDepth } from './response-depth.js';
|
|
17
16
|
import { normalizeBaseagent } from '../../agents/baseagent.js';
|
|
18
17
|
import { renderActionAsText, renderCommandCardAsText } from '../interaction-router.js';
|
|
19
18
|
import { formatPeerKey } from '../relation/peer-identity.js';
|
|
@@ -66,6 +65,11 @@ function getContextCompactFailedHint(agent) {
|
|
|
66
65
|
function canCompactAgent(agent) {
|
|
67
66
|
return hasCompact(agent) && agent.capabilities?.compact !== false;
|
|
68
67
|
}
|
|
68
|
+
function autoCompactTokensFromMaxTokens(maxTokens) {
|
|
69
|
+
if (!maxTokens || maxTokens <= 0)
|
|
70
|
+
return undefined;
|
|
71
|
+
return maxTokens >= 1000000 ? maxTokens - 100000 : maxTokens;
|
|
72
|
+
}
|
|
69
73
|
/**
|
|
70
74
|
* 构造 OutboundEnvelope —— 出站三件套的信封部分。
|
|
71
75
|
*
|
|
@@ -187,6 +191,12 @@ export class MessageProcessor {
|
|
|
187
191
|
setAgentRegistry(registry) {
|
|
188
192
|
this.agentRegistry = registry;
|
|
189
193
|
}
|
|
194
|
+
/** 更新 EvolAgent.lastActivity —— 每次发出 status.* 事件(含 progress)时调用 */
|
|
195
|
+
touchAgentActivity(channelKey) {
|
|
196
|
+
const owning = this.agentRegistry?.resolveByChannel(channelKey);
|
|
197
|
+
if (owning)
|
|
198
|
+
owning.lastActivity = Date.now();
|
|
199
|
+
}
|
|
190
200
|
getAgentContext(channelName, chatType) {
|
|
191
201
|
if (!this.agentRegistry)
|
|
192
202
|
return null;
|
|
@@ -236,6 +246,25 @@ export class MessageProcessor {
|
|
|
236
246
|
this.channelTypeMap.set(type, adapter.channelName);
|
|
237
247
|
}
|
|
238
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* 注销渠道适配器(热重载断开渠道时调用,避免遗留死实例)。
|
|
251
|
+
* channelTypeMap 若指向被删实例,重定向到同类型的另一存活实例(无则删除映射)。
|
|
252
|
+
*/
|
|
253
|
+
unregisterChannel(channelName) {
|
|
254
|
+
const info = this.channels.get(channelName);
|
|
255
|
+
this.channels.delete(channelName);
|
|
256
|
+
const type = info?.options?.channelType || channelName;
|
|
257
|
+
if (this.channelTypeMap.get(type) === channelName) {
|
|
258
|
+
this.channelTypeMap.delete(type);
|
|
259
|
+
// 重定向到同类型的另一存活实例(保持按类型名路由可用)
|
|
260
|
+
for (const [name, ci] of this.channels) {
|
|
261
|
+
if ((ci.options?.channelType || name) === type) {
|
|
262
|
+
this.channelTypeMap.set(type, name);
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
239
268
|
/**
|
|
240
269
|
* 获取渠道适配器(支持实例名和 channelType)
|
|
241
270
|
*/
|
|
@@ -295,8 +324,6 @@ export class MessageProcessor {
|
|
|
295
324
|
// 先解析会话,再优先用 session.metadata.channelKey 精确定位实例级 adapter
|
|
296
325
|
// message.channel 现在存实例名(channelName),可直接用于精确路由
|
|
297
326
|
const { session, absoluteProjectPath } = await this.resolveSession(message);
|
|
298
|
-
// 群聊响应深度决策(resolveSession 之后、_processMessageInternal 之前)
|
|
299
|
-
const responseDepth = await this.resolveResponseDepth(message, session);
|
|
300
327
|
// thread(feishu) pending strategy: inject replyContext so first reply creates the thread
|
|
301
328
|
if (message.triggerMeta?.pendingThread && message.triggerMeta?.rootMessageId) {
|
|
302
329
|
const triggerId = message.triggerMeta.triggerId;
|
|
@@ -386,7 +413,7 @@ export class MessageProcessor {
|
|
|
386
413
|
});
|
|
387
414
|
try {
|
|
388
415
|
await Promise.race([
|
|
389
|
-
this._processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, () => lastIdleSec
|
|
416
|
+
this._processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, () => lastIdleSec),
|
|
390
417
|
timeoutPromise
|
|
391
418
|
]);
|
|
392
419
|
}
|
|
@@ -434,7 +461,7 @@ export class MessageProcessor {
|
|
|
434
461
|
return message.replyContext;
|
|
435
462
|
}
|
|
436
463
|
/** 自动安全模式已禁用:仅保留错误计数,不再自动切换状态 */
|
|
437
|
-
async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, getLastIdleSec
|
|
464
|
+
async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, getLastIdleSec) {
|
|
438
465
|
const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
|
|
439
466
|
const channelKey = session.metadata?.channelKey || message.channel;
|
|
440
467
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
@@ -458,7 +485,7 @@ export class MessageProcessor {
|
|
|
458
485
|
const taskId = `task-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`;
|
|
459
486
|
const chatmode = session.sessionMode ?? 'interactive';
|
|
460
487
|
// 诊断日志:记录 inbound message_id 和生成的 task_id 的对应关系
|
|
461
|
-
logger.info(`[MessageProcessor] Task created: inboundMsgId=${message.messageId ?? 'none'} taskId=${taskId} sessionId=${session.id} chatmode=${chatmode}
|
|
488
|
+
logger.info(`[MessageProcessor] Task created: inboundMsgId=${message.messageId ?? 'none'} taskId=${taskId} sessionId=${session.id} chatmode=${chatmode}`);
|
|
462
489
|
// 构建带 taskId/chatmode 的 ReplyContext(本次任务所有出站消息共用)
|
|
463
490
|
const taskReplyContext = () => {
|
|
464
491
|
const base = this.getReplyContext(message);
|
|
@@ -503,6 +530,7 @@ export class MessageProcessor {
|
|
|
503
530
|
const budgetStatus = getBudgetStatus(resolveRoot(), budgetAgentAid, budgetPeerKey);
|
|
504
531
|
if (budgetStatus.hard_blocked) {
|
|
505
532
|
logger.warn(`[MessageProcessor] Budget hard limit reached: agent=${budgetAgentAid} peer=${budgetPeerKey} pct=${budgetStatus.pct_used.toFixed(1)}%`);
|
|
533
|
+
this.touchAgentActivity(channelKey);
|
|
506
534
|
adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs: 0 } }).catch(() => { });
|
|
507
535
|
return;
|
|
508
536
|
}
|
|
@@ -522,8 +550,10 @@ export class MessageProcessor {
|
|
|
522
550
|
this.eventBus.publish({ type: 'task:started', sessionId: session.id, agentName: agentNameForStats, encrypt: taskEncrypt, chatmode: session.sessionMode || 'interactive' });
|
|
523
551
|
// 触发器消息不发 processing status(无需通知用户)
|
|
524
552
|
if (message.source !== 'trigger') {
|
|
553
|
+
this.touchAgentActivity(channelKey);
|
|
525
554
|
adapter.send(envelope, { kind: 'status.started' }).catch(() => { });
|
|
526
555
|
}
|
|
556
|
+
await this.runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, adapter, envelope);
|
|
527
557
|
logger.message({
|
|
528
558
|
msgId: messageId,
|
|
529
559
|
sessionId: session.id,
|
|
@@ -565,6 +595,8 @@ export class MessageProcessor {
|
|
|
565
595
|
opts.title = '\u2705 \u6700\u7ec8\u56de\u590d:';
|
|
566
596
|
}
|
|
567
597
|
opts.metadata = { ...(opts.metadata ?? {}), taskId, chatmode };
|
|
598
|
+
if (payload.kind.startsWith('status.'))
|
|
599
|
+
this.touchAgentActivity(channelKey);
|
|
568
600
|
const enrichedEnvelope = { ...envelope, replyContext: opts };
|
|
569
601
|
await adapter.send(enrichedEnvelope, payload);
|
|
570
602
|
},
|
|
@@ -687,20 +719,6 @@ export class MessageProcessor {
|
|
|
687
719
|
}
|
|
688
720
|
modelOverride = evolclawModelOverride;
|
|
689
721
|
}
|
|
690
|
-
// ④ 群聊 responseDepth → effort 动态映射
|
|
691
|
-
// 仅当群聊且 evolclaw 作用域未显式指定 effort 时生效(显式配置优先)
|
|
692
|
-
if (message.chatType === 'group' && responseDepth && !(modelOverride?.effort)) {
|
|
693
|
-
const depthEffortMap = {
|
|
694
|
-
lightweight: 'low',
|
|
695
|
-
standard: 'medium',
|
|
696
|
-
deep: 'high',
|
|
697
|
-
};
|
|
698
|
-
const mappedEffort = depthEffortMap[responseDepth];
|
|
699
|
-
if (mappedEffort) {
|
|
700
|
-
modelOverride = { ...(modelOverride || {}), effort: mappedEffort };
|
|
701
|
-
logger.info(`[MessageProcessor] Group depth→effort: ${responseDepth} → ${mappedEffort} session=${session.id}`);
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
722
|
agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
|
|
705
723
|
// Kit renderer: 组装上下文
|
|
706
724
|
const pkgRoot = getPackageRoot();
|
|
@@ -744,9 +762,8 @@ export class MessageProcessor {
|
|
|
744
762
|
channel: currentChannelType || null,
|
|
745
763
|
venueUid: undefined,
|
|
746
764
|
// 群分发模式 / 客户端类型 / 权限模式
|
|
747
|
-
// 优先本地 session 覆盖(/dispatch 命令),fallback 到服务器 dispatch_mode
|
|
748
|
-
dispatch: session.metadata?.dispatchMode
|
|
749
|
-
responseDepth: responseDepth || undefined,
|
|
765
|
+
// 优先本地 session 覆盖(/dispatch 命令),fallback 到服务器 dispatch_mode 缓存
|
|
766
|
+
dispatch: (session.metadata?.dispatchModeOverride ?? session.metadata?.dispatchMode ?? message.dispatchMode) || undefined,
|
|
750
767
|
clientType: message.clientType || undefined,
|
|
751
768
|
permissionMode: session.metadata?.permissionMode || 'auto',
|
|
752
769
|
capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
|
|
@@ -844,6 +861,10 @@ export class MessageProcessor {
|
|
|
844
861
|
}
|
|
845
862
|
// 可重试错误(403/429/5xx)指数退避重试,最多 3 次
|
|
846
863
|
const MAX_RETRIES = 3;
|
|
864
|
+
// Runner 开始执行前:将 Pin 升级为 CheckMark(表示"正在处理")
|
|
865
|
+
if (message.messageId && message.source !== 'trigger') {
|
|
866
|
+
adapter.promoteAck?.(message.messageId).catch(() => { });
|
|
867
|
+
}
|
|
847
868
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
848
869
|
let streamRegistered = false;
|
|
849
870
|
try {
|
|
@@ -912,12 +933,12 @@ export class MessageProcessor {
|
|
|
912
933
|
}
|
|
913
934
|
// prompt_too_long:SDK 以 complete 事件(非异常)返回,需在此处触发 compact
|
|
914
935
|
// 检测条件:terminalReason 明确为 prompt_too_long,或文本/errors 包含相关错误文本
|
|
915
|
-
const
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
936
|
+
const streamHitContextLimit = (sr) => sr.terminalReason === 'prompt_too_long' ||
|
|
937
|
+
isContextTooLongText(sr.lastReplyText) ||
|
|
938
|
+
isContextTooLongText(sr.errors?.join(' ') || '') ||
|
|
939
|
+
isContextTooLongText(sr.fullText);
|
|
940
|
+
const isPromptTooLong = streamResult.isError && !!session.agentSessionId && canCompactAgent(agent)
|
|
941
|
+
&& streamHitContextLimit(streamResult);
|
|
921
942
|
if (isPromptTooLong) {
|
|
922
943
|
renderer.addNotice('上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
|
|
923
944
|
await renderer.flush();
|
|
@@ -928,11 +949,7 @@ export class MessageProcessor {
|
|
|
928
949
|
agent.registerStream(streamKey, retryStream);
|
|
929
950
|
streamResult = await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress);
|
|
930
951
|
// 重试后仍然 prompt_too_long:清理 renderer 中可能混入的错误文本,显示友好提示
|
|
931
|
-
const
|
|
932
|
-
const retryStillTooLong = streamResult.isError && (streamResult.terminalReason === 'prompt_too_long' ||
|
|
933
|
-
contextTooLongPattern.test(streamResult.lastReplyText) ||
|
|
934
|
-
contextTooLongPattern.test(retryErrorsText) ||
|
|
935
|
-
contextTooLongPattern.test(streamResult.fullText));
|
|
952
|
+
const retryStillTooLong = streamResult.isError && streamHitContextLimit(streamResult);
|
|
936
953
|
if (retryStillTooLong) {
|
|
937
954
|
renderer.addNotice(getContextTooLongHint(agent), 'warn', 'context-too-long', true);
|
|
938
955
|
}
|
|
@@ -941,10 +958,7 @@ export class MessageProcessor {
|
|
|
941
958
|
throw new Error('CONTEXT_COMPACT_FAILED');
|
|
942
959
|
}
|
|
943
960
|
}
|
|
944
|
-
else if (streamResult.isError &&
|
|
945
|
-
contextTooLongPattern.test(streamResult.lastReplyText) ||
|
|
946
|
-
contextTooLongPattern.test(errorsText) ||
|
|
947
|
-
contextTooLongPattern.test(streamResult.fullText))) {
|
|
961
|
+
else if (streamResult.isError && streamHitContextLimit(streamResult)) {
|
|
948
962
|
// 上下文过长但无法 auto-compact(无 session ID 或 agent 不支持),显示友好提示
|
|
949
963
|
renderer.addNotice(getContextTooLongHint(agent), 'warn', 'context-too-long', true);
|
|
950
964
|
}
|
|
@@ -1073,12 +1087,6 @@ export class MessageProcessor {
|
|
|
1073
1087
|
// Flush 剩余内容(文件标记已在 flush 时自动移除)
|
|
1074
1088
|
await renderer.flush(true);
|
|
1075
1089
|
}
|
|
1076
|
-
// 更新 EvolAgent.lastActivity
|
|
1077
|
-
if (this.agentRegistry) {
|
|
1078
|
-
const owningAgent = this.agentRegistry.resolveByChannel(channelKey);
|
|
1079
|
-
if (owningAgent)
|
|
1080
|
-
owningAgent.lastActivity = Date.now();
|
|
1081
|
-
}
|
|
1082
1090
|
// 注意:不在此处清除 interruptedSessions,由下一条消息的 prompt 包装逻辑消费
|
|
1083
1091
|
const interruptReason = this.interruptedSessions.get(session.id);
|
|
1084
1092
|
if (streamResult.isError) {
|
|
@@ -1086,37 +1094,60 @@ export class MessageProcessor {
|
|
|
1086
1094
|
const errorSummary = streamResult.errors?.join('; ') || '任务执行失败';
|
|
1087
1095
|
const rawSubtype = streamResult.subtype || 'agent_error';
|
|
1088
1096
|
const errorType = prefixErrorType(ERROR_PREFIX.AGENT, rawSubtype);
|
|
1089
|
-
|
|
1097
|
+
// 用户主动打断(新消息//stop/撤回)会让 SDK 流在工具调用中途被掐断,
|
|
1098
|
+
// 末尾 result message 形状异常并被标记为 error(含 SDK 内部 ede_diagnostic 串)。
|
|
1099
|
+
// 这不是真正的失败,不应把诊断串暴露给用户,也不计入错误统计。
|
|
1100
|
+
const isUserInterrupt = interruptReason === 'new_message' || interruptReason === 'stop' || interruptReason === 'recalled';
|
|
1101
|
+
if (message.source !== 'trigger' && !isUserInterrupt) {
|
|
1102
|
+
await adapter.send(envelope, { kind: 'result.error', text: errorSummary, reason: rawSubtype }).catch(() => { });
|
|
1090
1103
|
adapter.send(envelope, { kind: 'status.error', metadata: { errorType: rawSubtype } }).catch(() => { });
|
|
1104
|
+
this.touchAgentActivity(channelKey);
|
|
1091
1105
|
}
|
|
1092
|
-
if (
|
|
1093
|
-
|
|
1106
|
+
if (isUserInterrupt) {
|
|
1107
|
+
// 用户打断:打断本身已由 message-queue 发过 task:interrupted 事件,
|
|
1108
|
+
// 这里不再补发 task:error(否则同一次打断被记两遍且错误归类为 error)。
|
|
1109
|
+
// 仅记 info 日志收尾。注意:task:interrupted 已填充 interruptedSessions,
|
|
1110
|
+
// stats 侧已据此收尾任务生命周期,无需在此重复发事件。
|
|
1111
|
+
logger.info(`[${message.channel}] Stream result error suppressed (user interrupt: ${interruptReason}): ${errorSummary}`);
|
|
1112
|
+
logger.message({
|
|
1113
|
+
msgId: messageId,
|
|
1114
|
+
sessionId: session.id,
|
|
1115
|
+
dir: 'inbound',
|
|
1116
|
+
status: 'interrupted',
|
|
1117
|
+
error: errorSummary,
|
|
1118
|
+
terminalReason: streamResult.terminalReason
|
|
1119
|
+
});
|
|
1094
1120
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
if (
|
|
1109
|
-
|
|
1121
|
+
else {
|
|
1122
|
+
if (message.triggerMeta) {
|
|
1123
|
+
this.eventBus.publish({ type: 'trigger:failed', triggerId: message.triggerMeta.triggerId, name: message.triggerMeta.triggerName ?? '', messageId: messageId, error: errorSummary, targetChannel: message.channel, targetChannelId: message.channelId, fireTime: message.triggerMeta.fireTime ?? 0, phase: 'execute' });
|
|
1124
|
+
}
|
|
1125
|
+
this.eventBus.publish({
|
|
1126
|
+
type: 'task:error',
|
|
1127
|
+
sessionId: session.id,
|
|
1128
|
+
error: errorSummary,
|
|
1129
|
+
errorType,
|
|
1130
|
+
agentName: agentNameForStats,
|
|
1131
|
+
terminalReason: streamResult.terminalReason
|
|
1132
|
+
});
|
|
1133
|
+
// 系统级 subtype 仍累计错误计数,供 /status 诊断使用
|
|
1134
|
+
if (isInfraError(rawSubtype, streamResult.terminalReason)) {
|
|
1135
|
+
const chatType = message.chatType || 'private';
|
|
1136
|
+
const identityRole = session.identity?.role || 'anonymous';
|
|
1137
|
+
const { policy } = channelInfo;
|
|
1138
|
+
if (policy.accumulateErrors(chatType, identityRole)) {
|
|
1139
|
+
await this.sessionManager.recordError(session.id, errorType, errorSummary);
|
|
1140
|
+
}
|
|
1110
1141
|
}
|
|
1142
|
+
logger.message({
|
|
1143
|
+
msgId: messageId,
|
|
1144
|
+
sessionId: session.id,
|
|
1145
|
+
dir: 'inbound',
|
|
1146
|
+
status: 'failed',
|
|
1147
|
+
error: errorSummary,
|
|
1148
|
+
terminalReason: streamResult.terminalReason
|
|
1149
|
+
});
|
|
1111
1150
|
}
|
|
1112
|
-
logger.message({
|
|
1113
|
-
msgId: messageId,
|
|
1114
|
-
sessionId: session.id,
|
|
1115
|
-
dir: 'inbound',
|
|
1116
|
-
status: 'failed',
|
|
1117
|
-
error: errorSummary,
|
|
1118
|
-
terminalReason: streamResult.terminalReason
|
|
1119
|
-
});
|
|
1120
1151
|
}
|
|
1121
1152
|
else {
|
|
1122
1153
|
// 真正的成功
|
|
@@ -1160,6 +1191,9 @@ export class MessageProcessor {
|
|
|
1160
1191
|
output_tokens: mc.tokenUsage.output_tokens ?? 0,
|
|
1161
1192
|
cache_creation_tokens: mc.tokenUsage.cache_creation_input_tokens ?? 0,
|
|
1162
1193
|
cache_read_tokens: mc.tokenUsage.cache_read_input_tokens ?? 0,
|
|
1194
|
+
context_tokens: mc.contextUsage?.totalTokens,
|
|
1195
|
+
max_tokens: mc.contextUsage?.maxTokens,
|
|
1196
|
+
auto_compact_tokens: mc.contextUsage?.autoCompactTokens,
|
|
1163
1197
|
degraded: mc.degraded ? 1 : 0,
|
|
1164
1198
|
}));
|
|
1165
1199
|
insertModelCalls(resolveRoot(), mcRows);
|
|
@@ -1217,6 +1251,7 @@ export class MessageProcessor {
|
|
|
1217
1251
|
}
|
|
1218
1252
|
catch { /* non-fatal */ }
|
|
1219
1253
|
if (message.source !== 'trigger') {
|
|
1254
|
+
this.touchAgentActivity(channelKey);
|
|
1220
1255
|
if (interruptReason) {
|
|
1221
1256
|
adapter.send(envelope, { kind: 'status.interrupted', metadata: { reason: interruptReason } }).catch(() => { });
|
|
1222
1257
|
}
|
|
@@ -1311,6 +1346,7 @@ export class MessageProcessor {
|
|
|
1311
1346
|
? { kind: 'status.interrupted', metadata: { reason: 'stream_error' } }
|
|
1312
1347
|
: { kind: 'status.error' };
|
|
1313
1348
|
adapter.send(envelope, statusPayload).catch(() => { });
|
|
1349
|
+
this.touchAgentActivity(channelKey);
|
|
1314
1350
|
}
|
|
1315
1351
|
// 用户主动中断时降级日志;其余仍按 error 记录
|
|
1316
1352
|
if (isUserInterrupt) {
|
|
@@ -1321,19 +1357,25 @@ export class MessageProcessor {
|
|
|
1321
1357
|
}
|
|
1322
1358
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1323
1359
|
const errorType = prefixErrorType(ERROR_PREFIX.INFRA, errType);
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1360
|
+
// 用户主动打断:流被掐断抛出的异常不是真正的失败。打断发生时 source
|
|
1361
|
+
// (message-queue / slash-handler)已发过 task:interrupted(它填充了
|
|
1362
|
+
// interruptedSessions,isUserInterrupt 才会为真),stats 侧已据此收尾任务。
|
|
1363
|
+
// 此处不再发任何事件——发 task:error 会误归类,重发 task:interrupted 会重复记账。
|
|
1364
|
+
if (!isUserInterrupt) {
|
|
1365
|
+
this.eventBus.publish({
|
|
1366
|
+
type: 'task:error',
|
|
1367
|
+
sessionId: session.id,
|
|
1368
|
+
error: errorMsg,
|
|
1369
|
+
errorType,
|
|
1370
|
+
agentName: agentNameForStats,
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1331
1373
|
// 记录处理失败
|
|
1332
1374
|
logger.message({
|
|
1333
1375
|
msgId: messageId,
|
|
1334
1376
|
sessionId: session.id,
|
|
1335
1377
|
dir: 'inbound',
|
|
1336
|
-
status: 'failed',
|
|
1378
|
+
status: isUserInterrupt ? 'interrupted' : 'failed',
|
|
1337
1379
|
error: error instanceof Error ? error.message : String(error)
|
|
1338
1380
|
});
|
|
1339
1381
|
if (error instanceof Error && !isUserInterrupt) {
|
|
@@ -1367,14 +1409,83 @@ export class MessageProcessor {
|
|
|
1367
1409
|
...(sendOpts ?? {}),
|
|
1368
1410
|
metadata: { ...(sendOpts?.metadata ?? {}), taskId, chatmode },
|
|
1369
1411
|
};
|
|
1370
|
-
const errorPayload =
|
|
1371
|
-
|
|
1372
|
-
|
|
1412
|
+
const errorPayload = {
|
|
1413
|
+
kind: 'result.error',
|
|
1414
|
+
text: userMessage,
|
|
1415
|
+
reason: isTimeout ? 'timeout' : errType,
|
|
1416
|
+
};
|
|
1373
1417
|
await adapter.send({ ...envelope, replyContext: sendOpts }, errorPayload);
|
|
1374
1418
|
// Proactive 可观测:catch 块的基础设施错误也透传为 thought,保证按 task_id 聚合完整
|
|
1375
1419
|
}
|
|
1376
1420
|
}
|
|
1377
1421
|
}
|
|
1422
|
+
async runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, adapter, envelope) {
|
|
1423
|
+
if (!session.agentSessionId || !canCompactAgent(agent))
|
|
1424
|
+
return;
|
|
1425
|
+
const ctx = await this.readLastModelCallContextUsage(session.id, session.agentSessionId);
|
|
1426
|
+
if (!ctx || ctx.totalTokens < ctx.autoCompactTokens)
|
|
1427
|
+
return;
|
|
1428
|
+
logger.info(`[MessageProcessor] Auto compact at task.start: session=${session.id} totalTokens=${ctx.totalTokens} autoCompactTokens=${ctx.autoCompactTokens}`);
|
|
1429
|
+
await adapter.send(envelope, { kind: 'system.notice', text: '上下文接近上限,正在压缩会话...', subtype: 'auto-compact-start' }).catch(() => { });
|
|
1430
|
+
try {
|
|
1431
|
+
const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
|
|
1432
|
+
if (compacted) {
|
|
1433
|
+
await adapter.send(envelope, { kind: 'system.notice', text: '✅ 上下文压缩完成,继续处理...', subtype: 'auto-compact-complete' }).catch(() => { });
|
|
1434
|
+
}
|
|
1435
|
+
else {
|
|
1436
|
+
logger.warn(`[MessageProcessor] Auto compact at task.start returned false (session=${session.id})`);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
catch (err) {
|
|
1440
|
+
logger.warn(`[MessageProcessor] Auto compact at task.start failed (non-fatal):`, err);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
async readLastModelCallContextUsage(sessionId, agentSessionId) {
|
|
1444
|
+
try {
|
|
1445
|
+
const { openReadonlyDb, getDbPath } = await import('../stats/db.js');
|
|
1446
|
+
const rdb = openReadonlyDb(getDbPath(resolveRoot()));
|
|
1447
|
+
if (!rdb)
|
|
1448
|
+
return undefined;
|
|
1449
|
+
try {
|
|
1450
|
+
const row = rdb.prepare(`SELECT model, input_tokens, cache_creation_tokens, cache_read_tokens,
|
|
1451
|
+
context_tokens, max_tokens, auto_compact_tokens
|
|
1452
|
+
FROM model_calls
|
|
1453
|
+
WHERE session_id = ?
|
|
1454
|
+
AND agent_session_id = ?
|
|
1455
|
+
ORDER BY ts DESC, call_index DESC
|
|
1456
|
+
LIMIT 1`).get(sessionId, agentSessionId);
|
|
1457
|
+
if (!row)
|
|
1458
|
+
return undefined;
|
|
1459
|
+
const model = row.model || '';
|
|
1460
|
+
const recordedTotalTokens = row.context_tokens ?? undefined;
|
|
1461
|
+
let totalTokens;
|
|
1462
|
+
if (recordedTotalTokens && recordedTotalTokens > 0) {
|
|
1463
|
+
totalTokens = recordedTotalTokens;
|
|
1464
|
+
}
|
|
1465
|
+
else if (isClaudeContextUsageModel(model)) {
|
|
1466
|
+
totalTokens = (row.input_tokens ?? 0) + (row.cache_creation_tokens ?? 0) + (row.cache_read_tokens ?? 0);
|
|
1467
|
+
}
|
|
1468
|
+
else {
|
|
1469
|
+
totalTokens = row.input_tokens ?? 0;
|
|
1470
|
+
}
|
|
1471
|
+
if (totalTokens <= 0)
|
|
1472
|
+
return undefined;
|
|
1473
|
+
const recordedAutoCompactTokens = row.auto_compact_tokens ?? undefined;
|
|
1474
|
+
const inferredAutoCompactTokens = autoCompactTokensFromMaxTokens(row.max_tokens ?? undefined);
|
|
1475
|
+
const autoCompactTokens = recordedAutoCompactTokens && recordedAutoCompactTokens > 0
|
|
1476
|
+
? recordedAutoCompactTokens
|
|
1477
|
+
: inferredAutoCompactTokens ?? autoCompactWindowForModel(model);
|
|
1478
|
+
return { totalTokens, autoCompactTokens };
|
|
1479
|
+
}
|
|
1480
|
+
finally {
|
|
1481
|
+
rdb.close();
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
catch (err) {
|
|
1485
|
+
logger.debug(`[MessageProcessor] Failed to read last model call context usage: ${err}`);
|
|
1486
|
+
return undefined;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1378
1489
|
/**
|
|
1379
1490
|
* 解析会话和项目路径
|
|
1380
1491
|
*/
|
|
@@ -1388,15 +1499,8 @@ export class MessageProcessor {
|
|
|
1388
1499
|
}
|
|
1389
1500
|
: undefined;
|
|
1390
1501
|
const projectPath = this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd();
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
if (!existing) {
|
|
1394
|
-
const role = this.sessionManager.resolveIdentity(message.channel, message.peerId).role;
|
|
1395
|
-
if (role !== 'owner' && role !== 'admin') {
|
|
1396
|
-
throw new Error('群聊中无权限创建话题');
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1502
|
+
// 话题创建权限守卫已统一移至 MessageBridge.canCreateThreadSession(enqueue 前拦截),
|
|
1503
|
+
// 此处不再重复检查——bridge 层拒绝后消息根本不会到达 processMessage。
|
|
1400
1504
|
// current strategy: resume bound session, make it active so output is not suppressed
|
|
1401
1505
|
if (message.triggerMeta?.boundSessionId) {
|
|
1402
1506
|
const bound = await this.sessionManager.getSessionById(message.triggerMeta.boundSessionId);
|
|
@@ -1458,32 +1562,6 @@ export class MessageProcessor {
|
|
|
1458
1562
|
: path.resolve(process.cwd(), session.projectPath);
|
|
1459
1563
|
return { session, absoluteProjectPath };
|
|
1460
1564
|
}
|
|
1461
|
-
/**
|
|
1462
|
-
* 群聊响应深度决策。根据 dispatch 模式、消息特征、话题轮次综合判断。
|
|
1463
|
-
* 返回 per-message 的瞬时深度枚举,不持久化到 session.metadata。
|
|
1464
|
-
* 同时更新 session.metadata 中的 topicRounds/lastTopicHash(话题追踪状态)。
|
|
1465
|
-
*/
|
|
1466
|
-
async resolveResponseDepth(message, session) {
|
|
1467
|
-
const result = computeResponseDepth({
|
|
1468
|
-
chatType: message.chatType,
|
|
1469
|
-
content: message.content,
|
|
1470
|
-
selfAid: session.selfAID || message.selfAID,
|
|
1471
|
-
mentionAids: message.mentionAids,
|
|
1472
|
-
dispatch: session.metadata?.dispatchMode || message.dispatchMode,
|
|
1473
|
-
topicRounds: session.metadata?.topicRounds ?? 0,
|
|
1474
|
-
lastTopicHash: session.metadata?.lastTopicHash,
|
|
1475
|
-
});
|
|
1476
|
-
// 持久化话题追踪状态(仅群聊时有意义)
|
|
1477
|
-
if (message.chatType === 'group') {
|
|
1478
|
-
session.metadata = {
|
|
1479
|
-
...(session.metadata || {}),
|
|
1480
|
-
topicRounds: result.topicRounds,
|
|
1481
|
-
lastTopicHash: result.topicHash,
|
|
1482
|
-
};
|
|
1483
|
-
await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
|
|
1484
|
-
}
|
|
1485
|
-
return result.depth;
|
|
1486
|
-
}
|
|
1487
1565
|
/**
|
|
1488
1566
|
* 处理标准事件流(AgentEvent)
|
|
1489
1567
|
*
|
|
@@ -1538,6 +1616,7 @@ export class MessageProcessor {
|
|
|
1538
1616
|
// session_id 已在 AgentRunner.transformStream 中处理,此处仅记录
|
|
1539
1617
|
if (event.type === 'session_id') {
|
|
1540
1618
|
logger.debug(`[MessageProcessor] Session ID updated: ${event.sessionId} for session: ${session.id}`);
|
|
1619
|
+
session.agentSessionId = event.sessionId;
|
|
1541
1620
|
continue;
|
|
1542
1621
|
}
|
|
1543
1622
|
// session 状态变更(idle/running/requires_action)
|
|
@@ -1619,7 +1698,6 @@ export class MessageProcessor {
|
|
|
1619
1698
|
sessionId: session.id,
|
|
1620
1699
|
toolName: event.name,
|
|
1621
1700
|
isError: event.isError,
|
|
1622
|
-
content: event.result,
|
|
1623
1701
|
agentName: agentNameForStats,
|
|
1624
1702
|
timestamp: Date.now()
|
|
1625
1703
|
});
|
|
@@ -1642,7 +1720,7 @@ export class MessageProcessor {
|
|
|
1642
1720
|
// 记录错误文本到 lastReplyText,供后续 isPromptTooLong 检测
|
|
1643
1721
|
lastReplyText += event.error || '';
|
|
1644
1722
|
// 上下文过长的错误不在此处输出 notice,留给外层 isPromptTooLong 触发 auto-compact
|
|
1645
|
-
const isContextError =
|
|
1723
|
+
const isContextError = isContextTooLongText(event.error || '');
|
|
1646
1724
|
if (!isContextError && !hasErrorResult && !shouldSuppress()) {
|
|
1647
1725
|
hasErrorResult = true;
|
|
1648
1726
|
renderer.addNotice(`${event.error}`, 'warn', 'runtime-error', true);
|
|
@@ -1669,8 +1747,8 @@ export class MessageProcessor {
|
|
|
1669
1747
|
const interruptReason = this.interruptedSessions.get(session.id);
|
|
1670
1748
|
const isUserInterrupt = interruptReason === 'new_message' || interruptReason === 'stop' || interruptReason === 'recalled';
|
|
1671
1749
|
const isContextTooLong = event.terminalReason === 'prompt_too_long'
|
|
1672
|
-
||
|
|
1673
|
-
||
|
|
1750
|
+
|| isContextTooLongText(event.errors?.join(' ') || '')
|
|
1751
|
+
|| isContextTooLongText(lastReplyText);
|
|
1674
1752
|
if (event.isError && !hasErrorResult && !shouldSuppress() && !isUserInterrupt && !isContextTooLong) {
|
|
1675
1753
|
const errorSummary = event.errors?.join('; ') || '任务执行失败';
|
|
1676
1754
|
// 使用 terminalReason 提供更友好的错误提示(不带 emoji,由 formatter 统一加)
|
|
@@ -16,6 +16,7 @@ export class MessageQueue {
|
|
|
16
16
|
recentMessageIds = new Set();
|
|
17
17
|
DEDUP_WINDOW = 60_000; // 1 分钟窗口
|
|
18
18
|
interceptors = new Map();
|
|
19
|
+
mutedAgents = new Set(); // 禁言的 agent:消息照常入队,但不取出给大模型
|
|
19
20
|
constructor(handler) {
|
|
20
21
|
this.handler = handler;
|
|
21
22
|
}
|
|
@@ -145,6 +146,14 @@ export class MessageQueue {
|
|
|
145
146
|
this.activeMessageIds.clear();
|
|
146
147
|
return;
|
|
147
148
|
}
|
|
149
|
+
// 禁言:消息留在队列里,暂停消费(解禁后由 unmuteAgent 重新触发 processNext)
|
|
150
|
+
const headAgent = queue[0].agentName || DEFAULT_AGENT_NAME;
|
|
151
|
+
if (this.mutedAgents.has(headAgent)) {
|
|
152
|
+
logger.info(`[Queue] processNext: agent ${headAgent} muted, pausing key=${queueKey} (${queue.length} queued)`);
|
|
153
|
+
this.processing.delete(queueKey);
|
|
154
|
+
this.processingAgent.delete(queueKey);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
148
157
|
// FIFO 贪心合并:弹出队首连续同 peerId 的消息
|
|
149
158
|
const items = this.dequeueGreedy(queue);
|
|
150
159
|
const merged = items.length === 1 ? items[0] : this.mergeItems(items);
|
|
@@ -377,4 +386,63 @@ export class MessageQueue {
|
|
|
377
386
|
}
|
|
378
387
|
return total;
|
|
379
388
|
}
|
|
389
|
+
/**
|
|
390
|
+
* 清空指定 agent 的待处理消息(不影响正在处理中的消息)。
|
|
391
|
+
* 被移除的消息直接 resolve(与 cancel 一致),让 enqueue 的等待方正常解除阻塞。
|
|
392
|
+
* @returns 被清除的消息数量
|
|
393
|
+
*/
|
|
394
|
+
clearByAgent(agentName) {
|
|
395
|
+
let cleared = 0;
|
|
396
|
+
for (const queue of this.queues.values()) {
|
|
397
|
+
for (let i = queue.length - 1; i >= 0; i--) {
|
|
398
|
+
if ((queue[i].agentName || DEFAULT_AGENT_NAME) === agentName) {
|
|
399
|
+
const [removed] = queue.splice(i, 1);
|
|
400
|
+
removed.resolve();
|
|
401
|
+
cleared++;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (cleared > 0)
|
|
406
|
+
logger.info(`[Queue] Cleared ${cleared} pending message(s) for agent ${agentName}`);
|
|
407
|
+
return cleared;
|
|
408
|
+
}
|
|
409
|
+
/** 禁言 agent:后续消息照常入队,但 processNext 不再取出处理。 */
|
|
410
|
+
muteAgent(agentName) {
|
|
411
|
+
this.mutedAgents.add(agentName);
|
|
412
|
+
logger.info(`[Queue] Muted agent ${agentName}`);
|
|
413
|
+
}
|
|
414
|
+
/** 解除禁言:重新触发该 agent 已积压、且当前未在处理的队列。 */
|
|
415
|
+
unmuteAgent(agentName) {
|
|
416
|
+
if (!this.mutedAgents.delete(agentName))
|
|
417
|
+
return;
|
|
418
|
+
logger.info(`[Queue] Unmuted agent ${agentName}, resuming queued messages`);
|
|
419
|
+
for (const [key, queue] of this.queues) {
|
|
420
|
+
if (queue.length > 0 && !this.processing.has(key)) {
|
|
421
|
+
const headAgent = queue[0].agentName || DEFAULT_AGENT_NAME;
|
|
422
|
+
if (headAgent === agentName)
|
|
423
|
+
this.processNext(key);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
isAgentMuted(agentName) {
|
|
428
|
+
return this.mutedAgents.has(agentName);
|
|
429
|
+
}
|
|
430
|
+
/** 中断指定 agent 所有正在处理中的会话(停止 agent 时调用)。 */
|
|
431
|
+
interruptByAgent(agentName) {
|
|
432
|
+
for (const [queueKey, name] of this.processingAgent) {
|
|
433
|
+
if ((name || DEFAULT_AGENT_NAME) === agentName) {
|
|
434
|
+
const sessionKey = queueKey.split('::')[0];
|
|
435
|
+
logger.info(`[Queue] Interrupting session ${sessionKey} for stopped agent ${agentName}`);
|
|
436
|
+
this.eventBus?.publish({
|
|
437
|
+
type: 'task:interrupted',
|
|
438
|
+
sessionId: sessionKey,
|
|
439
|
+
reason: 'new_message',
|
|
440
|
+
agentName: name,
|
|
441
|
+
});
|
|
442
|
+
if (this.interruptCallback) {
|
|
443
|
+
this.interruptCallback(sessionKey, this.currentAgentId, name).catch(() => { });
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
380
448
|
}
|