evolcore 0.0.2 → 0.0.4
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 +93 -778
- package/README.md +45 -10
- package/bin/ec-safe-output.js +89 -24
- package/dist/agents/baseagent.js +46 -0
- package/dist/agents/claude-runner.js +198 -48
- package/dist/agents/codex-app-server-client.js +68 -0
- package/dist/agents/codex-runner.js +203 -8
- package/dist/agents/runner-types.js +2 -2
- package/dist/aun/aid/agentmd.js +79 -0
- package/dist/aun/aid/encryption-seed-policy.js +29 -0
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +2 -5
- package/dist/aun/outbox.js +8 -0
- package/dist/channels/aun.js +241 -116
- package/dist/channels/daemon.js +18 -4
- package/dist/channels/dingtalk.js +1005 -260
- package/dist/channels/feishu.js +184 -56
- package/dist/channels/qqbot.js +23 -1
- package/dist/channels/wechat.js +303 -155
- package/dist/channels/wecom.js +1132 -164
- package/dist/cli/agent-command.js +2 -1
- package/dist/cli/aun-commands.js +98 -43
- package/dist/cli/bench.js +2 -2
- package/dist/cli/contact.js +71 -0
- package/dist/cli/ctl-command.js +2 -2
- package/dist/cli/daemon-commands.js +77 -214
- package/dist/cli/handoff-command.js +4 -3
- package/dist/cli/help.js +9 -5
- package/dist/cli/index.js +132 -116
- package/dist/cli/init-channel.js +242 -129
- package/dist/cli/init.js +70 -31
- package/dist/cli/model.js +2 -1
- package/dist/cli/net-check.js +2 -2
- package/dist/cli/queue-command.js +30 -6
- package/dist/cli/raw-key-input.js +25 -0
- package/dist/cli/response.js +5 -6
- package/dist/cli/restart-monitor.js +25 -1
- package/dist/cli/stats.js +6 -4
- package/dist/cli/trigger-command.js +146 -25
- package/dist/cli/version.js +6 -1
- package/dist/config/access-policy-domain.js +38 -0
- package/dist/config/access-policy.js +134 -0
- package/dist/config/builtin-role-templates.js +42 -17
- package/dist/config/builtin-roles.js +31 -6
- package/dist/config/config-field-policy.js +17 -5
- package/dist/config/config-manager.js +409 -29
- package/dist/config/config-operation-service.js +35 -38
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-bind-code.js +274 -0
- package/dist/config/contact-book-store.js +622 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +185 -300
- package/dist/config/contact-operation-service.js +116 -0
- package/dist/config/contact-request-service.js +331 -0
- package/dist/config/peer-role-resolver.js +136 -55
- package/dist/config/role-ranks.js +18 -0
- package/dist/config/role-service.js +16 -19
- package/dist/config/role-store.js +16 -5
- package/dist/config/roles.js +10 -1
- package/dist/config/schema-registry.js +49 -24
- package/dist/config-store.js +14 -4
- package/dist/core/auth/agent-delegation.js +105 -11
- package/dist/core/auth/authorization-audit.js +16 -1
- package/dist/core/auth/operation-authorizer.js +10 -0
- package/dist/core/auth/operation-catalog.js +105 -1
- package/dist/core/auth/trigger-authorization.js +15 -0
- package/dist/core/bootstrap-service.js +2 -9
- package/dist/core/channel-loader.js +6 -2
- package/dist/core/command/command-handler.js +113 -21
- package/dist/core/command/connect-menu.js +599 -0
- package/dist/core/command/evol-menu-version-gate.js +38 -0
- package/dist/core/command/group-menu.js +421 -0
- package/dist/core/command/menu-handler.js +382 -100
- package/dist/core/command/menu-protocol.js +8 -2
- package/dist/core/command/menu-token-store.js +102 -0
- package/dist/core/command/role-menu.js +128 -29
- package/dist/core/command/slash-gate.js +1 -1
- package/dist/core/command/slash-handler.js +186 -49
- package/dist/core/daemon-file-cache.js +40 -6
- package/dist/core/event-catalog.js +99 -0
- package/dist/core/evolagent-registry.js +4 -40
- package/dist/core/evolagent.js +20 -9
- package/dist/core/handoff/runtime.js +162 -37
- package/dist/core/handoff/store.js +46 -2
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/inference/text-inference.js +16 -74
- package/dist/core/message/file-markers.js +7 -0
- package/dist/core/message/im-renderer.js +20 -14
- package/dist/core/message/inbound-admission.js +124 -0
- package/dist/core/message/message-bridge.js +670 -57
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +195 -10
- package/dist/core/message/message-utils.js +8 -2
- package/dist/core/message/peer-mode.js +7 -8
- package/dist/core/message/response-engine.js +642 -96
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +11 -2
- package/dist/core/permission/approval-gateway.js +17 -10
- package/dist/core/permission/ec-command-parser.js +101 -46
- package/dist/core/permission/tool-policy.js +115 -38
- package/dist/core/protected-paths.js +38 -11
- package/dist/core/session/session-fs-store.js +44 -3
- package/dist/core/session/session-manager.js +86 -8
- package/dist/core/session/session-mapper.js +2 -0
- package/dist/core/session/session-renew.js +125 -69
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/eck/kit-renderer.js +12 -1
- package/dist/eck/message-renderer.js +79 -1
- package/dist/index.js +285 -94
- package/dist/ipc.js +114 -8
- package/dist/paths.js +3 -0
- package/dist/response-system/config-resolver.js +29 -0
- package/dist/response-system/coordinator.js +8 -32
- package/dist/response-system/engines/v1/proactive-flow.js +1 -1
- package/dist/response-system/index.js +1 -0
- package/dist/response-system/modes/single-session/index.js +7 -4
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/manager.js +3 -0
- package/dist/trigger/parser.js +126 -11
- package/dist/trigger/patch.js +12 -2
- package/dist/trigger/scheduler.js +444 -40
- package/dist/trigger/validation.js +13 -0
- package/dist/utils/aid-bind.js +43 -29
- package/dist/utils/error-dict.json +7 -0
- package/dist/utils/evolcore-version.js +21 -0
- package/dist/utils/instance-registry.js +14 -7
- package/dist/utils/log-writer.js +46 -0
- package/dist/utils/logger.js +2 -2
- package/dist/utils/media-cache.js +4 -1
- package/dist/utils/model-prices.jsonl +6 -3
- package/dist/utils/restart-safety.js +31 -0
- package/dist/utils/stable-semver.js +21 -0
- package/dist/utils/stats.js +33 -9
- package/dist/utils/system-memory.js +62 -0
- package/kits/docs/INDEX.md +2 -1
- package/kits/docs/channels/aun.md +4 -13
- package/kits/docs/evolcore/INDEX.md +5 -3
- package/kits/docs/evolcore/agent.md +9 -1
- package/kits/docs/evolcore/aid.md +5 -2
- package/kits/docs/evolcore/config.md +47 -2
- package/kits/docs/evolcore/contact.md +61 -0
- package/kits/docs/evolcore/fs.md +9 -0
- package/kits/docs/evolcore/group-rules.md +46 -4
- package/kits/docs/evolcore/group.md +15 -6
- package/kits/docs/evolcore/model.md +4 -1
- package/kits/docs/evolcore/msg.md +12 -6
- package/kits/docs/evolcore/response.md +16 -21
- package/kits/docs/evolcore/rpc.md +2 -0
- package/kits/docs/evolcore/stats.md +15 -2
- package/kits/docs/evolcore/storage.md +1 -0
- package/kits/docs/evolcore/trigger.md +42 -10
- package/kits/docs/path-registry.md +36 -17
- package/kits/eck_manifest.json +12 -0
- package/kits/eck_message_manifest.json +12 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
- package/kits/rules/01-overview.md +17 -7
- package/kits/rules/02-navigation.md +38 -18
- package/kits/rules/03-identity.md +28 -24
- package/kits/rules/04-relation.md +44 -28
- package/kits/rules/05-venue.md +31 -15
- package/kits/rules/06-channel.md +11 -7
- package/kits/schemas/_meta.json +21 -7
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/agent-config.schema.7.json +303 -0
- package/kits/schemas/agent-config.schema.8.json +304 -0
- package/kits/schemas/agent-config.schema.9.json +374 -0
- package/kits/schemas/contact-book.schema.2.json +43 -0
- package/kits/schemas/contact-book.schema.3.json +67 -0
- package/kits/schemas/daemon.schema.1.json +3 -2
- package/kits/schemas/daemon.schema.2.json +101 -0
- package/kits/schemas/daemon.schema.3.json +123 -0
- package/kits/schemas/defaults.schema.2.json +85 -0
- package/kits/schemas/defaults.schema.3.json +73 -0
- package/kits/schemas/relation-config.schema.5.json +47 -0
- package/kits/schemas/relation-config.schema.6.json +46 -0
- package/kits/schemas/relation-config.schema.7.json +59 -0
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/role-registry.schema.1.json +2 -2
- package/kits/schemas/single-session.schema.2.json +57 -0
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
- package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
- package/kits/templates/roles/admin.json +47 -0
- package/kits/templates/roles/member.json +5 -0
- package/kits/templates/roles/owner.json +1 -0
- package/kits/templates/roles/visitor.json +5 -0
- package/kits/templates/system-fragments/channel.md +12 -2
- package/kits/templates/system-fragments/commands.md +3 -1
- package/kits/templates/system-fragments/identity.md +3 -1
- package/kits/templates/system-fragments/relation.md +1 -1
- package/kits/templates/system-fragments/session.md +6 -2
- package/package.json +7 -5
- package/MIGRATION-0.5.0.md +0 -378
- package/ROLE_ACCESS_CONTROL.md +0 -174
- package/assets/brand/evolcore/README.md +0 -19
- package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
- package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
- package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
- package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
- package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
- package/assets/brand/evolcore/evolcore-logo.png +0 -0
- package/assets/brand/evolcore/evolcore-logo.svg +0 -14
- package/assets/brand/evolcore/evolcore-mark.png +0 -0
- package/assets/brand/evolcore/evolcore-mark.svg +0 -10
|
@@ -5,6 +5,7 @@ import crypto from 'crypto';
|
|
|
5
5
|
import { BaseagentRunnerUnavailableError, hasCompact, hasClearSession, autoCompactWindowForModel, isClaudeContextUsageModel } from '../../agents/runner-types.js';
|
|
6
6
|
import { buildTaskRuntimeEnv } from '../../cli/task-context.js';
|
|
7
7
|
import { IMRenderer } from './im-renderer.js';
|
|
8
|
+
import { createSendFileMarkerPattern } from './file-markers.js';
|
|
8
9
|
import { createTextInferenceProvider } from '../inference/text-inference.js';
|
|
9
10
|
import { StreamIdleMonitor } from './stream-idle-monitor.js';
|
|
10
11
|
import { logger } from '../../utils/logger.js';
|
|
@@ -90,6 +91,45 @@ export function normalizeCompleteAgentEvent(event) {
|
|
|
90
91
|
terminalReason: normalizedTerminalReason,
|
|
91
92
|
};
|
|
92
93
|
}
|
|
94
|
+
class RetryAttemptTimeoutError extends Error {
|
|
95
|
+
timeoutMs;
|
|
96
|
+
constructor(timeoutMs) {
|
|
97
|
+
super(`RETRY_ATTEMPT_STALLED: no agent events for ${Math.round(timeoutMs / 1000)}s`);
|
|
98
|
+
this.timeoutMs = timeoutMs;
|
|
99
|
+
this.name = 'RetryAttemptTimeoutError';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function createRetryAttemptTimeout(timeoutMs) {
|
|
103
|
+
let timer;
|
|
104
|
+
let settled = false;
|
|
105
|
+
let rejectTimeout;
|
|
106
|
+
const promise = new Promise((_, reject) => {
|
|
107
|
+
rejectTimeout = reject;
|
|
108
|
+
});
|
|
109
|
+
const arm = () => {
|
|
110
|
+
if (settled)
|
|
111
|
+
return;
|
|
112
|
+
if (timer)
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
timer = setTimeout(() => {
|
|
115
|
+
settled = true;
|
|
116
|
+
timer = undefined;
|
|
117
|
+
rejectTimeout(new RetryAttemptTimeoutError(timeoutMs));
|
|
118
|
+
}, timeoutMs);
|
|
119
|
+
timer.unref?.();
|
|
120
|
+
};
|
|
121
|
+
arm();
|
|
122
|
+
return {
|
|
123
|
+
promise,
|
|
124
|
+
reset: arm,
|
|
125
|
+
clear: () => {
|
|
126
|
+
settled = true;
|
|
127
|
+
if (timer)
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
timer = undefined;
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
93
133
|
const RETRY_EXHAUSTED_MARK = Symbol('evolcore.retryExhausted');
|
|
94
134
|
const RETRY_MADE_PROGRESS_MARK = Symbol('evolcore.retryMadeProgress');
|
|
95
135
|
const RETRY_HEALTH_RECORDED_MARK = Symbol('evolcore.retryHealthRecorded');
|
|
@@ -123,6 +163,12 @@ function markRetryHealthRecorded(error) {
|
|
|
123
163
|
function wasRetryHealthRecorded(error) {
|
|
124
164
|
return !!(error && typeof error === 'object' && error[RETRY_HEALTH_RECORDED_MARK] === true);
|
|
125
165
|
}
|
|
166
|
+
function isNonErrorInterruptReason(reason) {
|
|
167
|
+
return reason === 'new_message'
|
|
168
|
+
|| reason === 'stop'
|
|
169
|
+
|| reason === 'recalled'
|
|
170
|
+
|| reason === 'daemon_restart';
|
|
171
|
+
}
|
|
126
172
|
function formatRetryableErrorFinalMessage(error, retries) {
|
|
127
173
|
const reason = getErrorMessage(error, undefined, false) || 'API 暂时不可用';
|
|
128
174
|
return `❌ API 暂时不可用,已自动重试 ${retries} 次仍失败,任务已停止。\n原因:${reason}`;
|
|
@@ -171,6 +217,25 @@ function streamHitContextLimit(result) {
|
|
|
171
217
|
isContextTooLongText(result.errors?.join(' ') || '') ||
|
|
172
218
|
isContextTooLongText(result.fullText);
|
|
173
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* `isContextTooLongText` 是给错误分类用的宽松匹配,不能拿来删除模型正文:正常回答
|
|
222
|
+
* 可能会说明或引用 `Prompt is too long`。这里只识别网关会单独输出的短错误行。
|
|
223
|
+
*/
|
|
224
|
+
function isStandaloneContextLimitErrorLine(line) {
|
|
225
|
+
return /^(?:(?:api )?error\s*:\s*)?(?:prompt is too long|input is too long|context too long|context_length_exceeded|context_window_exceeded|context window limit|maximum context length|上下文过长)\s*[.!。]?$/i.test(line.trim());
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* 网关有时把上下文超限错误包装进 text event。只丢弃独立错误行,保留同一 event
|
|
229
|
+
* 中其它行及包含错误字面量的正常输出;原文本仍由调用方保留作恢复判定。
|
|
230
|
+
*/
|
|
231
|
+
function stripContextLimitErrorLines(text) {
|
|
232
|
+
if (!isContextTooLongText(text))
|
|
233
|
+
return text;
|
|
234
|
+
return text
|
|
235
|
+
.split(/\r?\n/)
|
|
236
|
+
.filter(line => !isStandaloneContextLimitErrorLine(line))
|
|
237
|
+
.join('\n');
|
|
238
|
+
}
|
|
174
239
|
const SHELL_CONTROL_RE = /[;&|`]|[$][(]|\r|\n/;
|
|
175
240
|
function isCtlQueueReadCommand(toolName, input) {
|
|
176
241
|
if (toolName !== 'Bash' && toolName !== 'Shell')
|
|
@@ -230,6 +295,15 @@ function canCompactAgent(agent) {
|
|
|
230
295
|
function canClearAgent(agent) {
|
|
231
296
|
return hasClearSession(agent) && agent.capabilities?.clear !== false;
|
|
232
297
|
}
|
|
298
|
+
export async function resolveGroupRulesForTurn(opts, sync = syncGroupRulesContext) {
|
|
299
|
+
if (!opts.isAunGroup)
|
|
300
|
+
return {};
|
|
301
|
+
if (opts.mode === 'ignore') {
|
|
302
|
+
logger.debug(`[GroupRules] skipped by local policy: group=${opts.groupId ?? '<unknown>'} mode=ignore`);
|
|
303
|
+
return { groupRulesStatus: 'ignored' };
|
|
304
|
+
}
|
|
305
|
+
return sync({ selfAid: opts.selfAid, groupId: opts.groupId, channel: opts.channel });
|
|
306
|
+
}
|
|
233
307
|
function autoCompactTokensFromMaxTokens(maxTokens) {
|
|
234
308
|
if (!maxTokens || maxTokens <= 0)
|
|
235
309
|
return undefined;
|
|
@@ -259,8 +333,10 @@ export class ResponseEngine {
|
|
|
259
333
|
commandHandler;
|
|
260
334
|
channels = new Map();
|
|
261
335
|
channelTypeMap = new Map(); // channelType → channelName(首个实例)
|
|
262
|
-
|
|
263
|
-
|
|
336
|
+
/** 仅登记正在处理的任务,runner 的 session 回调绝不能投递到其它会话。 */
|
|
337
|
+
activeRenderers = new Map();
|
|
338
|
+
/** 已主动发送开始提示时,忽略 runner 对同一次 compact 的回调。 */
|
|
339
|
+
announcedCompactStarts = new Set();
|
|
264
340
|
agentMap;
|
|
265
341
|
primaryRunnerKey;
|
|
266
342
|
interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
|
|
@@ -274,6 +350,10 @@ export class ResponseEngine {
|
|
|
274
350
|
activeMonitors = new Map();
|
|
275
351
|
/** sessionId → 当前正在处理任务的运行时上下文,供 in-task CLI 通过 IPC 查询。 */
|
|
276
352
|
activeTaskRuntimeContexts = new Map();
|
|
353
|
+
/** task.run started 后立即登记,覆盖运行时上下文尚未建立的 shutdown 窗口。 */
|
|
354
|
+
activeTaskSpans = new Map();
|
|
355
|
+
/** sessionId → 尚未收到 runner:task-notification 的子任务。 */
|
|
356
|
+
activeRunnerTasks = new Map();
|
|
277
357
|
triggerTerminalMessages = new WeakSet();
|
|
278
358
|
agentDelegationRegistry;
|
|
279
359
|
turnCoordinator;
|
|
@@ -402,6 +482,14 @@ export class ResponseEngine {
|
|
|
402
482
|
this.agentDelegationRegistry?.revokeSession(event.sessionId);
|
|
403
483
|
}
|
|
404
484
|
});
|
|
485
|
+
this.eventBus.subscribe('task:completed', event => {
|
|
486
|
+
if ('sessionId' in event && event.sessionId)
|
|
487
|
+
this.activeTaskSpans.delete(event.sessionId);
|
|
488
|
+
});
|
|
489
|
+
this.eventBus.subscribe('task:error', event => {
|
|
490
|
+
if ('sessionId' in event && event.sessionId)
|
|
491
|
+
this.activeTaskSpans.delete(event.sessionId);
|
|
492
|
+
});
|
|
405
493
|
// 初始化响应模式协调器,注册内置模式(interactive/proactive)
|
|
406
494
|
const registry = new ResponseModeRegistry();
|
|
407
495
|
registerBuiltinModes(registry);
|
|
@@ -428,21 +516,107 @@ export class ResponseEngine {
|
|
|
428
516
|
if (!session)
|
|
429
517
|
return;
|
|
430
518
|
this.turnCoordinator.hydrate(session);
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
519
|
+
const effectiveReason = reason === 'daemon_restart'
|
|
520
|
+
|| this.interruptedSessions.get(sessionId) === 'daemon_restart'
|
|
521
|
+
? 'daemon_restart'
|
|
522
|
+
: reason;
|
|
523
|
+
const interruptedTurn = this.turnCoordinator.invalidate(sessionId, effectiveReason);
|
|
524
|
+
const interruptedTaskId = interruptedTurn?.active?.taskId;
|
|
525
|
+
this.interruptedSessions.set(sessionId, effectiveReason);
|
|
526
|
+
this.messageQueue?.markActiveTerminal?.(sessionId, effectiveReason);
|
|
527
|
+
// 终态必须先于渠道发送和 runner I/O 落盘。否则任一外部调用卡住时,
|
|
528
|
+
// shutdown 超时退出会再次留下 started/interrupting 的悬空任务。
|
|
529
|
+
const runtimeContext = this.activeTaskRuntimeContexts.get(sessionId);
|
|
530
|
+
const taskSpan = this.activeTaskSpans.get(sessionId);
|
|
531
|
+
const taskId = (await this.turnCoordinator.completeInterrupt(session, effectiveReason))
|
|
532
|
+
?? interruptedTaskId
|
|
533
|
+
?? taskSpan?.taskId;
|
|
439
534
|
if (taskId) {
|
|
535
|
+
const taskCausation = taskSpan?.causation ?? runtimeContext?.causation;
|
|
536
|
+
if (taskCausation && effectiveReason === 'daemon_restart') {
|
|
537
|
+
recordCausationSpan(taskCausation, 'task.run', {
|
|
538
|
+
status: 'interrupted',
|
|
539
|
+
refs: {
|
|
540
|
+
taskId,
|
|
541
|
+
sessionId,
|
|
542
|
+
messageId: taskSpan?.messageId ?? runtimeContext?.messageId,
|
|
543
|
+
},
|
|
544
|
+
reason: effectiveReason,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
this.activeTaskSpans.delete(sessionId);
|
|
440
548
|
this.sessionManager.clearProcessingIfTask(sessionId, taskId);
|
|
441
549
|
if (this.activeTaskRuntimeContexts.get(sessionId)?.taskId === taskId) {
|
|
442
550
|
this.activeTaskRuntimeContexts.delete(sessionId);
|
|
443
551
|
}
|
|
444
552
|
this.agentDelegationRegistry?.revokeTask(sessionId, taskId);
|
|
445
553
|
}
|
|
554
|
+
this.closeActiveRunnerTasks(sessionId, effectiveReason, taskSpan?.causation ?? runtimeContext?.causation);
|
|
555
|
+
await this.interactionRouter?.cancelAll(sessionId, effectiveReason);
|
|
556
|
+
const channelKey = session.metadata?.channelKey || session.channel;
|
|
557
|
+
const agent = this.getAgent(channelKey, session.baseagent, session.selfAID);
|
|
558
|
+
const terminalStatus = (() => {
|
|
559
|
+
// TimeoutController owns timeout delivery (including user-facing timeout
|
|
560
|
+
// details) and already guarantees a single terminal status. Sending it
|
|
561
|
+
// here as well would race that barrier and duplicate status.timeout.
|
|
562
|
+
if (!interruptedTaskId || effectiveReason === 'timeout')
|
|
563
|
+
return Promise.resolve();
|
|
564
|
+
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
565
|
+
if (!channelInfo)
|
|
566
|
+
return Promise.resolve();
|
|
567
|
+
const envelope = buildEnvelope({
|
|
568
|
+
taskId: interruptedTaskId,
|
|
569
|
+
sessionId: session.id,
|
|
570
|
+
channel: channelInfo.adapter.channelName,
|
|
571
|
+
channelId: session.channelId,
|
|
572
|
+
agentName: agent.name,
|
|
573
|
+
chatmode: this.resolveEffectiveChatmodeForSession(session, channelKey, session.channelId),
|
|
574
|
+
replyContext: {
|
|
575
|
+
...(session.metadata?.replyContext ?? {}),
|
|
576
|
+
sessionId: session.id,
|
|
577
|
+
threadId: session.threadId || session.metadata?.replyContext?.threadId,
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
const payload = {
|
|
581
|
+
kind: 'status.interrupted',
|
|
582
|
+
metadata: { reason: effectiveReason },
|
|
583
|
+
};
|
|
584
|
+
return channelInfo.adapter.send(envelope, payload).then(() => undefined).catch(error => {
|
|
585
|
+
logger.debug(`[ResponseEngine] interrupt terminal status failure: session=${sessionId} task=${interruptedTaskId} error=${error instanceof Error ? error.message : String(error)}`);
|
|
586
|
+
});
|
|
587
|
+
})();
|
|
588
|
+
const runnerInterrupt = agent.interrupt(sessionId).catch(error => {
|
|
589
|
+
logger.debug(`[ResponseEngine] interrupt barrier runner failure: session=${sessionId} error=${error instanceof Error ? error.message : String(error)}`);
|
|
590
|
+
});
|
|
591
|
+
await Promise.all([terminalStatus, runnerInterrupt]);
|
|
592
|
+
}
|
|
593
|
+
closeActiveRunnerTasks(sessionId, reason, causation) {
|
|
594
|
+
const tasks = this.activeRunnerTasks.get(sessionId);
|
|
595
|
+
if (!tasks || tasks.size === 0)
|
|
596
|
+
return;
|
|
597
|
+
this.activeRunnerTasks.delete(sessionId);
|
|
598
|
+
for (const task of tasks.values()) {
|
|
599
|
+
const hasUsage = task.totalTokens != null || task.toolUses != null || task.durationMs != null;
|
|
600
|
+
this.eventBus.publish({
|
|
601
|
+
type: 'runner:task-notification',
|
|
602
|
+
sessionId,
|
|
603
|
+
taskId: task.taskId,
|
|
604
|
+
taskKind: task.taskKind,
|
|
605
|
+
toolUseId: task.toolUseId,
|
|
606
|
+
description: task.description,
|
|
607
|
+
subagentType: task.subagentType,
|
|
608
|
+
status: 'stopped',
|
|
609
|
+
terminalReason: reason,
|
|
610
|
+
usage: hasUsage ? {
|
|
611
|
+
totalTokens: task.totalTokens,
|
|
612
|
+
toolUses: task.toolUses,
|
|
613
|
+
durationMs: task.durationMs,
|
|
614
|
+
} : undefined,
|
|
615
|
+
skipTranscript: task.skipTranscript,
|
|
616
|
+
timestamp: Date.now(),
|
|
617
|
+
causation,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
446
620
|
}
|
|
447
621
|
setHandoffRuntime(runtime) {
|
|
448
622
|
this.handoffRuntime = runtime;
|
|
@@ -460,7 +634,7 @@ export class ResponseEngine {
|
|
|
460
634
|
if (!params.sessionId)
|
|
461
635
|
return { ok: false, code: 'HANDOFF_ID_REQUIRED', error: 'current session is required' };
|
|
462
636
|
if (this.agentDelegationRegistry) {
|
|
463
|
-
const delegation = this.agentDelegationRegistry.validate(params.delegationToken, params.sessionId);
|
|
637
|
+
const delegation = this.agentDelegationRegistry.validate(params.delegationToken, params.sessionId, params.delegationCommandHash);
|
|
464
638
|
if (!delegation.ok)
|
|
465
639
|
return { ok: false, code: delegation.code, error: delegation.reason };
|
|
466
640
|
}
|
|
@@ -620,26 +794,44 @@ export class ResponseEngine {
|
|
|
620
794
|
if (sessionId) {
|
|
621
795
|
this.eventBus.publish({ type: 'runner:compact-start', sessionId });
|
|
622
796
|
}
|
|
623
|
-
if (
|
|
624
|
-
|
|
625
|
-
|
|
797
|
+
if (!sessionId || this.announcedCompactStarts.has(sessionId))
|
|
798
|
+
return;
|
|
799
|
+
const active = this.activeRenderers.get(sessionId);
|
|
800
|
+
if (!active || active.suppressActivities)
|
|
801
|
+
return;
|
|
802
|
+
void this.emitOperationalNotice(active.renderer, '\u23f3 会话压缩中...', 'info', 'compact-start');
|
|
803
|
+
}
|
|
804
|
+
async emitOperationalNotice(renderer, text, severity, subtype) {
|
|
805
|
+
if (await renderer.sendOperationalNoticeAsText(text))
|
|
806
|
+
return;
|
|
807
|
+
renderer.addNotice(text, severity, subtype, true);
|
|
626
808
|
}
|
|
627
809
|
async retryAfterContextRecovery(prompt, opts) {
|
|
628
810
|
const { streamKey, renderer, agent, session, absoluteProjectPath, effectiveSystemPrompt, modelOverride, runtimeEnv, resetTimer, shouldSuppress, proactive, turnLease, } = opts;
|
|
629
811
|
if (!session.agentSessionId || !canCompactAgent(agent)) {
|
|
630
812
|
throw new Error('CONTEXT_COMPACT_FAILED');
|
|
631
813
|
}
|
|
632
|
-
|
|
814
|
+
// text event 在进入 renderer 前已经剔除了上下文超限错误行。保留缓冲中的
|
|
815
|
+
// 有效正文,由 sendOperationalNoticeAsText 先 flush,避免恢复过程丢失已有模型输出。
|
|
816
|
+
await this.emitOperationalNotice(renderer, '上下文过长,正在压缩会话...', 'warn', 'compact-trigger');
|
|
633
817
|
await renderer.flush();
|
|
634
|
-
|
|
818
|
+
this.announcedCompactStarts.add(session.id);
|
|
819
|
+
let compacted;
|
|
820
|
+
try {
|
|
821
|
+
compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
|
|
822
|
+
}
|
|
823
|
+
finally {
|
|
824
|
+
this.announcedCompactStarts.delete(session.id);
|
|
825
|
+
}
|
|
635
826
|
if (compacted) {
|
|
636
|
-
|
|
827
|
+
await this.emitOperationalNotice(renderer, '✅ 压缩完成,继续处理...', 'info', 'compact-retry');
|
|
828
|
+
await renderer.flush();
|
|
637
829
|
const retryStream = await agent.runQuery(session.id, prompt, absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
|
|
638
830
|
agent.registerStream(streamKey, retryStream);
|
|
639
831
|
return await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress, proactive, undefined, // 重试分支不调插件钩子
|
|
640
832
|
undefined, turnLease);
|
|
641
833
|
}
|
|
642
|
-
|
|
834
|
+
await this.emitOperationalNotice(renderer, '⚠️ 压缩失败,尝试清空会话历史后重试...', 'warn', 'compact-failed-clear');
|
|
643
835
|
await renderer.flush();
|
|
644
836
|
if (!canClearAgent(agent)) {
|
|
645
837
|
throw new Error('CONTEXT_COMPACT_FAILED');
|
|
@@ -657,7 +849,8 @@ export class ResponseEngine {
|
|
|
657
849
|
}
|
|
658
850
|
session.agentSessionId = undefined;
|
|
659
851
|
await this.sessionManager.updateSession(session.id, { agentSessionId: null });
|
|
660
|
-
|
|
852
|
+
await this.emitOperationalNotice(renderer, '✅ 会话已清空,继续处理...', 'info', 'clear-retry');
|
|
853
|
+
await renderer.flush();
|
|
661
854
|
const retryStream = await agent.runQuery(session.id, '会话历史已清空,请继续之前未完成的任务。', absoluteProjectPath, undefined, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
|
|
662
855
|
agent.registerStream(streamKey, retryStream);
|
|
663
856
|
return await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress, proactive, undefined, // 重试分支不调插件钩子
|
|
@@ -878,6 +1071,19 @@ export class ResponseEngine {
|
|
|
878
1071
|
const accessAdapter = accessChannelInfo?.adapter;
|
|
879
1072
|
const accessAdapterSelfAid = typeof accessAdapter?._selfAid === 'function' ? accessAdapter._selfAid() : undefined;
|
|
880
1073
|
const selfAidForAccess = accessAdapterSelfAid || message.selfAID || session.selfAID || undefined;
|
|
1074
|
+
if (message.handoffDelivery?.direction === 'target'
|
|
1075
|
+
&& this.handoffRuntime
|
|
1076
|
+
&& selfAidForAccess) {
|
|
1077
|
+
const handoffIds = message.handoffDelivery.handoffIds?.length
|
|
1078
|
+
? message.handoffDelivery.handoffIds
|
|
1079
|
+
: [message.handoffDelivery.handoffId];
|
|
1080
|
+
const validation = this.handoffRuntime.validateTargetSessionBinding(selfAidForAccess, handoffIds, session.id);
|
|
1081
|
+
if (!validation.ok) {
|
|
1082
|
+
logger.error(`[ResponseEngine] Refusing mismatched handoff context: executionSession=${session.id}`
|
|
1083
|
+
+ ` handoffs=${validation.invalidHandoffIds.join(',')}`);
|
|
1084
|
+
delete message.handoffDelivery;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
881
1087
|
const triggerExecutionIdentity = this.resolveTriggerExecutionIdentity(message, selfAidForAccess);
|
|
882
1088
|
const handoffExecutionIdentity = this.resolveHandoffOriginExecutionIdentity(message, selfAidForAccess);
|
|
883
1089
|
const executionIdentity = triggerExecutionIdentity ?? handoffExecutionIdentity;
|
|
@@ -979,26 +1185,40 @@ export class ResponseEngine {
|
|
|
979
1185
|
const shouldSuppress = () => {
|
|
980
1186
|
return outputState.middleOutputMode === 'none';
|
|
981
1187
|
};
|
|
982
|
-
this.shouldSuppressActivities = shouldSuppress();
|
|
983
1188
|
let monitor;
|
|
984
1189
|
let monitorInterval;
|
|
985
1190
|
let totalExecutionTimer;
|
|
986
1191
|
let rejectFn;
|
|
987
1192
|
let lastIdleSec = 0;
|
|
988
1193
|
let timeoutTriggered = false;
|
|
1194
|
+
const timeoutControl = {
|
|
1195
|
+
claimedByInternal: false,
|
|
1196
|
+
terminalDelivered: false,
|
|
1197
|
+
claim: () => {
|
|
1198
|
+
if (!timeoutControl.error || this.interruptedSessions.get(session.id) !== 'timeout')
|
|
1199
|
+
return undefined;
|
|
1200
|
+
timeoutControl.claimedByInternal = true;
|
|
1201
|
+
return timeoutControl.error;
|
|
1202
|
+
},
|
|
1203
|
+
};
|
|
989
1204
|
const rejectAfterInterruptBarrier = (error) => {
|
|
990
1205
|
if (timeoutTriggered)
|
|
991
1206
|
return;
|
|
992
1207
|
timeoutTriggered = true;
|
|
1208
|
+
timeoutControl.error = error;
|
|
993
1209
|
this.timeoutErrors.set(session.id, error);
|
|
994
1210
|
this.turnCoordinator.hydrate(session);
|
|
995
1211
|
this.turnCoordinator.invalidate(session.id, 'timeout');
|
|
996
1212
|
this.interruptedSessions.set(session.id, 'timeout');
|
|
997
|
-
|
|
1213
|
+
const barrier = this.interruptSession(session.id, 'timeout')
|
|
998
1214
|
.catch(interruptError => {
|
|
999
1215
|
logger.debug(`[ResponseEngine] Timeout interrupt barrier failed: ${interruptError instanceof Error ? interruptError.message : String(interruptError)}`);
|
|
1000
|
-
})
|
|
1001
|
-
|
|
1216
|
+
});
|
|
1217
|
+
timeoutControl.barrier = barrier;
|
|
1218
|
+
void barrier.finally(() => {
|
|
1219
|
+
if (!timeoutControl.claimedByInternal)
|
|
1220
|
+
rejectFn(error);
|
|
1221
|
+
});
|
|
1002
1222
|
};
|
|
1003
1223
|
const triggerAttemptId = message.replyContext?.metadata?.triggerAttemptId;
|
|
1004
1224
|
const resetTimer = (eventType, toolName) => {
|
|
@@ -1052,13 +1272,27 @@ export class ResponseEngine {
|
|
|
1052
1272
|
totalExecutionTimer.unref?.();
|
|
1053
1273
|
});
|
|
1054
1274
|
try {
|
|
1275
|
+
const processingPromise = this._processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, () => lastIdleSec, outputState, timeoutControl);
|
|
1276
|
+
const guardedProcessingPromise = processingPromise.then(async () => {
|
|
1277
|
+
if (!timeoutControl.error)
|
|
1278
|
+
return;
|
|
1279
|
+
await timeoutControl.barrier;
|
|
1280
|
+
throw timeoutControl.error;
|
|
1281
|
+
});
|
|
1055
1282
|
await Promise.race([
|
|
1056
|
-
|
|
1283
|
+
guardedProcessingPromise,
|
|
1057
1284
|
timeoutPromise,
|
|
1058
1285
|
totalExecutionPromise,
|
|
1059
1286
|
]);
|
|
1060
1287
|
}
|
|
1061
1288
|
catch (error) {
|
|
1289
|
+
if (error instanceof Error && (error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT')) {
|
|
1290
|
+
await timeoutControl.barrier;
|
|
1291
|
+
if (!timeoutControl.terminalDelivered) {
|
|
1292
|
+
await timeoutControl.deliverUnhandled?.(error);
|
|
1293
|
+
}
|
|
1294
|
+
this.timeoutErrors.delete(session.id);
|
|
1295
|
+
}
|
|
1062
1296
|
// 超时错误:kill 级别已发送诊断信息,无需再发
|
|
1063
1297
|
// 非超时错误走通用处理
|
|
1064
1298
|
// 记录错误到健康状态(复用已有 session)
|
|
@@ -1111,8 +1345,20 @@ export class ResponseEngine {
|
|
|
1111
1345
|
? configuredSeconds * 1000
|
|
1112
1346
|
: 60 * 60 * 1000;
|
|
1113
1347
|
}
|
|
1348
|
+
retryAttemptTimeoutMs() {
|
|
1349
|
+
if (this.globalSettings.idleMonitor?.enabled === false)
|
|
1350
|
+
return undefined;
|
|
1351
|
+
const configuredSeconds = this.globalSettings.idleMonitor?.retryAttemptTimeout
|
|
1352
|
+
?? this.globalSettings.idleMonitor?.timeout
|
|
1353
|
+
?? 120;
|
|
1354
|
+
return typeof configuredSeconds === 'number'
|
|
1355
|
+
&& Number.isFinite(configuredSeconds)
|
|
1356
|
+
&& configuredSeconds > 0
|
|
1357
|
+
? configuredSeconds * 1000
|
|
1358
|
+
: undefined;
|
|
1359
|
+
}
|
|
1114
1360
|
/** 自动安全模式已禁用:仅保留错误计数,不再自动切换状态 */
|
|
1115
|
-
async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, getLastIdleSec, outputState = { middleOutputMode: 'all' }) {
|
|
1361
|
+
async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, getLastIdleSec, outputState = { middleOutputMode: 'all' }, timeoutControl) {
|
|
1116
1362
|
const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
|
|
1117
1363
|
const channelKey = this.messageChannelKey(message, session);
|
|
1118
1364
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
@@ -1169,6 +1415,11 @@ export class ResponseEngine {
|
|
|
1169
1415
|
status: 'started',
|
|
1170
1416
|
refs: { taskId, sessionId: session.id, messageId: message.messageId },
|
|
1171
1417
|
});
|
|
1418
|
+
this.activeTaskSpans.set(session.id, {
|
|
1419
|
+
taskId,
|
|
1420
|
+
messageId: message.messageId,
|
|
1421
|
+
causation: taskCausation,
|
|
1422
|
+
});
|
|
1172
1423
|
for (const item of message.items ?? []) {
|
|
1173
1424
|
const linked = normalizeCausation(item.causation);
|
|
1174
1425
|
if (!linked || linked.spanId === inputCausation.spanId)
|
|
@@ -1193,7 +1444,9 @@ export class ResponseEngine {
|
|
|
1193
1444
|
const adapterAny = channelInfo.adapter;
|
|
1194
1445
|
const adapterSelfAid = typeof adapterAny._selfAid === 'function' ? adapterAny._selfAid() : undefined;
|
|
1195
1446
|
const selfAid = adapterSelfAid || message.selfAID || session.selfAID || undefined;
|
|
1196
|
-
const
|
|
1447
|
+
const registrySelfName = selfAid ? this.agentRegistry?.resolveDisplayName?.(selfAid) : undefined;
|
|
1448
|
+
const adapterSelfName = typeof adapterAny._selfName === 'function' ? adapterAny._selfName() : undefined;
|
|
1449
|
+
const selfName = registrySelfName ?? adapterSelfName;
|
|
1197
1450
|
const peerName = message.peerName || session.metadata?.peerName;
|
|
1198
1451
|
const peerIdRaw = message.peerId;
|
|
1199
1452
|
const triggerExecutionIdentity = this.resolveTriggerExecutionIdentity(message, selfAid);
|
|
@@ -1327,7 +1580,6 @@ export class ResponseEngine {
|
|
|
1327
1580
|
? 'none'
|
|
1328
1581
|
: configuredMiddleOutputMode;
|
|
1329
1582
|
outputState.middleOutputMode = middleOutputMode;
|
|
1330
|
-
this.shouldSuppressActivities = shouldSuppress();
|
|
1331
1583
|
// 诊断日志:记录 inbound message_id 和生成的 task_id 的对应关系
|
|
1332
1584
|
logger.info(`[ResponseEngine] Task created: inboundMsgId=${message.messageId ?? 'none'} taskId=${taskId} sessionId=${session.id} chatmode=${chatmode} mode=${resolvedMode?.mode.id ?? 'override/fallback'}`);
|
|
1333
1585
|
// 构建带 taskId/chatmode 的 ReplyContext(本次任务所有出站消息共用)
|
|
@@ -1386,6 +1638,71 @@ export class ResponseEngine {
|
|
|
1386
1638
|
replyContext: taskReplyContext(),
|
|
1387
1639
|
causation: taskCausation,
|
|
1388
1640
|
});
|
|
1641
|
+
if (timeoutControl) {
|
|
1642
|
+
timeoutControl.deliverUnhandled = async (timeoutError) => {
|
|
1643
|
+
if (timeoutControl.terminalDelivered)
|
|
1644
|
+
return;
|
|
1645
|
+
timeoutControl.terminalDelivered = true;
|
|
1646
|
+
const isTotalExecutionTimeout = timeoutError.message === 'TOTAL_EXECUTION_TIMEOUT';
|
|
1647
|
+
const totalExecutionMs = this.totalExecutionLimitMs();
|
|
1648
|
+
const statusPayload = {
|
|
1649
|
+
kind: 'status.timeout',
|
|
1650
|
+
metadata: isTotalExecutionTimeout
|
|
1651
|
+
? { totalExecutionMs }
|
|
1652
|
+
: { idleSec: getLastIdleSec?.() || undefined },
|
|
1653
|
+
};
|
|
1654
|
+
const idleSec = getLastIdleSec?.() || 0;
|
|
1655
|
+
const userMessage = isTotalExecutionTimeout
|
|
1656
|
+
? `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`
|
|
1657
|
+
: idleSec > 0
|
|
1658
|
+
? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断`
|
|
1659
|
+
: '⚠️ 任务超时,已自动中断';
|
|
1660
|
+
agent.cleanupStream(streamKey);
|
|
1661
|
+
this.sessionManager.clearProcessingIfTask(session.id, taskId);
|
|
1662
|
+
if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
|
|
1663
|
+
this.activeTaskRuntimeContexts.delete(session.id);
|
|
1664
|
+
}
|
|
1665
|
+
this.agentDelegationRegistry?.revokeTask(session.id, taskId);
|
|
1666
|
+
const daemonTrigger = this.isTrustedDaemonTrigger(message);
|
|
1667
|
+
if (!daemonTrigger) {
|
|
1668
|
+
await adapter.send(envelope, statusPayload).catch(() => { });
|
|
1669
|
+
}
|
|
1670
|
+
await adapter.send(envelope, {
|
|
1671
|
+
kind: 'result.error',
|
|
1672
|
+
text: userMessage,
|
|
1673
|
+
reason: isTotalExecutionTimeout ? 'total_execution_timeout' : 'timeout',
|
|
1674
|
+
}).catch(() => { });
|
|
1675
|
+
if (daemonTrigger) {
|
|
1676
|
+
await adapter.send(envelope, statusPayload).catch(() => { });
|
|
1677
|
+
}
|
|
1678
|
+
this.touchAgentActivity(channelKey);
|
|
1679
|
+
this.publishTriggerExecutionFailure(message, timeoutError.message, {
|
|
1680
|
+
messageId,
|
|
1681
|
+
causation: taskCausation,
|
|
1682
|
+
});
|
|
1683
|
+
const errorType = prefixErrorType(ERROR_PREFIX.INFRA, classifyError(timeoutError));
|
|
1684
|
+
this.eventBus.publish({
|
|
1685
|
+
type: 'task:error',
|
|
1686
|
+
sessionId: session.id,
|
|
1687
|
+
error: timeoutError.message,
|
|
1688
|
+
errorType,
|
|
1689
|
+
agentName: agentNameForStats,
|
|
1690
|
+
causation: taskCausation,
|
|
1691
|
+
});
|
|
1692
|
+
recordCausationSpan(taskCausation, 'task.run', {
|
|
1693
|
+
status: 'failed',
|
|
1694
|
+
refs: { taskId, sessionId: session.id, messageId },
|
|
1695
|
+
reason: errorType,
|
|
1696
|
+
});
|
|
1697
|
+
logger.message({
|
|
1698
|
+
msgId: messageId,
|
|
1699
|
+
sessionId: session.id,
|
|
1700
|
+
dir: 'inbound',
|
|
1701
|
+
status: 'failed',
|
|
1702
|
+
error: timeoutError.message,
|
|
1703
|
+
});
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1389
1706
|
try {
|
|
1390
1707
|
const isBackground = this.isBackgroundSession(session, message.channel, message.channelId);
|
|
1391
1708
|
// 记录收到消息
|
|
@@ -1474,19 +1791,32 @@ export class ResponseEngine {
|
|
|
1474
1791
|
const peerShort = peerId ? peerId.split('.')[0].split(':')[0] : '?';
|
|
1475
1792
|
const peerLabel = peerName && peerName !== peerShort ? `${peerShort}(${peerName})` : peerShort;
|
|
1476
1793
|
logger.info(`[ResponseEngine] session=${session.id} task=${taskId} peer=${peerLabel} chatType=${session.chatType} chatMode=${effectiveChatMode} baseagent=${session.baseagent} msgChatType=${message.chatType ?? 'n/a'}`);
|
|
1794
|
+
// Establish the durable turn before exposing any processing state. This
|
|
1795
|
+
// lets an immediate /stop or replacement message close the same task and
|
|
1796
|
+
// prevents a BusinessTrip reaction from becoming orphaned during setup.
|
|
1797
|
+
turnLease = await this.turnCoordinator.begin(session, taskId);
|
|
1477
1798
|
// 记录开始处理
|
|
1478
1799
|
const taskEncrypt = message.replyContext?.metadata?.encrypted != null ? !!(message.replyContext.metadata.encrypted) : undefined;
|
|
1479
1800
|
this.eventBus.publish({ type: 'task:started', sessionId: session.id, agentName: agentNameForStats, encrypt: taskEncrypt, chatmode, causation: taskCausation });
|
|
1480
1801
|
this.touchAgentActivity(channelKey);
|
|
1802
|
+
// Upgrade the channel acknowledgement at task start, before compaction and
|
|
1803
|
+
// runner setup, so processing feedback remains visible for the whole run.
|
|
1804
|
+
// Trigger messageId is an internal runId and has no channel reaction.
|
|
1805
|
+
if (message.messageId && message.source !== 'trigger') {
|
|
1806
|
+
adapter.promoteAck?.(message.messageId, {
|
|
1807
|
+
taskId,
|
|
1808
|
+
channelId: message.channelId,
|
|
1809
|
+
messageIds: message.sourceMessageIds,
|
|
1810
|
+
restartResume: message.restartResume?.submitted === true,
|
|
1811
|
+
}).catch(() => { });
|
|
1812
|
+
}
|
|
1481
1813
|
adapter.send(envelope, { kind: 'status.started' }).catch(() => { });
|
|
1482
|
-
await this.runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, adapter, envelope, false);
|
|
1483
1814
|
logger.message({
|
|
1484
1815
|
msgId: messageId,
|
|
1485
1816
|
sessionId: session.id,
|
|
1486
1817
|
dir: 'inbound',
|
|
1487
1818
|
status: 'processing'
|
|
1488
1819
|
});
|
|
1489
|
-
const startTime = Date.now();
|
|
1490
1820
|
// 创建 IMRenderer(统一 interactive/proactive 两条路径)
|
|
1491
1821
|
let firstReply = true;
|
|
1492
1822
|
const renderer = new IMRenderer({
|
|
@@ -1495,6 +1825,7 @@ export class ResponseEngine {
|
|
|
1495
1825
|
flushDelay: (options?.flushDelay ?? this.agentRegistry?.resolveByChannel(channelKey)?.config?.flush_delay ?? 3) * 1000,
|
|
1496
1826
|
suppressActivityItems: isProactive ? false : middleOutputMode !== 'all',
|
|
1497
1827
|
suppressIntermediateText: isProactive ? false : middleOutputMode === 'none',
|
|
1828
|
+
operationalNoticesAsText: !isProactive && middleOutputMode !== 'none',
|
|
1498
1829
|
fileMarkerPattern: options?.fileMarkerPattern,
|
|
1499
1830
|
diagEnabled: this.globalSettings.debug?.flusherDiag,
|
|
1500
1831
|
send: async (payload) => {
|
|
@@ -1538,8 +1869,17 @@ export class ResponseEngine {
|
|
|
1538
1869
|
await adapter.send(enrichedEnvelope, payload);
|
|
1539
1870
|
},
|
|
1540
1871
|
});
|
|
1541
|
-
this.
|
|
1872
|
+
this.activeRenderers.set(session.id, {
|
|
1873
|
+
taskId,
|
|
1874
|
+
renderer,
|
|
1875
|
+
suppressActivities: shouldSuppress(),
|
|
1876
|
+
});
|
|
1542
1877
|
renderer.addLifecycle('started');
|
|
1878
|
+
// 预压缩与任务过程中的自动压缩共用同一投影规则:interactive 的 text/all
|
|
1879
|
+
// 均以普通非最终文本显示运行提示;none 不显示。其它 activity 在 all 下仍保持结构化。
|
|
1880
|
+
// 它仍发生在真正的 prompt 执行之前。
|
|
1881
|
+
await this.runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, renderer);
|
|
1882
|
+
const startTime = Date.now();
|
|
1543
1883
|
if (isProactive) {
|
|
1544
1884
|
logger.info(`[ResponseEngine] proactive mode: outputs via thought.put task=${taskId}`);
|
|
1545
1885
|
}
|
|
@@ -1659,7 +1999,6 @@ export class ResponseEngine {
|
|
|
1659
1999
|
recordTriggerExecutionAnomaly(triggerRunId, anomaly);
|
|
1660
2000
|
}
|
|
1661
2001
|
: undefined;
|
|
1662
|
-
turnLease = await this.turnCoordinator.begin(session, taskId);
|
|
1663
2002
|
// 设置权限审批的交互上下文(支持交互卡片)
|
|
1664
2003
|
agent.setPermissionContext?.(session.id, {
|
|
1665
2004
|
sendPrompt: permissionPromptForOrigin,
|
|
@@ -1681,6 +2020,14 @@ export class ResponseEngine {
|
|
|
1681
2020
|
approvalRouting,
|
|
1682
2021
|
approvalInteractionPolicy: authorizationIdentity ? 'deny' : 'interactive',
|
|
1683
2022
|
recordExecutionAnomaly,
|
|
2023
|
+
armApprovedDelegationCommand: this.agentDelegationRegistry
|
|
2024
|
+
? (carrierToken, commandHash) => this.agentDelegationRegistry.armApprovedCommand({
|
|
2025
|
+
carrierToken,
|
|
2026
|
+
sessionId: session.id,
|
|
2027
|
+
taskId,
|
|
2028
|
+
commandHash,
|
|
2029
|
+
})
|
|
2030
|
+
: undefined,
|
|
1684
2031
|
turn: {
|
|
1685
2032
|
taskId,
|
|
1686
2033
|
turnId: turnLease.turnId,
|
|
@@ -1985,13 +2332,13 @@ export class ResponseEngine {
|
|
|
1985
2332
|
+ ` modelSource=${effectiveModelSource ?? 'runner-default'}`
|
|
1986
2333
|
+ ` effort=${effectiveEffort ?? '<runner-default>'}`
|
|
1987
2334
|
+ ` effortSource=${effectiveEffortSource ?? 'runner-default'}`);
|
|
1988
|
-
const groupRulesVars =
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2335
|
+
const groupRulesVars = await resolveGroupRulesForTurn({
|
|
2336
|
+
isAunGroup: session.chatType === 'group' && currentChannelType === 'aun',
|
|
2337
|
+
mode: effectiveAgentConfig?.groupRules?.mode ?? 'load',
|
|
2338
|
+
selfAid,
|
|
2339
|
+
groupId: session.metadata?.groupId || message.channelId,
|
|
2340
|
+
channel: currentChannelType || message.channel,
|
|
2341
|
+
});
|
|
1995
2342
|
// Kit renderer: 组装上下文
|
|
1996
2343
|
const pkgRoot = getPackageRoot();
|
|
1997
2344
|
const kitCtx = {
|
|
@@ -2057,6 +2404,7 @@ export class ResponseEngine {
|
|
|
2057
2404
|
// Stage 3: sessionKey 持久化字段
|
|
2058
2405
|
sessionKey: session.sessionKey,
|
|
2059
2406
|
chatMode: isProactive ? 'proactive' : 'interactive',
|
|
2407
|
+
isTriggerTask: causationPath.length > 0 || message.source === 'trigger',
|
|
2060
2408
|
// proactive 为 null(interactive 模式,这些开关不适用)时如实为 false——
|
|
2061
2409
|
// 默认值不在此硬编码,出厂默认由模式 schema 声明、经 buildState 落入 proactive.*。
|
|
2062
2410
|
proactivePreTool1stMsgChk: proactive?.preTool1stMsgChk ?? false,
|
|
@@ -2136,9 +2484,9 @@ export class ResponseEngine {
|
|
|
2136
2484
|
if (message.handoffDelivery && this.handoffRuntime) {
|
|
2137
2485
|
const items = this.handoffRuntime.buildPromptItems(message);
|
|
2138
2486
|
if (items.length > 0) {
|
|
2139
|
-
v2HandoffIds = items
|
|
2140
|
-
|
|
2141
|
-
.
|
|
2487
|
+
v2HandoffIds = Array.from(new Set(items.flatMap(item => (item.handoff?.handoffIds?.length
|
|
2488
|
+
? item.handoff.handoffIds
|
|
2489
|
+
: (item.handoff?.handoffId ? [item.handoff.handoffId] : [])))));
|
|
2142
2490
|
v2HandoffDirection = message.handoffDelivery.direction;
|
|
2143
2491
|
return items;
|
|
2144
2492
|
}
|
|
@@ -2205,9 +2553,12 @@ export class ResponseEngine {
|
|
|
2205
2553
|
this.publishTriggerExecutionSkipped(message, 'empty_prompt', taskCausation);
|
|
2206
2554
|
return;
|
|
2207
2555
|
}
|
|
2208
|
-
if (v2HandoffIds.length > 0 &&
|
|
2556
|
+
if (v2HandoffIds.length > 0 && handoffPromptRendered && this.handoffRuntime && selfAid) {
|
|
2209
2557
|
for (const handoffId of v2HandoffIds) {
|
|
2210
|
-
|
|
2558
|
+
if (v2HandoffDirection === 'origin')
|
|
2559
|
+
this.handoffRuntime.completeOriginContext(selfAid, handoffId);
|
|
2560
|
+
else if (v2HandoffDirection === 'target')
|
|
2561
|
+
this.handoffRuntime.completeTargetContext(selfAid, handoffId);
|
|
2211
2562
|
}
|
|
2212
2563
|
}
|
|
2213
2564
|
const taskRuntimeContext = {
|
|
@@ -2255,6 +2606,7 @@ export class ResponseEngine {
|
|
|
2255
2606
|
// 可重试错误(403/429/5xx/模型繁忙)按明确退避序列重试。
|
|
2256
2607
|
const RETRY_DELAYS_MS = [5_000, 10_000, 30_000];
|
|
2257
2608
|
const MAX_RETRIES = RETRY_DELAYS_MS.length;
|
|
2609
|
+
const retryAttemptTimeoutMs = this.retryAttemptTimeoutMs();
|
|
2258
2610
|
let runAttempt = 1;
|
|
2259
2611
|
let consecutiveRetryFailures = 0;
|
|
2260
2612
|
const recordRetryHealthError = async (retryError) => {
|
|
@@ -2270,19 +2622,26 @@ export class ResponseEngine {
|
|
|
2270
2622
|
logger.error('[ResponseEngine] Failed to record retry health status:', statusError);
|
|
2271
2623
|
}
|
|
2272
2624
|
};
|
|
2273
|
-
// Runner 开始执行前:将 Pin 升级为 CheckMark(表示"正在处理")。
|
|
2274
|
-
// Trigger messageId is an internal runId, so there is no channel ack to promote.
|
|
2275
|
-
if (message.messageId && message.source !== 'trigger') {
|
|
2276
|
-
adapter.promoteAck?.(message.messageId).catch(() => { });
|
|
2277
|
-
}
|
|
2278
2625
|
while (true) {
|
|
2279
2626
|
let streamRegistered = false;
|
|
2627
|
+
const shouldTimeoutRetryAttempt = consecutiveRetryFailures > 0 && retryAttemptTimeoutMs !== undefined;
|
|
2628
|
+
let attemptTimeout;
|
|
2280
2629
|
try {
|
|
2281
2630
|
logger.info(`[ResponseEngine] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${runAttempt} consecutiveFailures=${consecutiveRetryFailures} agentSessionId=${session.agentSessionId ?? 'none'}`);
|
|
2282
2631
|
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, renderResult?.images.length ? renderResult.images : message.images, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
|
|
2283
2632
|
agent.registerStream(streamKey, stream);
|
|
2284
2633
|
streamRegistered = true;
|
|
2285
|
-
|
|
2634
|
+
if (shouldTimeoutRetryAttempt) {
|
|
2635
|
+
attemptTimeout = createRetryAttemptTimeout(retryAttemptTimeoutMs);
|
|
2636
|
+
resetTimer('retry_attempt');
|
|
2637
|
+
}
|
|
2638
|
+
const processAttempt = this.processEventStream(stream, session, agent, renderer, (eventType, toolName) => {
|
|
2639
|
+
resetTimer(eventType, toolName);
|
|
2640
|
+
attemptTimeout?.reset();
|
|
2641
|
+
}, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease);
|
|
2642
|
+
streamResult = attemptTimeout
|
|
2643
|
+
? await Promise.race([processAttempt, attemptTimeout.promise])
|
|
2644
|
+
: await processAttempt;
|
|
2286
2645
|
if (streamResult.isError && !streamHitContextLimit(streamResult)) {
|
|
2287
2646
|
const streamErrorText = getStreamErrorText(streamResult);
|
|
2288
2647
|
if (streamErrorText && isRetryableError(new Error(streamErrorText))) {
|
|
@@ -2298,6 +2657,14 @@ export class ResponseEngine {
|
|
|
2298
2657
|
break; // 成功,跳出重试循环
|
|
2299
2658
|
}
|
|
2300
2659
|
catch (retryError) {
|
|
2660
|
+
const retryAttemptTimedOut = retryError instanceof RetryAttemptTimeoutError;
|
|
2661
|
+
if (retryAttemptTimedOut) {
|
|
2662
|
+
logger.warn(`[ResponseEngine] Retry attempt ${runAttempt} timed out after ${retryError.timeoutMs}ms without agent events; interrupting current stream`);
|
|
2663
|
+
renderer.discardPending();
|
|
2664
|
+
await agent.interrupt(session.id).catch(interruptError => {
|
|
2665
|
+
logger.warn(`[ResponseEngine] Failed to interrupt timed-out retry attempt: ${interruptError instanceof Error ? interruptError.message : String(interruptError)}`);
|
|
2666
|
+
});
|
|
2667
|
+
}
|
|
2301
2668
|
if (streamRegistered) {
|
|
2302
2669
|
agent.cleanupStream(streamKey);
|
|
2303
2670
|
}
|
|
@@ -2328,7 +2695,7 @@ export class ResponseEngine {
|
|
|
2328
2695
|
runAttempt++;
|
|
2329
2696
|
continue;
|
|
2330
2697
|
}
|
|
2331
|
-
if (isRetryableError(retryError)) {
|
|
2698
|
+
if (retryAttemptTimedOut || isRetryableError(retryError)) {
|
|
2332
2699
|
if (didRetryMakeProgress(retryError)) {
|
|
2333
2700
|
consecutiveRetryFailures = 0;
|
|
2334
2701
|
}
|
|
@@ -2344,7 +2711,7 @@ export class ResponseEngine {
|
|
|
2344
2711
|
const nextRetryNumber = consecutiveRetryFailures + 1;
|
|
2345
2712
|
consecutiveRetryFailures++;
|
|
2346
2713
|
logger.warn(`[ResponseEngine] Retryable error (attempt ${runAttempt}, consecutive ${consecutiveRetryFailures}/${MAX_RETRIES}), retrying in ${delay}ms:`, retryError);
|
|
2347
|
-
|
|
2714
|
+
await this.emitOperationalNotice(renderer, `API 不可用,${delay / 1000}秒后重试 ${nextRetryNumber}/${MAX_RETRIES}`, 'warn', 'retry');
|
|
2348
2715
|
await renderer.flush();
|
|
2349
2716
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
2350
2717
|
// 延迟后再次检查中断状态(延迟期间可能收到中断信号)
|
|
@@ -2360,6 +2727,9 @@ export class ResponseEngine {
|
|
|
2360
2727
|
}
|
|
2361
2728
|
throw retryError; // 不可重试或已耗尽重试次数
|
|
2362
2729
|
}
|
|
2730
|
+
finally {
|
|
2731
|
+
attemptTimeout?.clear();
|
|
2732
|
+
}
|
|
2363
2733
|
}
|
|
2364
2734
|
}
|
|
2365
2735
|
catch (error) {
|
|
@@ -2383,6 +2753,10 @@ export class ResponseEngine {
|
|
|
2383
2753
|
throw error;
|
|
2384
2754
|
}
|
|
2385
2755
|
}
|
|
2756
|
+
const pendingTimeoutError = timeoutControl?.claim();
|
|
2757
|
+
if (pendingTimeoutError) {
|
|
2758
|
+
throw pendingTimeoutError;
|
|
2759
|
+
}
|
|
2386
2760
|
if (permissionTerminalFailure) {
|
|
2387
2761
|
renderer.discardPending();
|
|
2388
2762
|
streamResult = {
|
|
@@ -2418,12 +2792,12 @@ export class ResponseEngine {
|
|
|
2418
2792
|
// 重试后仍然 prompt_too_long:显示友好提示
|
|
2419
2793
|
const retryStillTooLong = streamResult.isError && streamHitContextLimit(streamResult);
|
|
2420
2794
|
if (retryStillTooLong) {
|
|
2421
|
-
|
|
2795
|
+
await this.emitOperationalNotice(renderer, getContextTooLongHint(agent), 'warn', 'context-too-long');
|
|
2422
2796
|
}
|
|
2423
2797
|
}
|
|
2424
2798
|
else if (streamResult.isError && streamHitContextLimit(streamResult)) {
|
|
2425
2799
|
// 上下文过长但无法 auto-compact(无 session ID 或 agent 不支持),显示友好提示
|
|
2426
|
-
|
|
2800
|
+
await this.emitOperationalNotice(renderer, getContextTooLongHint(agent), 'warn', 'context-too-long');
|
|
2427
2801
|
}
|
|
2428
2802
|
if (!streamResult.isError) {
|
|
2429
2803
|
const commitEvidence = this.buildTurnCommitEvidence(session, streamResult);
|
|
@@ -2495,7 +2869,7 @@ export class ResponseEngine {
|
|
|
2495
2869
|
if (!this.turnCoordinator.isCurrent(turnLease))
|
|
2496
2870
|
return 0;
|
|
2497
2871
|
// 仅支持公开文件标记语法:[SEND_FILE:path] 或 [SEND_FILE:channel:path]。
|
|
2498
|
-
const SEND_FILE_RE = options?.fileMarkerPattern ??
|
|
2872
|
+
const SEND_FILE_RE = options?.fileMarkerPattern ?? createSendFileMarkerPattern();
|
|
2499
2873
|
const sendFileMatches = [...scanText.matchAll(SEND_FILE_RE)];
|
|
2500
2874
|
if (sendFileMatches.length === 0)
|
|
2501
2875
|
return 0;
|
|
@@ -2591,6 +2965,7 @@ export class ResponseEngine {
|
|
|
2591
2965
|
}
|
|
2592
2966
|
// 最终回复文本:suppressed 模式或无 text 事件时需要兜底添加
|
|
2593
2967
|
const finalReplyText = streamResult.lastReplyText || streamResult.fullText;
|
|
2968
|
+
const visibleFinalReplyText = stripContextLimitErrorLines(finalReplyText);
|
|
2594
2969
|
if (!this.turnCoordinator.isCurrent(turnLease)) {
|
|
2595
2970
|
agent.cleanupStream(streamKey);
|
|
2596
2971
|
this.sessionManager.clearProcessingIfTask(session.id, taskId);
|
|
@@ -2602,9 +2977,11 @@ export class ResponseEngine {
|
|
|
2602
2977
|
await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_turn', taskCausation);
|
|
2603
2978
|
return;
|
|
2604
2979
|
}
|
|
2605
|
-
if (
|
|
2606
|
-
|
|
2607
|
-
|
|
2980
|
+
if (visibleFinalReplyText) {
|
|
2981
|
+
// `hasReceivedText` 是任务级计数。工具调用前的文字已经显示,并不表示最后一轮
|
|
2982
|
+
// (通常来自 complete.result)的文字也已显示;用本轮投影标记避免终态丢失。
|
|
2983
|
+
if (shouldSuppress() || !streamResult.hasProjectedCurrentReplyText) {
|
|
2984
|
+
renderer.addText(visibleFinalReplyText);
|
|
2608
2985
|
}
|
|
2609
2986
|
}
|
|
2610
2987
|
// 先清理流和处理中状态(保证即使 flush 卡住,session 也不会永久处于"处理中")
|
|
@@ -2632,7 +3009,7 @@ export class ResponseEngine {
|
|
|
2632
3009
|
}
|
|
2633
3010
|
// 被用户中断(新消息打断)时跳过 flush — 新 task 已接管渠道,旧 task 的 flush 无意义且可能卡住
|
|
2634
3011
|
const preFlushInterrupt = this.interruptedSessions.get(session.id);
|
|
2635
|
-
if (preFlushInterrupt
|
|
3012
|
+
if (isNonErrorInterruptReason(preFlushInterrupt)) {
|
|
2636
3013
|
logger.info(`[ResponseEngine] Skipping flush for interrupted task=${taskId} reason=${preFlushInterrupt}`);
|
|
2637
3014
|
}
|
|
2638
3015
|
else {
|
|
@@ -2673,13 +3050,13 @@ export class ResponseEngine {
|
|
|
2673
3050
|
// 用户主动打断(新消息//stop/撤回)会让 SDK 流在工具调用中途被掐断,
|
|
2674
3051
|
// 末尾 result message 形状异常并被标记为 error(含 SDK 内部 ede_diagnostic 串)。
|
|
2675
3052
|
// 这不是真正的失败,不应把诊断串暴露给用户,也不计入错误统计。
|
|
2676
|
-
const
|
|
2677
|
-
if (!
|
|
3053
|
+
const isExpectedInterrupt = isNonErrorInterruptReason(interruptReason);
|
|
3054
|
+
if (!isExpectedInterrupt && !permissionTerminalFailure) {
|
|
2678
3055
|
await adapter.send(envelope, { kind: 'result.error', text: userErrorSummary, reason: rawSubtype }).catch(() => { });
|
|
2679
3056
|
await adapter.send(envelope, { kind: 'status.error', metadata: { errorType: rawSubtype } }).catch(() => { });
|
|
2680
3057
|
this.touchAgentActivity(channelKey);
|
|
2681
3058
|
}
|
|
2682
|
-
if (
|
|
3059
|
+
if (isExpectedInterrupt) {
|
|
2683
3060
|
// 用户打断:打断本身已由 message-queue 发过 task:interrupted 事件,
|
|
2684
3061
|
// 这里不再补发 task:error(否则同一次打断被记两遍且错误归类为 error)。
|
|
2685
3062
|
// 仅记 info 日志收尾。注意:task:interrupted 已填充 interruptedSessions,
|
|
@@ -2696,6 +3073,8 @@ export class ResponseEngine {
|
|
|
2696
3073
|
await this.publishTriggerExecutionInterrupted(message, adapter, envelope, interruptReason ?? 'interrupted', taskCausation);
|
|
2697
3074
|
}
|
|
2698
3075
|
else {
|
|
3076
|
+
logger.error(`[ResponseEngine] Task failed: session=${session.id} task=${taskId} ` +
|
|
3077
|
+
`type=${errorType} terminalReason=${streamResult.terminalReason ?? 'none'} error=${errorSummary}`);
|
|
2699
3078
|
this.publishTriggerExecutionFailure(message, errorSummary, { messageId, causation: taskCausation });
|
|
2700
3079
|
this.eventBus.publish({
|
|
2701
3080
|
type: 'task:error',
|
|
@@ -2734,6 +3113,12 @@ export class ResponseEngine {
|
|
|
2734
3113
|
return;
|
|
2735
3114
|
}
|
|
2736
3115
|
const durationMs = Date.now() - startTime;
|
|
3116
|
+
const statsModel = streamResult.lastModelCall?.model
|
|
3117
|
+
|| streamResult.model
|
|
3118
|
+
|| streamResult.contextUsage?.model
|
|
3119
|
+
|| modelOverride?.model
|
|
3120
|
+
|| agentModel
|
|
3121
|
+
|| 'unknown';
|
|
2737
3122
|
// ── Stats: 写入 usage_events(在 status.completed 之前,以便带上 cost) ──
|
|
2738
3123
|
// cost 含原价(official)与网关实际价(gateway)两套,由 insertUsageEvent 写库时一并算出。
|
|
2739
3124
|
let turnCost = { official: null, gateway: null };
|
|
@@ -2745,7 +3130,6 @@ export class ResponseEngine {
|
|
|
2745
3130
|
|| (configActorId
|
|
2746
3131
|
? formatPeerKey(configChannelType, configActorId)
|
|
2747
3132
|
: formatPeerKey(configChannelType, message.channelId));
|
|
2748
|
-
const statsModel = streamResult.contextUsage?.model || 'unknown';
|
|
2749
3133
|
const ctxPct = streamResult.contextUsage?.percentage;
|
|
2750
3134
|
const event = normalizeUsage(streamResult.tokenUsage, {
|
|
2751
3135
|
ts: Date.now(),
|
|
@@ -2803,7 +3187,6 @@ export class ResponseEngine {
|
|
|
2803
3187
|
try {
|
|
2804
3188
|
const { resolveModelSpec } = await import('../../stats/billing.js');
|
|
2805
3189
|
const { querySessionSummary } = await import('../../stats/query.js');
|
|
2806
|
-
const statsModel = streamResult.contextUsage?.model || 'unknown';
|
|
2807
3190
|
modelSpec = resolveModelSpec(resolveRoot(), statsModel);
|
|
2808
3191
|
const sum = querySessionSummary(resolveRoot(), session.id);
|
|
2809
3192
|
if (sum.calls > 0) {
|
|
@@ -2848,7 +3231,7 @@ export class ResponseEngine {
|
|
|
2848
3231
|
let lastModelCall = streamResult.lastModelCall;
|
|
2849
3232
|
if (lastModelCall?.tokenUsage) {
|
|
2850
3233
|
try {
|
|
2851
|
-
const lastModel = lastModelCall.model ||
|
|
3234
|
+
const lastModel = lastModelCall.model || statsModel;
|
|
2852
3235
|
const lastEvent = normalizeUsage(lastModelCall.tokenUsage, {
|
|
2853
3236
|
ts: Date.now(), agent_aid: '', peer_key: '', session_id: session.id,
|
|
2854
3237
|
model: lastModel, turns: 1,
|
|
@@ -2950,7 +3333,14 @@ export class ResponseEngine {
|
|
|
2950
3333
|
});
|
|
2951
3334
|
}
|
|
2952
3335
|
catch (error) {
|
|
2953
|
-
|
|
3336
|
+
const authoritativeTimeoutError = timeoutControl?.claim();
|
|
3337
|
+
if (authoritativeTimeoutError) {
|
|
3338
|
+
error = authoritativeTimeoutError;
|
|
3339
|
+
await timeoutControl?.barrier;
|
|
3340
|
+
}
|
|
3341
|
+
const handlingTimeout = error instanceof Error
|
|
3342
|
+
&& (error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT');
|
|
3343
|
+
if (handlingTimeout) {
|
|
2954
3344
|
this.timeoutErrors.delete(session.id);
|
|
2955
3345
|
}
|
|
2956
3346
|
const leaseWasCurrent = !!turnLease && this.turnCoordinator.isCurrent(turnLease);
|
|
@@ -2978,10 +3368,7 @@ export class ResponseEngine {
|
|
|
2978
3368
|
&& interruptReason !== 'timeout'
|
|
2979
3369
|
&& !isTimeout
|
|
2980
3370
|
&& !isTotalExecutionTimeout;
|
|
2981
|
-
const isUserInterrupt = interruptReason
|
|
2982
|
-
|| interruptReason === 'stop'
|
|
2983
|
-
|| interruptReason === 'recalled'
|
|
2984
|
-
|| staleSupersededTurn;
|
|
3371
|
+
const isUserInterrupt = isNonErrorInterruptReason(interruptReason) || staleSupersededTurn;
|
|
2985
3372
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2986
3373
|
if (isUserInterrupt) {
|
|
2987
3374
|
await this.publishTriggerExecutionInterrupted(message, adapter, envelope, interruptReason ?? 'stale_turn', taskCausation);
|
|
@@ -3055,7 +3442,7 @@ export class ResponseEngine {
|
|
|
3055
3442
|
if (isUserInterrupt) {
|
|
3056
3443
|
logger.info(`[ResponseEngine] User interrupt by new_message, skip sending error message`);
|
|
3057
3444
|
}
|
|
3058
|
-
else if (error?._errorAlreadySent && !retryExhaustedCount) {
|
|
3445
|
+
else if (error?._errorAlreadySent && !retryExhaustedCount && !isTimeout && !isTotalExecutionTimeout) {
|
|
3059
3446
|
logger.info(`[ResponseEngine] Error already sent via renderer, skip sending duplicate message`);
|
|
3060
3447
|
}
|
|
3061
3448
|
else {
|
|
@@ -3103,6 +3490,15 @@ export class ResponseEngine {
|
|
|
3103
3490
|
}).catch(() => { });
|
|
3104
3491
|
}
|
|
3105
3492
|
}
|
|
3493
|
+
if (handlingTimeout && timeoutControl) {
|
|
3494
|
+
timeoutControl.terminalDelivered = true;
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
finally {
|
|
3498
|
+
// 同一 session 的新任务可能已替换登记;旧任务结束时不得删掉新任务的 renderer。
|
|
3499
|
+
if (this.activeRenderers.get(session.id)?.taskId === taskId) {
|
|
3500
|
+
this.activeRenderers.delete(session.id);
|
|
3501
|
+
}
|
|
3106
3502
|
}
|
|
3107
3503
|
// [迁移探针] 任务收尾:记录工具提醒最终状态并落盘(防线 1)
|
|
3108
3504
|
if (snapshot.isEnabled()) {
|
|
@@ -3112,7 +3508,7 @@ export class ResponseEngine {
|
|
|
3112
3508
|
snapshot.end(session.id, taskId);
|
|
3113
3509
|
}
|
|
3114
3510
|
}
|
|
3115
|
-
async runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath,
|
|
3511
|
+
async runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, renderer) {
|
|
3116
3512
|
if (!session.agentSessionId || !canCompactAgent(agent)) {
|
|
3117
3513
|
logger.debug(`[ResponseEngine] Auto compact skipped: session=${session.id} agentSessionId=${session.agentSessionId || 'none'} canCompact=${canCompactAgent(agent)} agent=${agent.name}`);
|
|
3118
3514
|
return;
|
|
@@ -3123,15 +3519,14 @@ export class ResponseEngine {
|
|
|
3123
3519
|
return;
|
|
3124
3520
|
}
|
|
3125
3521
|
logger.info(`[ResponseEngine] Auto compact at task.start: session=${session.id} totalTokens=${ctx.totalTokens} autoCompactTokens=${ctx.autoCompactTokens}`);
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3522
|
+
await this.emitOperationalNotice(renderer, '上下文接近上限,正在压缩会话...', 'info', 'auto-compact-start');
|
|
3523
|
+
await renderer.flush();
|
|
3524
|
+
this.announcedCompactStarts.add(session.id);
|
|
3129
3525
|
try {
|
|
3130
3526
|
const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
|
|
3131
3527
|
if (compacted) {
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
}
|
|
3528
|
+
await this.emitOperationalNotice(renderer, '✅ 上下文压缩完成,继续处理...', 'info', 'auto-compact-complete');
|
|
3529
|
+
await renderer.flush();
|
|
3135
3530
|
}
|
|
3136
3531
|
else {
|
|
3137
3532
|
logger.warn(`[ResponseEngine] Auto compact at task.start returned false (session=${session.id})`);
|
|
@@ -3140,6 +3535,9 @@ export class ResponseEngine {
|
|
|
3140
3535
|
catch (err) {
|
|
3141
3536
|
logger.warn(`[ResponseEngine] Auto compact at task.start failed (non-fatal):`, err);
|
|
3142
3537
|
}
|
|
3538
|
+
finally {
|
|
3539
|
+
this.announcedCompactStarts.delete(session.id);
|
|
3540
|
+
}
|
|
3143
3541
|
}
|
|
3144
3542
|
async readLastModelCallContextUsage(sessionId, agentSessionId) {
|
|
3145
3543
|
try {
|
|
@@ -3206,17 +3604,28 @@ export class ResponseEngine {
|
|
|
3206
3604
|
const owningAgent = this.agentRegistry?.resolveByChannel(message.channel);
|
|
3207
3605
|
const projectPath = owningAgent?.projectPath || process.cwd();
|
|
3208
3606
|
const resolvedBaseagent = message.baseagent || owningAgent?.baseagent || (!this.agentRegistry ? this.inferPrimaryBaseagent() : undefined);
|
|
3209
|
-
|
|
3607
|
+
// 优先复用 MessageBridge 入队前已解析的身份,避免二次独立解析产生角色漂移
|
|
3608
|
+
// (曾观测到瞬时竞态下把 owner 误判为 none,导致合法用户被静默拒绝)。
|
|
3609
|
+
// 仅当消息不经 MessageBridge.register()(如 trigger 直接投递)时才回退重新解析。
|
|
3610
|
+
const resolvedIdentity = message.resolvedIdentity ?? this.inferDirectSessionIdentity(message, owningAgent);
|
|
3210
3611
|
if (!resolvedBaseagent) {
|
|
3211
3612
|
throw new Error(`[ResponseEngine] resolveSession: baseagent could not be determined (message.baseagent=${message.baseagent}, owningAgent=${owningAgent?.name || 'none'})`);
|
|
3212
3613
|
}
|
|
3213
3614
|
// 话题创建权限守卫已统一移至 MessageBridge.canCreateThreadSession(enqueue 前拦截),
|
|
3214
3615
|
// 此处不再重复检查——bridge 层拒绝后消息根本不会到达 processMessage。
|
|
3215
|
-
//
|
|
3216
|
-
|
|
3217
|
-
|
|
3616
|
+
// Resume the exact session selected before enqueue. Handoff binding and model
|
|
3617
|
+
// execution must never independently re-resolve to different sessions.
|
|
3618
|
+
const boundSessionId = message.boundSessionId ?? message.triggerMeta?.boundSessionId;
|
|
3619
|
+
if (boundSessionId) {
|
|
3620
|
+
const bound = await this.sessionManager.getSessionById(boundSessionId);
|
|
3218
3621
|
if (bound) {
|
|
3219
3622
|
this.ensureSessionBaseagent(bound, resolvedBaseagent);
|
|
3623
|
+
// Session.identity is deliberately runtime-only and is not persisted in
|
|
3624
|
+
// active.json/session JSONL. Re-attaching a bridge-bound session without
|
|
3625
|
+
// restoring the freshly resolved identity would therefore downgrade a
|
|
3626
|
+
// valid mapped owner/admin to role=none at the execution boundary.
|
|
3627
|
+
if (resolvedIdentity)
|
|
3628
|
+
bound.identity = resolvedIdentity;
|
|
3220
3629
|
if (bound.threadId) {
|
|
3221
3630
|
const absoluteProjectPath = path.isAbsolute(bound.projectPath)
|
|
3222
3631
|
? bound.projectPath : path.resolve(process.cwd(), bound.projectPath);
|
|
@@ -3225,14 +3634,22 @@ export class ResponseEngine {
|
|
|
3225
3634
|
const switched = await this.sessionManager.switchToSession(bound.channel, bound.channelId, bound.id);
|
|
3226
3635
|
if (switched) {
|
|
3227
3636
|
this.ensureSessionBaseagent(switched, resolvedBaseagent);
|
|
3637
|
+
if (resolvedIdentity)
|
|
3638
|
+
switched.identity = resolvedIdentity;
|
|
3228
3639
|
const absoluteProjectPath = path.isAbsolute(switched.projectPath)
|
|
3229
3640
|
? switched.projectPath : path.resolve(process.cwd(), switched.projectPath);
|
|
3230
3641
|
return { session: switched, absoluteProjectPath };
|
|
3231
3642
|
}
|
|
3643
|
+
if (message.boundSessionId) {
|
|
3644
|
+
throw new Error(`[ResponseEngine] Bound session ${bound.id} could not be activated`);
|
|
3645
|
+
}
|
|
3232
3646
|
logger.warn(`[ResponseEngine] switchToSession failed for bound session ${bound.id}, falling back to latest`);
|
|
3233
3647
|
}
|
|
3234
3648
|
else {
|
|
3235
|
-
|
|
3649
|
+
if (message.boundSessionId) {
|
|
3650
|
+
throw new Error(`[ResponseEngine] Bound session ${boundSessionId} not found`);
|
|
3651
|
+
}
|
|
3652
|
+
logger.warn(`[ResponseEngine] Bound session ${boundSessionId} not found, falling back to latest`);
|
|
3236
3653
|
}
|
|
3237
3654
|
}
|
|
3238
3655
|
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, message.topicName, message.peerId, message.chatType, resolvedBaseagent, message.selfAID, message.channelType, message.peerType, resolvedIdentity);
|
|
@@ -3291,6 +3708,7 @@ export class ResponseEngine {
|
|
|
3291
3708
|
const statsChannelKey = session.channel === 'daemon' ? session.channel : (session.metadata?.channelKey || session.channel);
|
|
3292
3709
|
const agentNameForStats = this.agentRegistry?.resolveByChannel(statsChannelKey)?.name ?? '<unknown>';
|
|
3293
3710
|
let hasReceivedText = false;
|
|
3711
|
+
let hasProjectedCurrentReplyText = false;
|
|
3294
3712
|
let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
|
|
3295
3713
|
let madeProgress = false;
|
|
3296
3714
|
let completeResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
|
|
@@ -3304,6 +3722,7 @@ export class ResponseEngine {
|
|
|
3304
3722
|
const finalizeResult = () => ({
|
|
3305
3723
|
...completeResult,
|
|
3306
3724
|
hasReceivedText,
|
|
3725
|
+
hasProjectedCurrentReplyText,
|
|
3307
3726
|
inputAccepted,
|
|
3308
3727
|
assistantUuid,
|
|
3309
3728
|
protocolIncompleteReason,
|
|
@@ -3374,7 +3793,7 @@ export class ResponseEngine {
|
|
|
3374
3793
|
assistantUuid = event.assistantUuid ?? assistantUuid;
|
|
3375
3794
|
protocolIncompleteReason = event.protocolIncompleteReason ?? protocolIncompleteReason;
|
|
3376
3795
|
}
|
|
3377
|
-
//
|
|
3796
|
+
// 记录事件类型:高价值事件(含子任务生命周期)INFO,
|
|
3378
3797
|
// 框架事件(session_id/state_changed/status)DEBUG
|
|
3379
3798
|
let eventDetail = '';
|
|
3380
3799
|
if (event.type === 'text' && event.text) {
|
|
@@ -3429,18 +3848,144 @@ export class ResponseEngine {
|
|
|
3429
3848
|
});
|
|
3430
3849
|
continue;
|
|
3431
3850
|
}
|
|
3851
|
+
if (event.type === 'task_started') {
|
|
3852
|
+
let tasks = this.activeRunnerTasks.get(session.id);
|
|
3853
|
+
if (!tasks) {
|
|
3854
|
+
tasks = new Map();
|
|
3855
|
+
this.activeRunnerTasks.set(session.id, tasks);
|
|
3856
|
+
}
|
|
3857
|
+
tasks.set(event.taskId, {
|
|
3858
|
+
taskId: event.taskId,
|
|
3859
|
+
taskKind: event.taskKind,
|
|
3860
|
+
toolUseId: event.toolUseId,
|
|
3861
|
+
description: event.description,
|
|
3862
|
+
subagentType: event.subagentType,
|
|
3863
|
+
skipTranscript: event.skipTranscript,
|
|
3864
|
+
});
|
|
3865
|
+
this.eventBus.publish({
|
|
3866
|
+
type: 'runner:task-started',
|
|
3867
|
+
sessionId: session.id,
|
|
3868
|
+
taskId: event.taskId,
|
|
3869
|
+
taskKind: event.taskKind,
|
|
3870
|
+
toolUseId: event.toolUseId,
|
|
3871
|
+
description: event.description,
|
|
3872
|
+
subagentType: event.subagentType,
|
|
3873
|
+
taskType: event.taskType,
|
|
3874
|
+
workflowName: event.workflowName,
|
|
3875
|
+
prompt: event.prompt,
|
|
3876
|
+
skipTranscript: event.skipTranscript,
|
|
3877
|
+
timestamp: Date.now(),
|
|
3878
|
+
causation,
|
|
3879
|
+
});
|
|
3880
|
+
if (!event.skipTranscript) {
|
|
3881
|
+
renderer.addLifecycle('started', {
|
|
3882
|
+
scope: 'runner_task',
|
|
3883
|
+
taskId: event.taskId,
|
|
3884
|
+
taskKind: event.taskKind,
|
|
3885
|
+
toolUseId: event.toolUseId,
|
|
3886
|
+
description: event.description,
|
|
3887
|
+
subagentType: event.subagentType,
|
|
3888
|
+
taskType: event.taskType,
|
|
3889
|
+
}, `${event.taskKind === 'agent' ? 'Agent' : 'Background'} task started: ${event.description}`);
|
|
3890
|
+
}
|
|
3891
|
+
continue;
|
|
3892
|
+
}
|
|
3893
|
+
if (event.type === 'background_tasks_changed') {
|
|
3894
|
+
this.eventBus.publish({
|
|
3895
|
+
type: 'runner:background-tasks-changed',
|
|
3896
|
+
sessionId: session.id,
|
|
3897
|
+
tasks: event.tasks,
|
|
3898
|
+
timestamp: Date.now(),
|
|
3899
|
+
causation,
|
|
3900
|
+
});
|
|
3901
|
+
continue;
|
|
3902
|
+
}
|
|
3903
|
+
if (event.type === 'task_notification') {
|
|
3904
|
+
const tasks = this.activeRunnerTasks.get(session.id);
|
|
3905
|
+
tasks?.delete(event.taskId);
|
|
3906
|
+
if (tasks?.size === 0)
|
|
3907
|
+
this.activeRunnerTasks.delete(session.id);
|
|
3908
|
+
this.eventBus.publish({
|
|
3909
|
+
type: 'runner:task-notification',
|
|
3910
|
+
sessionId: session.id,
|
|
3911
|
+
taskId: event.taskId,
|
|
3912
|
+
taskKind: event.taskKind,
|
|
3913
|
+
toolUseId: event.toolUseId,
|
|
3914
|
+
description: event.description,
|
|
3915
|
+
subagentType: event.subagentType,
|
|
3916
|
+
status: event.status,
|
|
3917
|
+
outputFile: event.outputFile,
|
|
3918
|
+
summary: event.summary,
|
|
3919
|
+
usage: event.usage,
|
|
3920
|
+
skipTranscript: event.skipTranscript,
|
|
3921
|
+
timestamp: Date.now(),
|
|
3922
|
+
causation,
|
|
3923
|
+
});
|
|
3924
|
+
if (!event.skipTranscript) {
|
|
3925
|
+
const label = event.description || event.taskId;
|
|
3926
|
+
renderer.addLifecycle('completed', {
|
|
3927
|
+
scope: 'runner_task',
|
|
3928
|
+
taskId: event.taskId,
|
|
3929
|
+
taskKind: event.taskKind,
|
|
3930
|
+
toolUseId: event.toolUseId,
|
|
3931
|
+
description: event.description,
|
|
3932
|
+
subagentType: event.subagentType,
|
|
3933
|
+
status: event.status,
|
|
3934
|
+
outputFile: event.outputFile,
|
|
3935
|
+
summary: event.summary,
|
|
3936
|
+
usage: event.usage,
|
|
3937
|
+
}, `${event.taskKind === 'agent' ? 'Agent' : 'Background'} task ${event.status}: ${label}`);
|
|
3938
|
+
}
|
|
3939
|
+
continue;
|
|
3940
|
+
}
|
|
3941
|
+
if (event.type === 'task_progress') {
|
|
3942
|
+
if (event.taskId) {
|
|
3943
|
+
const task = this.activeRunnerTasks.get(session.id)?.get(event.taskId);
|
|
3944
|
+
if (task) {
|
|
3945
|
+
task.description = task.description || event.description;
|
|
3946
|
+
task.subagentType = task.subagentType || event.subagentType;
|
|
3947
|
+
task.totalTokens = event.totalTokens ?? task.totalTokens;
|
|
3948
|
+
task.toolUses = event.toolUses ?? task.toolUses;
|
|
3949
|
+
task.durationMs = event.durationMs ?? task.durationMs;
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
this.eventBus.publish({
|
|
3953
|
+
type: 'runner:task-progress',
|
|
3954
|
+
sessionId: session.id,
|
|
3955
|
+
taskId: event.taskId,
|
|
3956
|
+
taskKind: event.taskKind,
|
|
3957
|
+
toolUseId: event.toolUseId,
|
|
3958
|
+
description: event.description,
|
|
3959
|
+
subagentType: event.subagentType,
|
|
3960
|
+
summary: event.summary,
|
|
3961
|
+
lastToolName: event.lastToolName,
|
|
3962
|
+
totalTokens: event.totalTokens,
|
|
3963
|
+
toolUses: event.toolUses,
|
|
3964
|
+
durationMs: event.durationMs,
|
|
3965
|
+
timestamp: Date.now(),
|
|
3966
|
+
causation,
|
|
3967
|
+
});
|
|
3968
|
+
}
|
|
3432
3969
|
const isCurrentlyBackground = this.isBackgroundSession(session, session.channel, session.channelId);
|
|
3433
3970
|
// === 前台任务:正常处理所有事件 ===
|
|
3434
3971
|
if (!isCurrentlyBackground) {
|
|
3435
3972
|
// 流式文本
|
|
3436
3973
|
if (event.type === 'text') {
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3974
|
+
// 网关偶尔会把上下文超限错误伪装成普通 text event。不要向事件总线或
|
|
3975
|
+
// 渲染器投影错误行;lastReplyText 仍保留原文本,以便流结束后触发恢复。
|
|
3976
|
+
const visibleText = stripContextLimitErrorLines(event.text);
|
|
3977
|
+
if (visibleText) {
|
|
3978
|
+
hasReceivedText = true;
|
|
3979
|
+
if (visibleText.trim())
|
|
3980
|
+
madeProgress = true;
|
|
3981
|
+
}
|
|
3440
3982
|
lastReplyText += event.text;
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3983
|
+
if (visibleText) {
|
|
3984
|
+
this.eventBus.publish({ type: 'message:text', sessionId: session.id, text: visibleText, isFinal: false });
|
|
3985
|
+
}
|
|
3986
|
+
if (visibleText && !shouldSuppress()) {
|
|
3987
|
+
renderer.addText(visibleText, event.outputTokens, event.turn);
|
|
3988
|
+
hasProjectedCurrentReplyText = true;
|
|
3444
3989
|
}
|
|
3445
3990
|
}
|
|
3446
3991
|
// compact 完成
|
|
@@ -3448,7 +3993,7 @@ export class ResponseEngine {
|
|
|
3448
3993
|
madeProgress = true;
|
|
3449
3994
|
this.eventBus.publish({ type: 'runner:compact-complete', sessionId: session.id, preTokens: event.preTokens });
|
|
3450
3995
|
if (!shouldSuppress()) {
|
|
3451
|
-
|
|
3996
|
+
await this.emitOperationalNotice(renderer, '\ud83d\udca1 会话压缩完成,继续执行...', 'info', 'compact');
|
|
3452
3997
|
}
|
|
3453
3998
|
}
|
|
3454
3999
|
// 子任务进度
|
|
@@ -3474,6 +4019,7 @@ export class ResponseEngine {
|
|
|
3474
4019
|
}
|
|
3475
4020
|
// 重置最后回复追踪
|
|
3476
4021
|
lastReplyText = '';
|
|
4022
|
+
hasProjectedCurrentReplyText = false;
|
|
3477
4023
|
this.eventBus.publish({
|
|
3478
4024
|
type: 'tool:use',
|
|
3479
4025
|
sessionId: session.id,
|
|
@@ -3570,14 +4116,14 @@ export class ResponseEngine {
|
|
|
3570
4116
|
// 仅记录状态,最终 flush(true) 在流结束后统一执行
|
|
3571
4117
|
if (event.type === 'complete') {
|
|
3572
4118
|
const isAbort = event.terminalReason === 'aborted_streaming' || event.terminalReason === 'aborted_tools';
|
|
3573
|
-
logger.info(`[ResponseEngine] ${isAbort ? 'task interrupted' : 'complete event'}: isError=${event.isError} terminalReason=${event.terminalReason ?? 'none'} subtype=${event.subtype ?? 'none'} hasReceivedText=${hasReceivedText}`);
|
|
4119
|
+
logger.info(`[ResponseEngine] ${isAbort ? 'task interrupted' : 'complete event'}: queryFinal=${event.queryFinal !== false} isError=${event.isError} terminalReason=${event.terminalReason ?? 'none'} subtype=${event.subtype ?? 'none'} hasReceivedText=${hasReceivedText}`);
|
|
3574
4120
|
// 自动回填会话名称
|
|
3575
4121
|
if (event.sessionTitle && shouldAutoFillSessionTitle(session.name, session.threadId)) {
|
|
3576
4122
|
await this.sessionManager.renameSession(session.id, event.sessionTitle);
|
|
3577
4123
|
logger.info(`[ResponseEngine] Auto-filled session name: ${event.sessionTitle}`);
|
|
3578
4124
|
}
|
|
3579
4125
|
// 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
|
|
3580
|
-
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 };
|
|
4126
|
+
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, model: event.model, tokenUsage: event.tokenUsage, contextUsage: event.contextUsage, lastModelCall: event.lastModelCall, modelCalls: event.modelCalls };
|
|
3581
4127
|
if (!event.isError)
|
|
3582
4128
|
madeProgress = true;
|
|
3583
4129
|
// thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
|
|
@@ -3586,7 +4132,7 @@ export class ResponseEngine {
|
|
|
3586
4132
|
// 但用户主动中断(新消息打断 或 /stop 命令)时不显示错误提示
|
|
3587
4133
|
// 上下文过长的错误留给外层 isPromptTooLong 触发 auto-compact,不在此处输出
|
|
3588
4134
|
const interruptReason = this.interruptedSessions.get(session.id);
|
|
3589
|
-
const isUserInterrupt = interruptReason
|
|
4135
|
+
const isUserInterrupt = isNonErrorInterruptReason(interruptReason);
|
|
3590
4136
|
const isContextTooLong = event.terminalReason === 'prompt_too_long'
|
|
3591
4137
|
|| isContextTooLongText(event.errors?.join(' ') || '')
|
|
3592
4138
|
|| isContextTooLongText(lastReplyText);
|
|
@@ -3613,7 +4159,7 @@ export class ResponseEngine {
|
|
|
3613
4159
|
}
|
|
3614
4160
|
// 检测 proactive 标志位,设置 lastProactiveFlag 供模式切换提示使用
|
|
3615
4161
|
// [迁移点5] 标志位检查:由 ProactiveMode.onComplete 实现
|
|
3616
|
-
if (modeHooks?.mode?.onComplete && lastReplyText) {
|
|
4162
|
+
if (event.queryFinal !== false && modeHooks?.mode?.onComplete && lastReplyText) {
|
|
3617
4163
|
await modeHooks.mode.onComplete({
|
|
3618
4164
|
session,
|
|
3619
4165
|
state: modeHooks.state,
|
|
@@ -3647,7 +4193,7 @@ export class ResponseEngine {
|
|
|
3647
4193
|
logger.info(`[ResponseEngine] Auto-filled session name: ${event.sessionTitle}`);
|
|
3648
4194
|
}
|
|
3649
4195
|
// 记录完成状态
|
|
3650
|
-
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 };
|
|
4196
|
+
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, model: event.model, tokenUsage: event.tokenUsage, contextUsage: event.contextUsage, lastModelCall: event.lastModelCall };
|
|
3651
4197
|
if (event.subtype === 'success')
|
|
3652
4198
|
madeProgress = true;
|
|
3653
4199
|
}
|
|
@@ -3655,7 +4201,7 @@ export class ResponseEngine {
|
|
|
3655
4201
|
catch (error) {
|
|
3656
4202
|
// User interrupt (AbortError) is expected, log at info level
|
|
3657
4203
|
const catchInterruptReason = this.interruptedSessions.get(session.id);
|
|
3658
|
-
const catchIsUserInterrupt = catchInterruptReason
|
|
4204
|
+
const catchIsUserInterrupt = isNonErrorInterruptReason(catchInterruptReason);
|
|
3659
4205
|
if (catchInterruptReason === 'timeout') {
|
|
3660
4206
|
logger.info('[ResponseEngine] Stream closed by timeout barrier');
|
|
3661
4207
|
throw this.timeoutErrors.get(session.id) ?? new Error('SDK_TIMEOUT');
|