evolclaw 3.2.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 +53 -0
- package/README.md +7 -4
- package/dist/agents/{resolve.js → baseagent.js} +34 -5
- package/dist/agents/claude-runner.js +120 -31
- package/dist/agents/codex-app-server-client.js +364 -0
- package/dist/agents/codex-runner.js +1152 -140
- package/dist/agents/gemini-runner.js +2 -2
- package/dist/agents/runner-types.js +58 -0
- package/dist/aun/aid/store.js +1 -1
- package/dist/aun/outbox.js +14 -2
- package/dist/aun/storage/download.js +1 -1
- package/dist/aun/storage/upload.js +13 -1
- package/dist/channels/aun.js +869 -358
- package/dist/channels/dingtalk.js +77 -140
- package/dist/channels/feishu.js +125 -154
- package/dist/channels/qqbot.js +75 -138
- package/dist/channels/wechat.js +75 -136
- package/dist/channels/wecom.js +75 -138
- package/dist/cli/agent-command.js +591 -0
- package/dist/cli/agent.js +23 -8
- 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 +23 -4905
- package/dist/cli/init.js +33 -6
- package/dist/cli/model.js +1 -1
- package/dist/cli/restart-monitor.js +539 -0
- package/dist/cli/stats.js +558 -0
- package/dist/cli/version.js +87 -0
- package/dist/cli/watch-logs.js +33 -0
- package/dist/cli/watch-msg.js +5 -2
- package/dist/config-store.js +12 -6
- package/dist/core/channel-loader.js +88 -83
- 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 +82 -0
- package/dist/core/evolagent.js +17 -1
- package/dist/core/interaction-router.js +8 -0
- package/dist/core/message/command-handler-agent-control.js +63 -1
- package/dist/core/message/im-renderer.js +91 -51
- package/dist/core/message/items-formatter.js +9 -1
- package/dist/core/message/message-bridge.js +73 -24
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-processor.js +432 -94
- package/dist/core/message/message-queue.js +70 -2
- package/dist/core/message/pending-hints.js +232 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-scope.js +2 -2
- package/dist/core/permission.js +25 -12
- package/dist/core/relation/peer-identity.js +16 -1
- package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
- package/dist/core/session/session-manager.js +86 -26
- package/dist/core/session/session-title.js +26 -0
- package/dist/core/stats/billing.js +151 -0
- package/dist/core/stats/budget.js +93 -0
- package/dist/core/stats/db.js +334 -0
- package/dist/core/stats/eck-vars.js +84 -0
- package/dist/core/stats/index.js +10 -0
- package/dist/core/stats/normalizer.js +78 -0
- package/dist/core/stats/query.js +760 -0
- package/dist/core/stats/writer.js +115 -0
- package/dist/core/trigger/manager.js +34 -0
- package/dist/core/trigger/parser.js +9 -3
- package/dist/core/trigger/scheduler.js +20 -17
- package/dist/data/error-dict.json +7 -0
- package/dist/{agents → eck}/manifest-engine.js +20 -1
- package/dist/{agents → eck}/message-renderer.js +24 -1
- package/dist/index.js +174 -9
- package/dist/ipc.js +116 -1
- package/dist/utils/cross-platform.js +58 -5
- package/dist/utils/ecweb-launch.js +49 -0
- package/dist/utils/ecweb-pair.js +20 -0
- package/dist/utils/error-utils.js +18 -5
- package/dist/utils/npm-ops.js +38 -8
- package/dist/utils/stats.js +77 -6
- package/kits/docs/evolclaw/INDEX.md +3 -1
- package/kits/docs/evolclaw/fs-architecture.md +1215 -0
- package/kits/docs/evolclaw/fs.md +131 -0
- package/kits/docs/evolclaw/group-fs.md +209 -0
- package/kits/docs/evolclaw/stats.md +70 -0
- package/kits/docs/venues/aun-group.md +29 -6
- package/kits/docs/venues/group.md +5 -4
- package/kits/eck_message_manifest.json +30 -3
- package/kits/rules/05-venue.md +1 -1
- package/kits/templates/message-fragments/inject-default.md +2 -0
- package/package.json +5 -6
- package/dist/agents/baseagent-normalize.js +0 -19
- package/dist/core/command-handler.js +0 -3876
- package/dist/core/relation/peer-key.js +0 -16
- package/dist/evolclaw-config.js +0 -11
- package/dist/utils/channel-helpers.js +0 -46
- /package/dist/core/{cache/file-cache.js → daemon-file-cache.js} +0 -0
- /package/dist/{agents → eck}/kit-renderer.js +0 -0
|
@@ -2,20 +2,24 @@ 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/
|
|
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
|
-
import { getPackageRoot, resolveRoot } from '../../paths.js';
|
|
13
|
-
import { renderKitSections } from '../../
|
|
14
|
-
import { renderMessageBody } from '../../
|
|
15
|
-
import {
|
|
12
|
+
import { getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
|
|
13
|
+
import { renderKitSections } from '../../eck/kit-renderer.js';
|
|
14
|
+
import { renderMessageBody } from '../../eck/message-renderer.js';
|
|
15
|
+
import { consumeHints, hintsToSubMessages, composeHintFallback } from './pending-hints.js';
|
|
16
|
+
import { normalizeBaseagent } from '../../agents/baseagent.js';
|
|
16
17
|
import { renderActionAsText, renderCommandCardAsText } from '../interaction-router.js';
|
|
17
|
-
import { formatPeerKey } from '../relation/peer-
|
|
18
|
+
import { formatPeerKey } from '../relation/peer-identity.js';
|
|
18
19
|
import { resolveEffectiveModel } from '../model/model-scope.js';
|
|
20
|
+
import { insertUsageEvent, insertContextBreakdown, insertModelCalls } from '../stats/writer.js';
|
|
21
|
+
import { normalizeUsage } from '../stats/normalizer.js';
|
|
22
|
+
import { getBudgetStatus } from '../stats/budget.js';
|
|
19
23
|
/** OS 信息在进程生命周期内是常量,模块加载时算一次。例: "Windows 11 Pro (win32 10.0.26200)" */
|
|
20
24
|
const OS_INFO = (() => {
|
|
21
25
|
let label = '';
|
|
@@ -61,6 +65,11 @@ function getContextCompactFailedHint(agent) {
|
|
|
61
65
|
function canCompactAgent(agent) {
|
|
62
66
|
return hasCompact(agent) && agent.capabilities?.compact !== false;
|
|
63
67
|
}
|
|
68
|
+
function autoCompactTokensFromMaxTokens(maxTokens) {
|
|
69
|
+
if (!maxTokens || maxTokens <= 0)
|
|
70
|
+
return undefined;
|
|
71
|
+
return maxTokens >= 1000000 ? maxTokens - 100000 : maxTokens;
|
|
72
|
+
}
|
|
64
73
|
/**
|
|
65
74
|
* 构造 OutboundEnvelope —— 出站三件套的信封部分。
|
|
66
75
|
*
|
|
@@ -182,6 +191,12 @@ export class MessageProcessor {
|
|
|
182
191
|
setAgentRegistry(registry) {
|
|
183
192
|
this.agentRegistry = registry;
|
|
184
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
|
+
}
|
|
185
200
|
getAgentContext(channelName, chatType) {
|
|
186
201
|
if (!this.agentRegistry)
|
|
187
202
|
return null;
|
|
@@ -192,6 +207,34 @@ export class MessageProcessor {
|
|
|
192
207
|
const globalCm = agent.config?.chatmode ?? this.globalSettings.chatmode;
|
|
193
208
|
return agent.getContext(channelName, chatType, globalCm);
|
|
194
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* 观察者插话(v0.3):消费当前 (对端, thread) 的待用提示,转成 owner-hint SubMessage。
|
|
212
|
+
* 一次性语义:consumeHints 回放算有效集后清该 thread(其它 thread 残留则保留,否则删文件)。
|
|
213
|
+
* 仅 aun 渠道(pending-hints 落在 sessions/aun/<self>/<对端>/)。
|
|
214
|
+
*/
|
|
215
|
+
consumeOwnerHints(session, message) {
|
|
216
|
+
const channelType = session.channelType || message.channelType || session.channel;
|
|
217
|
+
if (channelType !== 'aun')
|
|
218
|
+
return [];
|
|
219
|
+
const selfAID = session.selfAID || message.selfAID;
|
|
220
|
+
if (!selfAID)
|
|
221
|
+
return [];
|
|
222
|
+
// 会话定位键:私聊=对端 AID,群聊=groupId(均为 session.channelId)。
|
|
223
|
+
const peerChannelId = session.channelId;
|
|
224
|
+
if (!peerChannelId)
|
|
225
|
+
return [];
|
|
226
|
+
try {
|
|
227
|
+
const hints = consumeHints(resolvePaths().sessionsDir, 'aun', peerChannelId, selfAID, session.threadId);
|
|
228
|
+
if (hints.length === 0)
|
|
229
|
+
return [];
|
|
230
|
+
logger.info(`[MessageProcessor] consumed ${hints.length} owner-hint(s) for ${peerChannelId} thread=${session.threadId || 'main'}`);
|
|
231
|
+
return hintsToSubMessages(hints);
|
|
232
|
+
}
|
|
233
|
+
catch (e) {
|
|
234
|
+
logger.warn(`[MessageProcessor] consumeOwnerHints failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
195
238
|
/**
|
|
196
239
|
* 注册渠道适配器
|
|
197
240
|
*/
|
|
@@ -203,6 +246,25 @@ export class MessageProcessor {
|
|
|
203
246
|
this.channelTypeMap.set(type, adapter.channelName);
|
|
204
247
|
}
|
|
205
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
|
+
}
|
|
206
268
|
/**
|
|
207
269
|
* 获取渠道适配器(支持实例名和 channelType)
|
|
208
270
|
*/
|
|
@@ -461,6 +523,18 @@ export class MessageProcessor {
|
|
|
461
523
|
agentName: agentNameForStats,
|
|
462
524
|
timestamp: Date.now()
|
|
463
525
|
});
|
|
526
|
+
// ── 硬上限检查:超限直接返回提示,不调模型 ──
|
|
527
|
+
{
|
|
528
|
+
const budgetAgentAid = session.selfAID || message.selfAID || '';
|
|
529
|
+
const budgetPeerKey = formatPeerKey(message.channel, message.channelId);
|
|
530
|
+
const budgetStatus = getBudgetStatus(resolveRoot(), budgetAgentAid, budgetPeerKey);
|
|
531
|
+
if (budgetStatus.hard_blocked) {
|
|
532
|
+
logger.warn(`[MessageProcessor] Budget hard limit reached: agent=${budgetAgentAid} peer=${budgetPeerKey} pct=${budgetStatus.pct_used.toFixed(1)}%`);
|
|
533
|
+
this.touchAgentActivity(channelKey);
|
|
534
|
+
adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs: 0 } }).catch(() => { });
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
464
538
|
const imageInfo = message.images && message.images.length > 0 ? ` [${message.images.length} image(s)]` : '';
|
|
465
539
|
const modeInfo = isBackground ? ' [\u540e\u53f0]' : '';
|
|
466
540
|
const e2eeInfo = message.replyContext?.metadata?.encrypted != null ? ` encrypt=${message.replyContext.metadata.encrypted}` : '';
|
|
@@ -476,8 +550,10 @@ export class MessageProcessor {
|
|
|
476
550
|
this.eventBus.publish({ type: 'task:started', sessionId: session.id, agentName: agentNameForStats, encrypt: taskEncrypt, chatmode: session.sessionMode || 'interactive' });
|
|
477
551
|
// 触发器消息不发 processing status(无需通知用户)
|
|
478
552
|
if (message.source !== 'trigger') {
|
|
553
|
+
this.touchAgentActivity(channelKey);
|
|
479
554
|
adapter.send(envelope, { kind: 'status.started' }).catch(() => { });
|
|
480
555
|
}
|
|
556
|
+
await this.runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, adapter, envelope);
|
|
481
557
|
logger.message({
|
|
482
558
|
msgId: messageId,
|
|
483
559
|
sessionId: session.id,
|
|
@@ -519,6 +595,8 @@ export class MessageProcessor {
|
|
|
519
595
|
opts.title = '\u2705 \u6700\u7ec8\u56de\u590d:';
|
|
520
596
|
}
|
|
521
597
|
opts.metadata = { ...(opts.metadata ?? {}), taskId, chatmode };
|
|
598
|
+
if (payload.kind.startsWith('status.'))
|
|
599
|
+
this.touchAgentActivity(channelKey);
|
|
522
600
|
const enrichedEnvelope = { ...envelope, replyContext: opts };
|
|
523
601
|
await adapter.send(enrichedEnvelope, payload);
|
|
524
602
|
},
|
|
@@ -546,6 +624,9 @@ export class MessageProcessor {
|
|
|
546
624
|
agentName: agentNameForStats,
|
|
547
625
|
taskId,
|
|
548
626
|
chatmode: isProactive ? 'proactive' : 'interactive',
|
|
627
|
+
flushPending: async () => {
|
|
628
|
+
await renderer.flush(false);
|
|
629
|
+
},
|
|
549
630
|
interceptNextMessage: this.messageQueue
|
|
550
631
|
? (sessionKey, handler) => this.messageQueue.interceptNext(sessionKey, handler)
|
|
551
632
|
: undefined,
|
|
@@ -582,7 +663,8 @@ export class MessageProcessor {
|
|
|
582
663
|
const currentChannelType = options?.channelType || message.channel;
|
|
583
664
|
// 提取 self 信息
|
|
584
665
|
const adapterAny = channelInfo.adapter;
|
|
585
|
-
const
|
|
666
|
+
const adapterSelfAid = typeof adapterAny._selfAid === 'function' ? adapterAny._selfAid() : undefined;
|
|
667
|
+
const selfAid = adapterSelfAid || message.selfAID || session.selfAID || undefined;
|
|
586
668
|
const selfName = typeof adapterAny._selfName === 'function' ? adapterAny._selfName() : undefined;
|
|
587
669
|
const peerName = message.peerName || session.metadata?.peerName;
|
|
588
670
|
// 通道能力
|
|
@@ -606,6 +688,7 @@ export class MessageProcessor {
|
|
|
606
688
|
const peerKey = (currentChannelType && peerIdRaw)
|
|
607
689
|
? formatPeerKey(currentChannelType, peerIdRaw)
|
|
608
690
|
: undefined;
|
|
691
|
+
const normalizedBaseagent = normalizeBaseagent(agent.name);
|
|
609
692
|
// 按 关系级 > agent级 > 全局 解析本次调用的模型/强度,作为 per-call 入参传入 runQuery。
|
|
610
693
|
// 不缓存、不绑会话——改关系级/agent级后该范围所有会话的下条消息即时生效;
|
|
611
694
|
// 多对端并发各自独立解析、各自传参,无共享状态可被污染。
|
|
@@ -625,7 +708,7 @@ export class MessageProcessor {
|
|
|
625
708
|
let evolclawModelOverride;
|
|
626
709
|
if (!skipEvolclawModel) {
|
|
627
710
|
try {
|
|
628
|
-
const resolved = resolveEffectiveModel({ self: selfAid || undefined, peerKey });
|
|
711
|
+
const resolved = resolveEffectiveModel({ self: selfAid || undefined, peerKey }, normalizedBaseagent.canonical);
|
|
629
712
|
if (resolved.model) {
|
|
630
713
|
evolclawModelOverride = { model: resolved.model, effort: resolved.effort };
|
|
631
714
|
effectiveModel = resolved.model;
|
|
@@ -636,7 +719,6 @@ export class MessageProcessor {
|
|
|
636
719
|
}
|
|
637
720
|
modelOverride = evolclawModelOverride;
|
|
638
721
|
}
|
|
639
|
-
const normalizedBaseagent = normalizeBaseagent(agent.name);
|
|
640
722
|
agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
|
|
641
723
|
// Kit renderer: 组装上下文
|
|
642
724
|
const pkgRoot = getPackageRoot();
|
|
@@ -680,7 +762,8 @@ export class MessageProcessor {
|
|
|
680
762
|
channel: currentChannelType || null,
|
|
681
763
|
venueUid: undefined,
|
|
682
764
|
// 群分发模式 / 客户端类型 / 权限模式
|
|
683
|
-
|
|
765
|
+
// 优先本地 session 覆盖(/dispatch 命令),fallback 到服务器 dispatch_mode 缓存
|
|
766
|
+
dispatch: (session.metadata?.dispatchModeOverride ?? session.metadata?.dispatchMode ?? message.dispatchMode) || undefined,
|
|
684
767
|
clientType: message.clientType || undefined,
|
|
685
768
|
permissionMode: session.metadata?.permissionMode || 'auto',
|
|
686
769
|
capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
|
|
@@ -706,6 +789,8 @@ export class MessageProcessor {
|
|
|
706
789
|
modelFallbackActive: (fbState.fallbackActive || skipEvolclawModel) ? true : undefined,
|
|
707
790
|
modelFallbackModel: (fbState.fallbackActive || skipEvolclawModel) ? (agentModel || undefined) : undefined,
|
|
708
791
|
agentSessionId: session.agentSessionId || undefined,
|
|
792
|
+
// 渲染模式:各类型当前激活的 modeName(从内存 config 读,渲染层据此选 manifest section)。
|
|
793
|
+
renderModes: this.agentRegistry?.resolveByChannel(channelKey)?.config?.render ?? undefined,
|
|
709
794
|
},
|
|
710
795
|
sessionId: session.id,
|
|
711
796
|
};
|
|
@@ -713,35 +798,73 @@ export class MessageProcessor {
|
|
|
713
798
|
if (kitContext)
|
|
714
799
|
contextParts.push(kitContext);
|
|
715
800
|
effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
|
|
801
|
+
// ── Stats: context_breakdown 旁路采集(各段估算 token 数,字符数/4 近似) ──
|
|
802
|
+
try {
|
|
803
|
+
const estTokens = (s) => s ? Math.ceil(s.length / 4) : 0;
|
|
804
|
+
const cbModel = effectiveModel || agentModel || 'unknown';
|
|
805
|
+
const cbMaxTokens = 200000; // 保守默认,后续可从 model-catalog 取
|
|
806
|
+
const systemPromptTokens = estTokens(options?.systemPromptAppend);
|
|
807
|
+
const personaTokens = estTokens(persona);
|
|
808
|
+
const workingTokens = estTokens(working);
|
|
809
|
+
const kitTokens = estTokens(kitContext);
|
|
810
|
+
const totalEst = estTokens(effectiveSystemPrompt);
|
|
811
|
+
insertContextBreakdown(resolveRoot(), {
|
|
812
|
+
ts: Date.now(),
|
|
813
|
+
agent_aid: selfAid || session.selfAID || '',
|
|
814
|
+
session_id: session.id,
|
|
815
|
+
turn_count: 0, // 按 ts 排序得轮次
|
|
816
|
+
model: cbModel,
|
|
817
|
+
max_tokens: cbMaxTokens,
|
|
818
|
+
system_prompt: systemPromptTokens + personaTokens + workingTokens,
|
|
819
|
+
system_tools: 0, // 工具 schema 不在此层,留 0(后续 runner 层补)
|
|
820
|
+
mcp_tools: 0,
|
|
821
|
+
custom_agents: 0,
|
|
822
|
+
memory_files: kitTokens, // ECK 渲染的所有段(含 memory/skills/rules)
|
|
823
|
+
skills: 0,
|
|
824
|
+
messages: 0, // messages 段在 runner 层才知道
|
|
825
|
+
free_space: Math.max(0, cbMaxTokens - totalEst),
|
|
826
|
+
total_estimated: totalEst,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
catch { /* non-fatal */ }
|
|
716
830
|
// 消息渲染层:用 message manifest 逐条渲染(时间 + 群聊发送者),组装成最终正文。
|
|
717
831
|
// 单条消息构造单元素 items;批量合并的消息 message.items 已由队列填充。
|
|
718
832
|
let renderResult;
|
|
719
833
|
const hasContent = message.content.trim() || (message.items && message.items.length > 0);
|
|
720
834
|
if (hasContent) {
|
|
835
|
+
const peerItems = message.items && message.items.length > 0
|
|
836
|
+
? message.items
|
|
837
|
+
: [{
|
|
838
|
+
peerId: message.peerId, peerName: peerName || undefined,
|
|
839
|
+
peerType: message.peerType,
|
|
840
|
+
sameDevice: message.sameDevice, sameNetwork: message.sameNetwork, sameEgressIp: message.sameEgressIp,
|
|
841
|
+
content: message.content, timestamp: message.timestamp,
|
|
842
|
+
images: message.images,
|
|
843
|
+
mentionAids: message.mentionAids,
|
|
844
|
+
}];
|
|
845
|
+
// 观察者插话(v0.3):消费 (对端, thread) 的待用提示,包成 owner-hint item 排在对端消息前。
|
|
846
|
+
// 一次性语义:consumeOwnerHints 读取并删除(见 pending-hints.ts)。在 try 外消费,
|
|
847
|
+
// 这样即便 renderMessageBody 抛错走 raw 兜底,也把提示原文拼进去——绝不静默丢提示。
|
|
848
|
+
const hintItems = this.consumeOwnerHints(session, message);
|
|
849
|
+
const renderItems = hintItems.length > 0 ? [...hintItems, ...peerItems] : peerItems;
|
|
721
850
|
try {
|
|
722
|
-
const renderItems = message.items && message.items.length > 0
|
|
723
|
-
? message.items
|
|
724
|
-
: [{
|
|
725
|
-
peerId: message.peerId, peerName: peerName || undefined,
|
|
726
|
-
peerType: message.peerType,
|
|
727
|
-
sameDevice: message.sameDevice, sameNetwork: message.sameNetwork, sameEgressIp: message.sameEgressIp,
|
|
728
|
-
content: message.content, timestamp: message.timestamp,
|
|
729
|
-
images: message.images,
|
|
730
|
-
mentionAids: message.mentionAids,
|
|
731
|
-
}];
|
|
732
851
|
renderResult = renderMessageBody(renderItems, kitCtx.vars, session.id);
|
|
733
852
|
if (renderResult.body.trim())
|
|
734
853
|
effectivePrompt = wrapPrompt(renderResult.body);
|
|
735
854
|
else
|
|
736
|
-
effectivePrompt = wrapPrompt(message.content);
|
|
855
|
+
effectivePrompt = wrapPrompt(composeHintFallback(hintItems, message.content));
|
|
737
856
|
}
|
|
738
857
|
catch (e) {
|
|
739
858
|
logger.warn(`[MessageProcessor] renderMessageBody failed, using raw content: ${e instanceof Error ? e.message : String(e)}`);
|
|
740
|
-
effectivePrompt = wrapPrompt(message.content);
|
|
859
|
+
effectivePrompt = wrapPrompt(composeHintFallback(hintItems, message.content));
|
|
741
860
|
}
|
|
742
861
|
}
|
|
743
862
|
// 可重试错误(403/429/5xx)指数退避重试,最多 3 次
|
|
744
863
|
const MAX_RETRIES = 3;
|
|
864
|
+
// Runner 开始执行前:将 Pin 升级为 CheckMark(表示"正在处理")
|
|
865
|
+
if (message.messageId && message.source !== 'trigger') {
|
|
866
|
+
adapter.promoteAck?.(message.messageId).catch(() => { });
|
|
867
|
+
}
|
|
745
868
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
746
869
|
let streamRegistered = false;
|
|
747
870
|
try {
|
|
@@ -810,12 +933,12 @@ export class MessageProcessor {
|
|
|
810
933
|
}
|
|
811
934
|
// prompt_too_long:SDK 以 complete 事件(非异常)返回,需在此处触发 compact
|
|
812
935
|
// 检测条件:terminalReason 明确为 prompt_too_long,或文本/errors 包含相关错误文本
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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);
|
|
819
942
|
if (isPromptTooLong) {
|
|
820
943
|
renderer.addNotice('上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
|
|
821
944
|
await renderer.flush();
|
|
@@ -826,11 +949,7 @@ export class MessageProcessor {
|
|
|
826
949
|
agent.registerStream(streamKey, retryStream);
|
|
827
950
|
streamResult = await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress);
|
|
828
951
|
// 重试后仍然 prompt_too_long:清理 renderer 中可能混入的错误文本,显示友好提示
|
|
829
|
-
const
|
|
830
|
-
const retryStillTooLong = streamResult.isError && (streamResult.terminalReason === 'prompt_too_long' ||
|
|
831
|
-
contextTooLongPattern.test(streamResult.lastReplyText) ||
|
|
832
|
-
contextTooLongPattern.test(retryErrorsText) ||
|
|
833
|
-
contextTooLongPattern.test(streamResult.fullText));
|
|
952
|
+
const retryStillTooLong = streamResult.isError && streamHitContextLimit(streamResult);
|
|
834
953
|
if (retryStillTooLong) {
|
|
835
954
|
renderer.addNotice(getContextTooLongHint(agent), 'warn', 'context-too-long', true);
|
|
836
955
|
}
|
|
@@ -839,10 +958,7 @@ export class MessageProcessor {
|
|
|
839
958
|
throw new Error('CONTEXT_COMPACT_FAILED');
|
|
840
959
|
}
|
|
841
960
|
}
|
|
842
|
-
else if (streamResult.isError &&
|
|
843
|
-
contextTooLongPattern.test(streamResult.lastReplyText) ||
|
|
844
|
-
contextTooLongPattern.test(errorsText) ||
|
|
845
|
-
contextTooLongPattern.test(streamResult.fullText))) {
|
|
961
|
+
else if (streamResult.isError && streamHitContextLimit(streamResult)) {
|
|
846
962
|
// 上下文过长但无法 auto-compact(无 session ID 或 agent 不支持),显示友好提示
|
|
847
963
|
renderer.addNotice(getContextTooLongHint(agent), 'warn', 'context-too-long', true);
|
|
848
964
|
}
|
|
@@ -971,12 +1087,6 @@ export class MessageProcessor {
|
|
|
971
1087
|
// Flush 剩余内容(文件标记已在 flush 时自动移除)
|
|
972
1088
|
await renderer.flush(true);
|
|
973
1089
|
}
|
|
974
|
-
// 更新 EvolAgent.lastActivity
|
|
975
|
-
if (this.agentRegistry) {
|
|
976
|
-
const owningAgent = this.agentRegistry.resolveByChannel(channelKey);
|
|
977
|
-
if (owningAgent)
|
|
978
|
-
owningAgent.lastActivity = Date.now();
|
|
979
|
-
}
|
|
980
1090
|
// 注意:不在此处清除 interruptedSessions,由下一条消息的 prompt 包装逻辑消费
|
|
981
1091
|
const interruptReason = this.interruptedSessions.get(session.id);
|
|
982
1092
|
if (streamResult.isError) {
|
|
@@ -984,55 +1094,193 @@ export class MessageProcessor {
|
|
|
984
1094
|
const errorSummary = streamResult.errors?.join('; ') || '任务执行失败';
|
|
985
1095
|
const rawSubtype = streamResult.subtype || 'agent_error';
|
|
986
1096
|
const errorType = prefixErrorType(ERROR_PREFIX.AGENT, rawSubtype);
|
|
987
|
-
|
|
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(() => { });
|
|
988
1103
|
adapter.send(envelope, { kind: 'status.error', metadata: { errorType: rawSubtype } }).catch(() => { });
|
|
1104
|
+
this.touchAgentActivity(channelKey);
|
|
989
1105
|
}
|
|
990
|
-
if (
|
|
991
|
-
|
|
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
|
+
});
|
|
992
1120
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
if (
|
|
1007
|
-
|
|
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
|
+
}
|
|
1008
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
|
+
});
|
|
1009
1150
|
}
|
|
1010
|
-
logger.message({
|
|
1011
|
-
msgId: messageId,
|
|
1012
|
-
sessionId: session.id,
|
|
1013
|
-
dir: 'inbound',
|
|
1014
|
-
status: 'failed',
|
|
1015
|
-
error: errorSummary,
|
|
1016
|
-
terminalReason: streamResult.terminalReason
|
|
1017
|
-
});
|
|
1018
1151
|
}
|
|
1019
1152
|
else {
|
|
1020
1153
|
// 真正的成功
|
|
1021
1154
|
const durationMs = Date.now() - startTime;
|
|
1155
|
+
// ── Stats: 写入 usage_events(在 status.completed 之前,以便带上 cost) ──
|
|
1156
|
+
let statsCostUsd = 0;
|
|
1157
|
+
let statsCostCny = 0;
|
|
1158
|
+
let statsCacheHitRate = 0;
|
|
1159
|
+
if (streamResult.tokenUsage) {
|
|
1160
|
+
try {
|
|
1161
|
+
const statsAgentAid = session.selfAID || message.selfAID || '';
|
|
1162
|
+
const statsPeerKey = formatPeerKey(message.channel, message.channelId);
|
|
1163
|
+
const statsModel = streamResult.contextUsage?.model || 'unknown';
|
|
1164
|
+
const ctxPct = streamResult.contextUsage?.percentage;
|
|
1165
|
+
const event = normalizeUsage(streamResult.tokenUsage, {
|
|
1166
|
+
ts: Date.now(),
|
|
1167
|
+
agent_aid: statsAgentAid,
|
|
1168
|
+
peer_key: statsPeerKey,
|
|
1169
|
+
peer_type: session.chatType || undefined,
|
|
1170
|
+
session_id: session.id,
|
|
1171
|
+
model: statsModel,
|
|
1172
|
+
turns: streamResult.numTurns,
|
|
1173
|
+
duration_ms: durationMs,
|
|
1174
|
+
context_window_pct: ctxPct,
|
|
1175
|
+
});
|
|
1176
|
+
insertUsageEvent(resolveRoot(), event);
|
|
1177
|
+
// 逐次大模型调用明细落库(model_calls 表)
|
|
1178
|
+
if (streamResult.modelCalls?.length) {
|
|
1179
|
+
const mcRows = streamResult.modelCalls.map(mc => ({
|
|
1180
|
+
ts: event.ts,
|
|
1181
|
+
task_id: taskId,
|
|
1182
|
+
session_id: session.id,
|
|
1183
|
+
agent_session_id: session.agentSessionId ?? undefined,
|
|
1184
|
+
agent_aid: statsAgentAid,
|
|
1185
|
+
peer_key: statsPeerKey,
|
|
1186
|
+
call_index: mc.call_index,
|
|
1187
|
+
model: mc.model || statsModel,
|
|
1188
|
+
request_id: mc.request_id,
|
|
1189
|
+
message_id: mc.message_id,
|
|
1190
|
+
input_tokens: mc.tokenUsage.input_tokens ?? 0,
|
|
1191
|
+
output_tokens: mc.tokenUsage.output_tokens ?? 0,
|
|
1192
|
+
cache_creation_tokens: mc.tokenUsage.cache_creation_input_tokens ?? 0,
|
|
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,
|
|
1197
|
+
degraded: mc.degraded ? 1 : 0,
|
|
1198
|
+
}));
|
|
1199
|
+
insertModelCalls(resolveRoot(), mcRows);
|
|
1200
|
+
}
|
|
1201
|
+
// 计算费用(用于合入 status.completed)
|
|
1202
|
+
const { calcCost } = await import('../stats/billing.js');
|
|
1203
|
+
const cost = calcCost(resolveRoot(), { ...event, ts: event.ts, model: event.model, billing_fn: event.billing_fn });
|
|
1204
|
+
statsCostUsd = cost.usd ?? 0;
|
|
1205
|
+
statsCostCny = cost.cny ?? 0;
|
|
1206
|
+
const totalIn = event.input_tokens + event.cache_read_tokens;
|
|
1207
|
+
statsCacheHitRate = totalIn > 0 ? Math.round((event.cache_read_tokens / totalIn) * 100) / 100 : 0;
|
|
1208
|
+
}
|
|
1209
|
+
catch (e) {
|
|
1210
|
+
logger.debug(`[MessageProcessor] Stats write failed (non-fatal): ${e}`);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
// 会话累计 + model spec(用于 status.completed 统计细目)
|
|
1214
|
+
let sessionStats;
|
|
1215
|
+
let modelSpec;
|
|
1216
|
+
try {
|
|
1217
|
+
const { openReadonlyDb, getDbPath } = await import('../stats/db.js');
|
|
1218
|
+
const { resolveModelSpec } = await import('../stats/billing.js');
|
|
1219
|
+
const statsModel = streamResult.contextUsage?.model || 'unknown';
|
|
1220
|
+
modelSpec = resolveModelSpec(resolveRoot(), statsModel);
|
|
1221
|
+
const rdb = openReadonlyDb(getDbPath(resolveRoot()));
|
|
1222
|
+
if (rdb) {
|
|
1223
|
+
try {
|
|
1224
|
+
const row = rdb.prepare(`SELECT COALESCE(SUM(input_tokens),0) AS input_tokens, COALESCE(SUM(output_tokens),0) AS output_tokens,
|
|
1225
|
+
COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens, COALESCE(SUM(cache_creation_tokens),0) AS cache_creation_tokens,
|
|
1226
|
+
COUNT(*) AS call_count FROM usage_events WHERE session_id = ?`).get(session.id);
|
|
1227
|
+
if (row) {
|
|
1228
|
+
// 逐行算费用太贵,用近似:最后一轮的 cost 乘以次数不准,所以这里用累加 token 近似
|
|
1229
|
+
sessionStats = {
|
|
1230
|
+
input_tokens: row.input_tokens,
|
|
1231
|
+
output_tokens: row.output_tokens,
|
|
1232
|
+
cache_read_tokens: row.cache_read_tokens,
|
|
1233
|
+
cache_creation_tokens: row.cache_creation_tokens,
|
|
1234
|
+
cost_usd: 0, cost_cny: 0,
|
|
1235
|
+
call_count: row.call_count,
|
|
1236
|
+
};
|
|
1237
|
+
// 快速费用估算:用会话所有行逐行算
|
|
1238
|
+
const rows = rdb.prepare(`SELECT * FROM usage_events WHERE session_id = ?`).all(session.id);
|
|
1239
|
+
const { calcCost: cc } = await import('../stats/billing.js');
|
|
1240
|
+
for (const r of rows) {
|
|
1241
|
+
const c = cc(resolveRoot(), r);
|
|
1242
|
+
sessionStats.cost_usd += c.usd ?? 0;
|
|
1243
|
+
sessionStats.cost_cny += c.cny ?? 0;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
finally {
|
|
1248
|
+
rdb.close();
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
catch { /* non-fatal */ }
|
|
1022
1253
|
if (message.source !== 'trigger') {
|
|
1254
|
+
this.touchAgentActivity(channelKey);
|
|
1023
1255
|
if (interruptReason) {
|
|
1024
1256
|
adapter.send(envelope, { kind: 'status.interrupted', metadata: { reason: interruptReason } }).catch(() => { });
|
|
1025
1257
|
}
|
|
1026
1258
|
else {
|
|
1027
|
-
adapter.send(envelope, { kind: 'status.completed', metadata: {
|
|
1259
|
+
adapter.send(envelope, { kind: 'status.completed', metadata: {
|
|
1260
|
+
durationMs,
|
|
1261
|
+
ttftMs: streamResult.ttftMs,
|
|
1262
|
+
numTurns: streamResult.numTurns,
|
|
1263
|
+
tokenUsage: streamResult.tokenUsage,
|
|
1264
|
+
contextUsage: streamResult.contextUsage,
|
|
1265
|
+
lastModelCall: streamResult.lastModelCall,
|
|
1266
|
+
cost_usd: statsCostUsd,
|
|
1267
|
+
cost_cny: statsCostCny,
|
|
1268
|
+
cache_hit_rate: statsCacheHitRate,
|
|
1269
|
+
model_spec: modelSpec,
|
|
1270
|
+
session_total: sessionStats,
|
|
1271
|
+
queue: {
|
|
1272
|
+
pending: this.messageQueue?.getQueueLength(session.id) ?? 0,
|
|
1273
|
+
processing: this.messageQueue?.isProcessing(session.id) ? 1 : 0,
|
|
1274
|
+
},
|
|
1275
|
+
} }).catch(() => { });
|
|
1028
1276
|
}
|
|
1029
1277
|
}
|
|
1030
1278
|
if (message.triggerMeta) {
|
|
1031
1279
|
if (interruptReason) {
|
|
1032
|
-
this.eventBus.publish({ type: 'trigger:skipped', triggerId: message.triggerMeta.triggerId, reason: 'interrupted' });
|
|
1280
|
+
this.eventBus.publish({ type: 'trigger:skipped', triggerId: message.triggerMeta.triggerId, name: message.triggerMeta.triggerName ?? '', reason: 'interrupted', targetChannel: message.channel, targetChannelId: message.channelId });
|
|
1033
1281
|
}
|
|
1034
1282
|
else {
|
|
1035
|
-
this.eventBus.publish({ type: 'trigger:completed', triggerId: message.triggerMeta.triggerId, messageId: messageId, durationMs });
|
|
1283
|
+
this.eventBus.publish({ type: 'trigger:completed', triggerId: message.triggerMeta.triggerId, name: message.triggerMeta.triggerName ?? '', messageId: messageId, durationMs, targetChannel: message.channel, targetChannelId: message.channelId, fireTime: message.triggerMeta.fireTime ?? 0 });
|
|
1036
1284
|
}
|
|
1037
1285
|
}
|
|
1038
1286
|
await this.sessionManager.recordSuccess(session.id);
|
|
@@ -1098,6 +1346,7 @@ export class MessageProcessor {
|
|
|
1098
1346
|
? { kind: 'status.interrupted', metadata: { reason: 'stream_error' } }
|
|
1099
1347
|
: { kind: 'status.error' };
|
|
1100
1348
|
adapter.send(envelope, statusPayload).catch(() => { });
|
|
1349
|
+
this.touchAgentActivity(channelKey);
|
|
1101
1350
|
}
|
|
1102
1351
|
// 用户主动中断时降级日志;其余仍按 error 记录
|
|
1103
1352
|
if (isUserInterrupt) {
|
|
@@ -1108,19 +1357,25 @@ export class MessageProcessor {
|
|
|
1108
1357
|
}
|
|
1109
1358
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1110
1359
|
const errorType = prefixErrorType(ERROR_PREFIX.INFRA, errType);
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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
|
+
}
|
|
1118
1373
|
// 记录处理失败
|
|
1119
1374
|
logger.message({
|
|
1120
1375
|
msgId: messageId,
|
|
1121
1376
|
sessionId: session.id,
|
|
1122
1377
|
dir: 'inbound',
|
|
1123
|
-
status: 'failed',
|
|
1378
|
+
status: isUserInterrupt ? 'interrupted' : 'failed',
|
|
1124
1379
|
error: error instanceof Error ? error.message : String(error)
|
|
1125
1380
|
});
|
|
1126
1381
|
if (error instanceof Error && !isUserInterrupt) {
|
|
@@ -1154,23 +1409,98 @@ export class MessageProcessor {
|
|
|
1154
1409
|
...(sendOpts ?? {}),
|
|
1155
1410
|
metadata: { ...(sendOpts?.metadata ?? {}), taskId, chatmode },
|
|
1156
1411
|
};
|
|
1157
|
-
const errorPayload =
|
|
1158
|
-
|
|
1159
|
-
|
|
1412
|
+
const errorPayload = {
|
|
1413
|
+
kind: 'result.error',
|
|
1414
|
+
text: userMessage,
|
|
1415
|
+
reason: isTimeout ? 'timeout' : errType,
|
|
1416
|
+
};
|
|
1160
1417
|
await adapter.send({ ...envelope, replyContext: sendOpts }, errorPayload);
|
|
1161
1418
|
// Proactive 可观测:catch 块的基础设施错误也透传为 thought,保证按 task_id 聚合完整
|
|
1162
1419
|
}
|
|
1163
1420
|
}
|
|
1164
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
|
+
}
|
|
1165
1489
|
/**
|
|
1166
1490
|
* 解析会话和项目路径
|
|
1167
1491
|
*/
|
|
1168
1492
|
async resolveSession(message) {
|
|
1169
|
-
//
|
|
1170
|
-
const metadata =
|
|
1171
|
-
? {
|
|
1493
|
+
// 话题会话创建时写入创建者和 replyContext(threadId 路由);主会话不写(避免群聊覆盖)
|
|
1494
|
+
const metadata = message.threadId
|
|
1495
|
+
? {
|
|
1496
|
+
...(message.replyContext ? { replyContext: message.replyContext } : {}),
|
|
1497
|
+
...(message.peerId ? { peerId: message.peerId } : {}),
|
|
1498
|
+
...(message.peerName ? { peerName: message.peerName } : {}),
|
|
1499
|
+
}
|
|
1172
1500
|
: undefined;
|
|
1173
1501
|
const projectPath = this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd();
|
|
1502
|
+
// 话题创建权限守卫已统一移至 MessageBridge.canCreateThreadSession(enqueue 前拦截),
|
|
1503
|
+
// 此处不再重复检查——bridge 层拒绝后消息根本不会到达 processMessage。
|
|
1174
1504
|
// current strategy: resume bound session, make it active so output is not suppressed
|
|
1175
1505
|
if (message.triggerMeta?.boundSessionId) {
|
|
1176
1506
|
const bound = await this.sessionManager.getSessionById(message.triggerMeta.boundSessionId);
|
|
@@ -1187,7 +1517,7 @@ export class MessageProcessor {
|
|
|
1187
1517
|
logger.warn(`[MessageProcessor] Bound session ${message.triggerMeta.boundSessionId} not found, falling back to latest`);
|
|
1188
1518
|
}
|
|
1189
1519
|
}
|
|
1190
|
-
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata,
|
|
1520
|
+
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, message.topicName, message.peerId, message.chatType, undefined, message.selfAID, message.channelType, message.peerType);
|
|
1191
1521
|
// 兜底纠正1:群聊强制 proactive
|
|
1192
1522
|
if (message.chatType === 'group' && session.sessionMode !== 'proactive') {
|
|
1193
1523
|
logger.info(`[MessageProcessor] group proactive upgrade: sessionId=${session.id} ${session.sessionMode} -> proactive`);
|
|
@@ -1204,6 +1534,14 @@ export class MessageProcessor {
|
|
|
1204
1534
|
await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
|
|
1205
1535
|
}
|
|
1206
1536
|
}
|
|
1537
|
+
// 群聊分发模式同步:aun.ts 从服务器信封解析的 dispatchMode 注入到 message,
|
|
1538
|
+
// 此处写入 session.metadata,确保 ECK 上下文的 venue fragment 正确渲染 dispatch 变量。
|
|
1539
|
+
// 仅当 message.dispatchMode 有值且与 session 记录不一致时更新。
|
|
1540
|
+
if (message.chatType === 'group' && message.dispatchMode && session.metadata?.dispatchMode !== message.dispatchMode) {
|
|
1541
|
+
logger.info(`[MessageProcessor] dispatchMode sync: sessionId=${session.id} ${session.metadata?.dispatchMode ?? 'none'} -> ${message.dispatchMode}`);
|
|
1542
|
+
session.metadata = { ...(session.metadata || {}), dispatchMode: message.dispatchMode };
|
|
1543
|
+
await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
|
|
1544
|
+
}
|
|
1207
1545
|
// 兜底纠正2:旧 session 创建时没传 peerType(建为 interactive),后续非 human 消息进来时升级为 proactive。
|
|
1208
1546
|
// 新建场景已由 getOrCreateSession 内部 resolveDefaultSessionMode 处理,这里只兜底历史会话。
|
|
1209
1547
|
if (message.peerType && message.peerType !== 'human' && message.peerType !== 'unknown' && session.sessionMode !== 'proactive') {
|
|
@@ -1278,6 +1616,7 @@ export class MessageProcessor {
|
|
|
1278
1616
|
// session_id 已在 AgentRunner.transformStream 中处理,此处仅记录
|
|
1279
1617
|
if (event.type === 'session_id') {
|
|
1280
1618
|
logger.debug(`[MessageProcessor] Session ID updated: ${event.sessionId} for session: ${session.id}`);
|
|
1619
|
+
session.agentSessionId = event.sessionId;
|
|
1281
1620
|
continue;
|
|
1282
1621
|
}
|
|
1283
1622
|
// session 状态变更(idle/running/requires_action)
|
|
@@ -1359,7 +1698,6 @@ export class MessageProcessor {
|
|
|
1359
1698
|
sessionId: session.id,
|
|
1360
1699
|
toolName: event.name,
|
|
1361
1700
|
isError: event.isError,
|
|
1362
|
-
content: event.result,
|
|
1363
1701
|
agentName: agentNameForStats,
|
|
1364
1702
|
timestamp: Date.now()
|
|
1365
1703
|
});
|
|
@@ -1382,7 +1720,7 @@ export class MessageProcessor {
|
|
|
1382
1720
|
// 记录错误文本到 lastReplyText,供后续 isPromptTooLong 检测
|
|
1383
1721
|
lastReplyText += event.error || '';
|
|
1384
1722
|
// 上下文过长的错误不在此处输出 notice,留给外层 isPromptTooLong 触发 auto-compact
|
|
1385
|
-
const isContextError =
|
|
1723
|
+
const isContextError = isContextTooLongText(event.error || '');
|
|
1386
1724
|
if (!isContextError && !hasErrorResult && !shouldSuppress()) {
|
|
1387
1725
|
hasErrorResult = true;
|
|
1388
1726
|
renderer.addNotice(`${event.error}`, 'warn', 'runtime-error', true);
|
|
@@ -1400,7 +1738,7 @@ export class MessageProcessor {
|
|
|
1400
1738
|
logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
|
|
1401
1739
|
}
|
|
1402
1740
|
// 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
|
|
1403
|
-
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 };
|
|
1741
|
+
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, lastModelCall: event.lastModelCall, modelCalls: event.modelCalls };
|
|
1404
1742
|
// thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
|
|
1405
1743
|
// 由那里按 LLM 输出的每个 text item 单独写一条,此处不再写。
|
|
1406
1744
|
// 失败且无前置错误输出:显示 errors 摘要
|
|
@@ -1409,8 +1747,8 @@ export class MessageProcessor {
|
|
|
1409
1747
|
const interruptReason = this.interruptedSessions.get(session.id);
|
|
1410
1748
|
const isUserInterrupt = interruptReason === 'new_message' || interruptReason === 'stop' || interruptReason === 'recalled';
|
|
1411
1749
|
const isContextTooLong = event.terminalReason === 'prompt_too_long'
|
|
1412
|
-
||
|
|
1413
|
-
||
|
|
1750
|
+
|| isContextTooLongText(event.errors?.join(' ') || '')
|
|
1751
|
+
|| isContextTooLongText(lastReplyText);
|
|
1414
1752
|
if (event.isError && !hasErrorResult && !shouldSuppress() && !isUserInterrupt && !isContextTooLong) {
|
|
1415
1753
|
const errorSummary = event.errors?.join('; ') || '任务执行失败';
|
|
1416
1754
|
// 使用 terminalReason 提供更友好的错误提示(不带 emoji,由 formatter 统一加)
|
|
@@ -1456,7 +1794,7 @@ export class MessageProcessor {
|
|
|
1456
1794
|
logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
|
|
1457
1795
|
}
|
|
1458
1796
|
// 记录完成状态
|
|
1459
|
-
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 };
|
|
1797
|
+
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, lastModelCall: event.lastModelCall };
|
|
1460
1798
|
if (event.subtype === 'success') {
|
|
1461
1799
|
this.messageCache.addEvent(session.id, {
|
|
1462
1800
|
type: 'completed',
|