evolcore 0.0.3 → 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 +64 -0
- 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 +1 -31
- package/dist/agents/codex-app-server-client.js +68 -0
- package/dist/agents/codex-runner.js +157 -5
- 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/channels/aun.js +220 -112
- package/dist/channels/daemon.js +18 -4
- package/dist/channels/dingtalk.js +52 -137
- package/dist/channels/feishu.js +56 -4
- package/dist/channels/qqbot.js +23 -1
- package/dist/channels/wechat.js +303 -155
- package/dist/channels/wecom.js +383 -7
- package/dist/cli/aun-commands.js +11 -11
- package/dist/cli/handoff-command.js +2 -1
- package/dist/cli/init-channel.js +185 -67
- package/dist/cli/init.js +7 -5
- package/dist/cli/trigger-command.js +92 -11
- package/dist/config/access-policy-domain.js +38 -0
- package/dist/config/access-policy.js +134 -0
- package/dist/config/builtin-role-templates.js +27 -14
- package/dist/config/builtin-roles.js +25 -6
- package/dist/config/config-field-policy.js +17 -5
- package/dist/config/config-manager.js +198 -22
- package/dist/config/config-operation-service.js +35 -38
- package/dist/config/contact-bind-code.js +274 -0
- package/dist/config/contact-book-store.js +173 -5
- package/dist/config/contact-book.js +32 -0
- package/dist/config/contact-operation-service.js +9 -3
- package/dist/config/contact-request-service.js +331 -0
- package/dist/config/peer-role-resolver.js +3 -1
- package/dist/config/schema-registry.js +49 -24
- package/dist/config-store.js +14 -2
- package/dist/core/auth/agent-delegation.js +105 -11
- package/dist/core/auth/authorization-audit.js +6 -0
- package/dist/core/auth/operation-authorizer.js +8 -0
- package/dist/core/auth/operation-catalog.js +51 -3
- 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 +10 -13
- package/dist/core/command/connect-menu.js +249 -24
- 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 +269 -85
- package/dist/core/command/menu-protocol.js +8 -2
- package/dist/core/command/menu-token-store.js +102 -0
- package/dist/core/command/slash-gate.js +1 -1
- package/dist/core/command/slash-handler.js +158 -31
- package/dist/core/daemon-file-cache.js +28 -0
- package/dist/core/event-catalog.js +29 -0
- package/dist/core/evolagent-registry.js +4 -39
- 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 +18 -14
- package/dist/core/message/inbound-admission.js +124 -0
- package/dist/core/message/message-bridge.js +616 -74
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +185 -10
- package/dist/core/message/peer-mode.js +7 -8
- package/dist/core/message/response-engine.js +381 -79
- 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 +69 -24
- package/dist/core/protected-paths.js +36 -11
- package/dist/core/session/session-manager.js +25 -3
- 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 +224 -89
- package/dist/ipc.js +81 -4
- 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/trigger/parser.js +55 -15
- package/dist/trigger/patch.js +4 -1
- package/dist/trigger/scheduler.js +441 -39
- package/dist/utils/error-dict.json +7 -0
- package/dist/utils/evolcore-version.js +21 -0
- package/dist/utils/logger.js +2 -2
- package/dist/utils/stable-semver.js +21 -0
- package/dist/utils/stats.js +33 -9
- package/kits/docs/channels/aun.md +4 -13
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/config.md +47 -2
- package/kits/docs/evolcore/contact.md +7 -3
- package/kits/docs/evolcore/group-rules.md +46 -4
- package/kits/docs/evolcore/group.md +4 -4
- package/kits/docs/evolcore/msg.md +5 -5
- package/kits/docs/evolcore/trigger.md +25 -8
- package/kits/docs/path-registry.md +36 -17
- package/kits/eck_message_manifest.json +12 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +7 -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 +18 -7
- 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.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.6.json +46 -0
- package/kits/schemas/relation-config.schema.7.json +59 -0
- 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 +46 -0
- package/kits/templates/roles/member.json +4 -0
- package/kits/templates/roles/visitor.json +4 -0
- package/kits/templates/system-fragments/channel.md +12 -2
- 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 +4 -2
- package/MIGRATION-0.5.0.md +0 -378
- package/ROLE_ACCESS_CONTROL.md +0 -174
- package/dist/channels/contact-bind-code.js +0 -134
- package/dist/channels/wecom-card.js +0 -101
- package/dist/channels/wecom-onboarding.js +0 -82
- package/dist/channels/wecom-state.js +0 -191
|
@@ -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,17 +516,50 @@ export class ResponseEngine {
|
|
|
428
516
|
if (!session)
|
|
429
517
|
return;
|
|
430
518
|
this.turnCoordinator.hydrate(session);
|
|
431
|
-
const
|
|
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);
|
|
432
524
|
const interruptedTaskId = interruptedTurn?.active?.taskId;
|
|
433
|
-
this.interruptedSessions.set(sessionId,
|
|
434
|
-
|
|
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;
|
|
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);
|
|
548
|
+
this.sessionManager.clearProcessingIfTask(sessionId, taskId);
|
|
549
|
+
if (this.activeTaskRuntimeContexts.get(sessionId)?.taskId === taskId) {
|
|
550
|
+
this.activeTaskRuntimeContexts.delete(sessionId);
|
|
551
|
+
}
|
|
552
|
+
this.agentDelegationRegistry?.revokeTask(sessionId, taskId);
|
|
553
|
+
}
|
|
554
|
+
this.closeActiveRunnerTasks(sessionId, effectiveReason, taskSpan?.causation ?? runtimeContext?.causation);
|
|
555
|
+
await this.interactionRouter?.cancelAll(sessionId, effectiveReason);
|
|
435
556
|
const channelKey = session.metadata?.channelKey || session.channel;
|
|
436
557
|
const agent = this.getAgent(channelKey, session.baseagent, session.selfAID);
|
|
437
558
|
const terminalStatus = (() => {
|
|
438
559
|
// TimeoutController owns timeout delivery (including user-facing timeout
|
|
439
560
|
// details) and already guarantees a single terminal status. Sending it
|
|
440
561
|
// here as well would race that barrier and duplicate status.timeout.
|
|
441
|
-
if (!interruptedTaskId ||
|
|
562
|
+
if (!interruptedTaskId || effectiveReason === 'timeout')
|
|
442
563
|
return Promise.resolve();
|
|
443
564
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
444
565
|
if (!channelInfo)
|
|
@@ -458,7 +579,7 @@ export class ResponseEngine {
|
|
|
458
579
|
});
|
|
459
580
|
const payload = {
|
|
460
581
|
kind: 'status.interrupted',
|
|
461
|
-
metadata: { reason },
|
|
582
|
+
metadata: { reason: effectiveReason },
|
|
462
583
|
};
|
|
463
584
|
return channelInfo.adapter.send(envelope, payload).then(() => undefined).catch(error => {
|
|
464
585
|
logger.debug(`[ResponseEngine] interrupt terminal status failure: session=${sessionId} task=${interruptedTaskId} error=${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -468,13 +589,33 @@ export class ResponseEngine {
|
|
|
468
589
|
logger.debug(`[ResponseEngine] interrupt barrier runner failure: session=${sessionId} error=${error instanceof Error ? error.message : String(error)}`);
|
|
469
590
|
});
|
|
470
591
|
await Promise.all([terminalStatus, runnerInterrupt]);
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
+
});
|
|
478
619
|
}
|
|
479
620
|
}
|
|
480
621
|
setHandoffRuntime(runtime) {
|
|
@@ -493,7 +634,7 @@ export class ResponseEngine {
|
|
|
493
634
|
if (!params.sessionId)
|
|
494
635
|
return { ok: false, code: 'HANDOFF_ID_REQUIRED', error: 'current session is required' };
|
|
495
636
|
if (this.agentDelegationRegistry) {
|
|
496
|
-
const delegation = this.agentDelegationRegistry.validate(params.delegationToken, params.sessionId);
|
|
637
|
+
const delegation = this.agentDelegationRegistry.validate(params.delegationToken, params.sessionId, params.delegationCommandHash);
|
|
497
638
|
if (!delegation.ok)
|
|
498
639
|
return { ok: false, code: delegation.code, error: delegation.reason };
|
|
499
640
|
}
|
|
@@ -653,26 +794,44 @@ export class ResponseEngine {
|
|
|
653
794
|
if (sessionId) {
|
|
654
795
|
this.eventBus.publish({ type: 'runner:compact-start', sessionId });
|
|
655
796
|
}
|
|
656
|
-
if (
|
|
657
|
-
|
|
658
|
-
|
|
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);
|
|
659
808
|
}
|
|
660
809
|
async retryAfterContextRecovery(prompt, opts) {
|
|
661
810
|
const { streamKey, renderer, agent, session, absoluteProjectPath, effectiveSystemPrompt, modelOverride, runtimeEnv, resetTimer, shouldSuppress, proactive, turnLease, } = opts;
|
|
662
811
|
if (!session.agentSessionId || !canCompactAgent(agent)) {
|
|
663
812
|
throw new Error('CONTEXT_COMPACT_FAILED');
|
|
664
813
|
}
|
|
665
|
-
|
|
814
|
+
// text event 在进入 renderer 前已经剔除了上下文超限错误行。保留缓冲中的
|
|
815
|
+
// 有效正文,由 sendOperationalNoticeAsText 先 flush,避免恢复过程丢失已有模型输出。
|
|
816
|
+
await this.emitOperationalNotice(renderer, '上下文过长,正在压缩会话...', 'warn', 'compact-trigger');
|
|
666
817
|
await renderer.flush();
|
|
667
|
-
|
|
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
|
+
}
|
|
668
826
|
if (compacted) {
|
|
669
|
-
|
|
827
|
+
await this.emitOperationalNotice(renderer, '✅ 压缩完成,继续处理...', 'info', 'compact-retry');
|
|
828
|
+
await renderer.flush();
|
|
670
829
|
const retryStream = await agent.runQuery(session.id, prompt, absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
|
|
671
830
|
agent.registerStream(streamKey, retryStream);
|
|
672
831
|
return await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress, proactive, undefined, // 重试分支不调插件钩子
|
|
673
832
|
undefined, turnLease);
|
|
674
833
|
}
|
|
675
|
-
|
|
834
|
+
await this.emitOperationalNotice(renderer, '⚠️ 压缩失败,尝试清空会话历史后重试...', 'warn', 'compact-failed-clear');
|
|
676
835
|
await renderer.flush();
|
|
677
836
|
if (!canClearAgent(agent)) {
|
|
678
837
|
throw new Error('CONTEXT_COMPACT_FAILED');
|
|
@@ -690,7 +849,8 @@ export class ResponseEngine {
|
|
|
690
849
|
}
|
|
691
850
|
session.agentSessionId = undefined;
|
|
692
851
|
await this.sessionManager.updateSession(session.id, { agentSessionId: null });
|
|
693
|
-
|
|
852
|
+
await this.emitOperationalNotice(renderer, '✅ 会话已清空,继续处理...', 'info', 'clear-retry');
|
|
853
|
+
await renderer.flush();
|
|
694
854
|
const retryStream = await agent.runQuery(session.id, '会话历史已清空,请继续之前未完成的任务。', absoluteProjectPath, undefined, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
|
|
695
855
|
agent.registerStream(streamKey, retryStream);
|
|
696
856
|
return await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress, proactive, undefined, // 重试分支不调插件钩子
|
|
@@ -911,6 +1071,19 @@ export class ResponseEngine {
|
|
|
911
1071
|
const accessAdapter = accessChannelInfo?.adapter;
|
|
912
1072
|
const accessAdapterSelfAid = typeof accessAdapter?._selfAid === 'function' ? accessAdapter._selfAid() : undefined;
|
|
913
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
|
+
}
|
|
914
1087
|
const triggerExecutionIdentity = this.resolveTriggerExecutionIdentity(message, selfAidForAccess);
|
|
915
1088
|
const handoffExecutionIdentity = this.resolveHandoffOriginExecutionIdentity(message, selfAidForAccess);
|
|
916
1089
|
const executionIdentity = triggerExecutionIdentity ?? handoffExecutionIdentity;
|
|
@@ -1012,7 +1185,6 @@ export class ResponseEngine {
|
|
|
1012
1185
|
const shouldSuppress = () => {
|
|
1013
1186
|
return outputState.middleOutputMode === 'none';
|
|
1014
1187
|
};
|
|
1015
|
-
this.shouldSuppressActivities = shouldSuppress();
|
|
1016
1188
|
let monitor;
|
|
1017
1189
|
let monitorInterval;
|
|
1018
1190
|
let totalExecutionTimer;
|
|
@@ -1173,6 +1345,18 @@ export class ResponseEngine {
|
|
|
1173
1345
|
? configuredSeconds * 1000
|
|
1174
1346
|
: 60 * 60 * 1000;
|
|
1175
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
|
+
}
|
|
1176
1360
|
/** 自动安全模式已禁用:仅保留错误计数,不再自动切换状态 */
|
|
1177
1361
|
async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, getLastIdleSec, outputState = { middleOutputMode: 'all' }, timeoutControl) {
|
|
1178
1362
|
const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
|
|
@@ -1231,6 +1415,11 @@ export class ResponseEngine {
|
|
|
1231
1415
|
status: 'started',
|
|
1232
1416
|
refs: { taskId, sessionId: session.id, messageId: message.messageId },
|
|
1233
1417
|
});
|
|
1418
|
+
this.activeTaskSpans.set(session.id, {
|
|
1419
|
+
taskId,
|
|
1420
|
+
messageId: message.messageId,
|
|
1421
|
+
causation: taskCausation,
|
|
1422
|
+
});
|
|
1234
1423
|
for (const item of message.items ?? []) {
|
|
1235
1424
|
const linked = normalizeCausation(item.causation);
|
|
1236
1425
|
if (!linked || linked.spanId === inputCausation.spanId)
|
|
@@ -1255,7 +1444,9 @@ export class ResponseEngine {
|
|
|
1255
1444
|
const adapterAny = channelInfo.adapter;
|
|
1256
1445
|
const adapterSelfAid = typeof adapterAny._selfAid === 'function' ? adapterAny._selfAid() : undefined;
|
|
1257
1446
|
const selfAid = adapterSelfAid || message.selfAID || session.selfAID || undefined;
|
|
1258
|
-
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;
|
|
1259
1450
|
const peerName = message.peerName || session.metadata?.peerName;
|
|
1260
1451
|
const peerIdRaw = message.peerId;
|
|
1261
1452
|
const triggerExecutionIdentity = this.resolveTriggerExecutionIdentity(message, selfAid);
|
|
@@ -1389,7 +1580,6 @@ export class ResponseEngine {
|
|
|
1389
1580
|
? 'none'
|
|
1390
1581
|
: configuredMiddleOutputMode;
|
|
1391
1582
|
outputState.middleOutputMode = middleOutputMode;
|
|
1392
|
-
this.shouldSuppressActivities = shouldSuppress();
|
|
1393
1583
|
// 诊断日志:记录 inbound message_id 和生成的 task_id 的对应关系
|
|
1394
1584
|
logger.info(`[ResponseEngine] Task created: inboundMsgId=${message.messageId ?? 'none'} taskId=${taskId} sessionId=${session.id} chatmode=${chatmode} mode=${resolvedMode?.mode.id ?? 'override/fallback'}`);
|
|
1395
1585
|
// 构建带 taskId/chatmode 的 ReplyContext(本次任务所有出站消息共用)
|
|
@@ -1617,17 +1807,16 @@ export class ResponseEngine {
|
|
|
1617
1807
|
taskId,
|
|
1618
1808
|
channelId: message.channelId,
|
|
1619
1809
|
messageIds: message.sourceMessageIds,
|
|
1810
|
+
restartResume: message.restartResume?.submitted === true,
|
|
1620
1811
|
}).catch(() => { });
|
|
1621
1812
|
}
|
|
1622
1813
|
adapter.send(envelope, { kind: 'status.started' }).catch(() => { });
|
|
1623
|
-
await this.runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, adapter, envelope, false);
|
|
1624
1814
|
logger.message({
|
|
1625
1815
|
msgId: messageId,
|
|
1626
1816
|
sessionId: session.id,
|
|
1627
1817
|
dir: 'inbound',
|
|
1628
1818
|
status: 'processing'
|
|
1629
1819
|
});
|
|
1630
|
-
const startTime = Date.now();
|
|
1631
1820
|
// 创建 IMRenderer(统一 interactive/proactive 两条路径)
|
|
1632
1821
|
let firstReply = true;
|
|
1633
1822
|
const renderer = new IMRenderer({
|
|
@@ -1636,6 +1825,7 @@ export class ResponseEngine {
|
|
|
1636
1825
|
flushDelay: (options?.flushDelay ?? this.agentRegistry?.resolveByChannel(channelKey)?.config?.flush_delay ?? 3) * 1000,
|
|
1637
1826
|
suppressActivityItems: isProactive ? false : middleOutputMode !== 'all',
|
|
1638
1827
|
suppressIntermediateText: isProactive ? false : middleOutputMode === 'none',
|
|
1828
|
+
operationalNoticesAsText: !isProactive && middleOutputMode !== 'none',
|
|
1639
1829
|
fileMarkerPattern: options?.fileMarkerPattern,
|
|
1640
1830
|
diagEnabled: this.globalSettings.debug?.flusherDiag,
|
|
1641
1831
|
send: async (payload) => {
|
|
@@ -1679,8 +1869,17 @@ export class ResponseEngine {
|
|
|
1679
1869
|
await adapter.send(enrichedEnvelope, payload);
|
|
1680
1870
|
},
|
|
1681
1871
|
});
|
|
1682
|
-
this.
|
|
1872
|
+
this.activeRenderers.set(session.id, {
|
|
1873
|
+
taskId,
|
|
1874
|
+
renderer,
|
|
1875
|
+
suppressActivities: shouldSuppress(),
|
|
1876
|
+
});
|
|
1683
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();
|
|
1684
1883
|
if (isProactive) {
|
|
1685
1884
|
logger.info(`[ResponseEngine] proactive mode: outputs via thought.put task=${taskId}`);
|
|
1686
1885
|
}
|
|
@@ -1821,6 +2020,14 @@ export class ResponseEngine {
|
|
|
1821
2020
|
approvalRouting,
|
|
1822
2021
|
approvalInteractionPolicy: authorizationIdentity ? 'deny' : 'interactive',
|
|
1823
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,
|
|
1824
2031
|
turn: {
|
|
1825
2032
|
taskId,
|
|
1826
2033
|
turnId: turnLease.turnId,
|
|
@@ -2125,13 +2332,13 @@ export class ResponseEngine {
|
|
|
2125
2332
|
+ ` modelSource=${effectiveModelSource ?? 'runner-default'}`
|
|
2126
2333
|
+ ` effort=${effectiveEffort ?? '<runner-default>'}`
|
|
2127
2334
|
+ ` effortSource=${effectiveEffortSource ?? 'runner-default'}`);
|
|
2128
|
-
const groupRulesVars =
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
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
|
+
});
|
|
2135
2342
|
// Kit renderer: 组装上下文
|
|
2136
2343
|
const pkgRoot = getPackageRoot();
|
|
2137
2344
|
const kitCtx = {
|
|
@@ -2197,6 +2404,7 @@ export class ResponseEngine {
|
|
|
2197
2404
|
// Stage 3: sessionKey 持久化字段
|
|
2198
2405
|
sessionKey: session.sessionKey,
|
|
2199
2406
|
chatMode: isProactive ? 'proactive' : 'interactive',
|
|
2407
|
+
isTriggerTask: causationPath.length > 0 || message.source === 'trigger',
|
|
2200
2408
|
// proactive 为 null(interactive 模式,这些开关不适用)时如实为 false——
|
|
2201
2409
|
// 默认值不在此硬编码,出厂默认由模式 schema 声明、经 buildState 落入 proactive.*。
|
|
2202
2410
|
proactivePreTool1stMsgChk: proactive?.preTool1stMsgChk ?? false,
|
|
@@ -2276,9 +2484,9 @@ export class ResponseEngine {
|
|
|
2276
2484
|
if (message.handoffDelivery && this.handoffRuntime) {
|
|
2277
2485
|
const items = this.handoffRuntime.buildPromptItems(message);
|
|
2278
2486
|
if (items.length > 0) {
|
|
2279
|
-
v2HandoffIds = items
|
|
2280
|
-
|
|
2281
|
-
.
|
|
2487
|
+
v2HandoffIds = Array.from(new Set(items.flatMap(item => (item.handoff?.handoffIds?.length
|
|
2488
|
+
? item.handoff.handoffIds
|
|
2489
|
+
: (item.handoff?.handoffId ? [item.handoff.handoffId] : [])))));
|
|
2282
2490
|
v2HandoffDirection = message.handoffDelivery.direction;
|
|
2283
2491
|
return items;
|
|
2284
2492
|
}
|
|
@@ -2345,9 +2553,12 @@ export class ResponseEngine {
|
|
|
2345
2553
|
this.publishTriggerExecutionSkipped(message, 'empty_prompt', taskCausation);
|
|
2346
2554
|
return;
|
|
2347
2555
|
}
|
|
2348
|
-
if (v2HandoffIds.length > 0 &&
|
|
2556
|
+
if (v2HandoffIds.length > 0 && handoffPromptRendered && this.handoffRuntime && selfAid) {
|
|
2349
2557
|
for (const handoffId of v2HandoffIds) {
|
|
2350
|
-
|
|
2558
|
+
if (v2HandoffDirection === 'origin')
|
|
2559
|
+
this.handoffRuntime.completeOriginContext(selfAid, handoffId);
|
|
2560
|
+
else if (v2HandoffDirection === 'target')
|
|
2561
|
+
this.handoffRuntime.completeTargetContext(selfAid, handoffId);
|
|
2351
2562
|
}
|
|
2352
2563
|
}
|
|
2353
2564
|
const taskRuntimeContext = {
|
|
@@ -2395,6 +2606,7 @@ export class ResponseEngine {
|
|
|
2395
2606
|
// 可重试错误(403/429/5xx/模型繁忙)按明确退避序列重试。
|
|
2396
2607
|
const RETRY_DELAYS_MS = [5_000, 10_000, 30_000];
|
|
2397
2608
|
const MAX_RETRIES = RETRY_DELAYS_MS.length;
|
|
2609
|
+
const retryAttemptTimeoutMs = this.retryAttemptTimeoutMs();
|
|
2398
2610
|
let runAttempt = 1;
|
|
2399
2611
|
let consecutiveRetryFailures = 0;
|
|
2400
2612
|
const recordRetryHealthError = async (retryError) => {
|
|
@@ -2412,12 +2624,24 @@ export class ResponseEngine {
|
|
|
2412
2624
|
};
|
|
2413
2625
|
while (true) {
|
|
2414
2626
|
let streamRegistered = false;
|
|
2627
|
+
const shouldTimeoutRetryAttempt = consecutiveRetryFailures > 0 && retryAttemptTimeoutMs !== undefined;
|
|
2628
|
+
let attemptTimeout;
|
|
2415
2629
|
try {
|
|
2416
2630
|
logger.info(`[ResponseEngine] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${runAttempt} consecutiveFailures=${consecutiveRetryFailures} agentSessionId=${session.agentSessionId ?? 'none'}`);
|
|
2417
2631
|
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, renderResult?.images.length ? renderResult.images : message.images, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
|
|
2418
2632
|
agent.registerStream(streamKey, stream);
|
|
2419
2633
|
streamRegistered = true;
|
|
2420
|
-
|
|
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;
|
|
2421
2645
|
if (streamResult.isError && !streamHitContextLimit(streamResult)) {
|
|
2422
2646
|
const streamErrorText = getStreamErrorText(streamResult);
|
|
2423
2647
|
if (streamErrorText && isRetryableError(new Error(streamErrorText))) {
|
|
@@ -2433,6 +2657,14 @@ export class ResponseEngine {
|
|
|
2433
2657
|
break; // 成功,跳出重试循环
|
|
2434
2658
|
}
|
|
2435
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
|
+
}
|
|
2436
2668
|
if (streamRegistered) {
|
|
2437
2669
|
agent.cleanupStream(streamKey);
|
|
2438
2670
|
}
|
|
@@ -2463,7 +2695,7 @@ export class ResponseEngine {
|
|
|
2463
2695
|
runAttempt++;
|
|
2464
2696
|
continue;
|
|
2465
2697
|
}
|
|
2466
|
-
if (isRetryableError(retryError)) {
|
|
2698
|
+
if (retryAttemptTimedOut || isRetryableError(retryError)) {
|
|
2467
2699
|
if (didRetryMakeProgress(retryError)) {
|
|
2468
2700
|
consecutiveRetryFailures = 0;
|
|
2469
2701
|
}
|
|
@@ -2479,7 +2711,7 @@ export class ResponseEngine {
|
|
|
2479
2711
|
const nextRetryNumber = consecutiveRetryFailures + 1;
|
|
2480
2712
|
consecutiveRetryFailures++;
|
|
2481
2713
|
logger.warn(`[ResponseEngine] Retryable error (attempt ${runAttempt}, consecutive ${consecutiveRetryFailures}/${MAX_RETRIES}), retrying in ${delay}ms:`, retryError);
|
|
2482
|
-
|
|
2714
|
+
await this.emitOperationalNotice(renderer, `API 不可用,${delay / 1000}秒后重试 ${nextRetryNumber}/${MAX_RETRIES}`, 'warn', 'retry');
|
|
2483
2715
|
await renderer.flush();
|
|
2484
2716
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
2485
2717
|
// 延迟后再次检查中断状态(延迟期间可能收到中断信号)
|
|
@@ -2495,6 +2727,9 @@ export class ResponseEngine {
|
|
|
2495
2727
|
}
|
|
2496
2728
|
throw retryError; // 不可重试或已耗尽重试次数
|
|
2497
2729
|
}
|
|
2730
|
+
finally {
|
|
2731
|
+
attemptTimeout?.clear();
|
|
2732
|
+
}
|
|
2498
2733
|
}
|
|
2499
2734
|
}
|
|
2500
2735
|
catch (error) {
|
|
@@ -2557,12 +2792,12 @@ export class ResponseEngine {
|
|
|
2557
2792
|
// 重试后仍然 prompt_too_long:显示友好提示
|
|
2558
2793
|
const retryStillTooLong = streamResult.isError && streamHitContextLimit(streamResult);
|
|
2559
2794
|
if (retryStillTooLong) {
|
|
2560
|
-
|
|
2795
|
+
await this.emitOperationalNotice(renderer, getContextTooLongHint(agent), 'warn', 'context-too-long');
|
|
2561
2796
|
}
|
|
2562
2797
|
}
|
|
2563
2798
|
else if (streamResult.isError && streamHitContextLimit(streamResult)) {
|
|
2564
2799
|
// 上下文过长但无法 auto-compact(无 session ID 或 agent 不支持),显示友好提示
|
|
2565
|
-
|
|
2800
|
+
await this.emitOperationalNotice(renderer, getContextTooLongHint(agent), 'warn', 'context-too-long');
|
|
2566
2801
|
}
|
|
2567
2802
|
if (!streamResult.isError) {
|
|
2568
2803
|
const commitEvidence = this.buildTurnCommitEvidence(session, streamResult);
|
|
@@ -2634,7 +2869,7 @@ export class ResponseEngine {
|
|
|
2634
2869
|
if (!this.turnCoordinator.isCurrent(turnLease))
|
|
2635
2870
|
return 0;
|
|
2636
2871
|
// 仅支持公开文件标记语法:[SEND_FILE:path] 或 [SEND_FILE:channel:path]。
|
|
2637
|
-
const SEND_FILE_RE = options?.fileMarkerPattern ??
|
|
2872
|
+
const SEND_FILE_RE = options?.fileMarkerPattern ?? createSendFileMarkerPattern();
|
|
2638
2873
|
const sendFileMatches = [...scanText.matchAll(SEND_FILE_RE)];
|
|
2639
2874
|
if (sendFileMatches.length === 0)
|
|
2640
2875
|
return 0;
|
|
@@ -2730,6 +2965,7 @@ export class ResponseEngine {
|
|
|
2730
2965
|
}
|
|
2731
2966
|
// 最终回复文本:suppressed 模式或无 text 事件时需要兜底添加
|
|
2732
2967
|
const finalReplyText = streamResult.lastReplyText || streamResult.fullText;
|
|
2968
|
+
const visibleFinalReplyText = stripContextLimitErrorLines(finalReplyText);
|
|
2733
2969
|
if (!this.turnCoordinator.isCurrent(turnLease)) {
|
|
2734
2970
|
agent.cleanupStream(streamKey);
|
|
2735
2971
|
this.sessionManager.clearProcessingIfTask(session.id, taskId);
|
|
@@ -2741,9 +2977,11 @@ export class ResponseEngine {
|
|
|
2741
2977
|
await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_turn', taskCausation);
|
|
2742
2978
|
return;
|
|
2743
2979
|
}
|
|
2744
|
-
if (
|
|
2745
|
-
|
|
2746
|
-
|
|
2980
|
+
if (visibleFinalReplyText) {
|
|
2981
|
+
// `hasReceivedText` 是任务级计数。工具调用前的文字已经显示,并不表示最后一轮
|
|
2982
|
+
// (通常来自 complete.result)的文字也已显示;用本轮投影标记避免终态丢失。
|
|
2983
|
+
if (shouldSuppress() || !streamResult.hasProjectedCurrentReplyText) {
|
|
2984
|
+
renderer.addText(visibleFinalReplyText);
|
|
2747
2985
|
}
|
|
2748
2986
|
}
|
|
2749
2987
|
// 先清理流和处理中状态(保证即使 flush 卡住,session 也不会永久处于"处理中")
|
|
@@ -2771,7 +3009,7 @@ export class ResponseEngine {
|
|
|
2771
3009
|
}
|
|
2772
3010
|
// 被用户中断(新消息打断)时跳过 flush — 新 task 已接管渠道,旧 task 的 flush 无意义且可能卡住
|
|
2773
3011
|
const preFlushInterrupt = this.interruptedSessions.get(session.id);
|
|
2774
|
-
if (preFlushInterrupt
|
|
3012
|
+
if (isNonErrorInterruptReason(preFlushInterrupt)) {
|
|
2775
3013
|
logger.info(`[ResponseEngine] Skipping flush for interrupted task=${taskId} reason=${preFlushInterrupt}`);
|
|
2776
3014
|
}
|
|
2777
3015
|
else {
|
|
@@ -2812,13 +3050,13 @@ export class ResponseEngine {
|
|
|
2812
3050
|
// 用户主动打断(新消息//stop/撤回)会让 SDK 流在工具调用中途被掐断,
|
|
2813
3051
|
// 末尾 result message 形状异常并被标记为 error(含 SDK 内部 ede_diagnostic 串)。
|
|
2814
3052
|
// 这不是真正的失败,不应把诊断串暴露给用户,也不计入错误统计。
|
|
2815
|
-
const
|
|
2816
|
-
if (!
|
|
3053
|
+
const isExpectedInterrupt = isNonErrorInterruptReason(interruptReason);
|
|
3054
|
+
if (!isExpectedInterrupt && !permissionTerminalFailure) {
|
|
2817
3055
|
await adapter.send(envelope, { kind: 'result.error', text: userErrorSummary, reason: rawSubtype }).catch(() => { });
|
|
2818
3056
|
await adapter.send(envelope, { kind: 'status.error', metadata: { errorType: rawSubtype } }).catch(() => { });
|
|
2819
3057
|
this.touchAgentActivity(channelKey);
|
|
2820
3058
|
}
|
|
2821
|
-
if (
|
|
3059
|
+
if (isExpectedInterrupt) {
|
|
2822
3060
|
// 用户打断:打断本身已由 message-queue 发过 task:interrupted 事件,
|
|
2823
3061
|
// 这里不再补发 task:error(否则同一次打断被记两遍且错误归类为 error)。
|
|
2824
3062
|
// 仅记 info 日志收尾。注意:task:interrupted 已填充 interruptedSessions,
|
|
@@ -2835,6 +3073,8 @@ export class ResponseEngine {
|
|
|
2835
3073
|
await this.publishTriggerExecutionInterrupted(message, adapter, envelope, interruptReason ?? 'interrupted', taskCausation);
|
|
2836
3074
|
}
|
|
2837
3075
|
else {
|
|
3076
|
+
logger.error(`[ResponseEngine] Task failed: session=${session.id} task=${taskId} ` +
|
|
3077
|
+
`type=${errorType} terminalReason=${streamResult.terminalReason ?? 'none'} error=${errorSummary}`);
|
|
2838
3078
|
this.publishTriggerExecutionFailure(message, errorSummary, { messageId, causation: taskCausation });
|
|
2839
3079
|
this.eventBus.publish({
|
|
2840
3080
|
type: 'task:error',
|
|
@@ -3128,10 +3368,7 @@ export class ResponseEngine {
|
|
|
3128
3368
|
&& interruptReason !== 'timeout'
|
|
3129
3369
|
&& !isTimeout
|
|
3130
3370
|
&& !isTotalExecutionTimeout;
|
|
3131
|
-
const isUserInterrupt = interruptReason
|
|
3132
|
-
|| interruptReason === 'stop'
|
|
3133
|
-
|| interruptReason === 'recalled'
|
|
3134
|
-
|| staleSupersededTurn;
|
|
3371
|
+
const isUserInterrupt = isNonErrorInterruptReason(interruptReason) || staleSupersededTurn;
|
|
3135
3372
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
3136
3373
|
if (isUserInterrupt) {
|
|
3137
3374
|
await this.publishTriggerExecutionInterrupted(message, adapter, envelope, interruptReason ?? 'stale_turn', taskCausation);
|
|
@@ -3257,6 +3494,12 @@ export class ResponseEngine {
|
|
|
3257
3494
|
timeoutControl.terminalDelivered = true;
|
|
3258
3495
|
}
|
|
3259
3496
|
}
|
|
3497
|
+
finally {
|
|
3498
|
+
// 同一 session 的新任务可能已替换登记;旧任务结束时不得删掉新任务的 renderer。
|
|
3499
|
+
if (this.activeRenderers.get(session.id)?.taskId === taskId) {
|
|
3500
|
+
this.activeRenderers.delete(session.id);
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3260
3503
|
// [迁移探针] 任务收尾:记录工具提醒最终状态并落盘(防线 1)
|
|
3261
3504
|
if (snapshot.isEnabled()) {
|
|
3262
3505
|
snapshot.set(session.id, taskId, {
|
|
@@ -3265,7 +3508,7 @@ export class ResponseEngine {
|
|
|
3265
3508
|
snapshot.end(session.id, taskId);
|
|
3266
3509
|
}
|
|
3267
3510
|
}
|
|
3268
|
-
async runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath,
|
|
3511
|
+
async runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, renderer) {
|
|
3269
3512
|
if (!session.agentSessionId || !canCompactAgent(agent)) {
|
|
3270
3513
|
logger.debug(`[ResponseEngine] Auto compact skipped: session=${session.id} agentSessionId=${session.agentSessionId || 'none'} canCompact=${canCompactAgent(agent)} agent=${agent.name}`);
|
|
3271
3514
|
return;
|
|
@@ -3276,15 +3519,14 @@ export class ResponseEngine {
|
|
|
3276
3519
|
return;
|
|
3277
3520
|
}
|
|
3278
3521
|
logger.info(`[ResponseEngine] Auto compact at task.start: session=${session.id} totalTokens=${ctx.totalTokens} autoCompactTokens=${ctx.autoCompactTokens}`);
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3522
|
+
await this.emitOperationalNotice(renderer, '上下文接近上限,正在压缩会话...', 'info', 'auto-compact-start');
|
|
3523
|
+
await renderer.flush();
|
|
3524
|
+
this.announcedCompactStarts.add(session.id);
|
|
3282
3525
|
try {
|
|
3283
3526
|
const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
|
|
3284
3527
|
if (compacted) {
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
}
|
|
3528
|
+
await this.emitOperationalNotice(renderer, '✅ 上下文压缩完成,继续处理...', 'info', 'auto-compact-complete');
|
|
3529
|
+
await renderer.flush();
|
|
3288
3530
|
}
|
|
3289
3531
|
else {
|
|
3290
3532
|
logger.warn(`[ResponseEngine] Auto compact at task.start returned false (session=${session.id})`);
|
|
@@ -3293,6 +3535,9 @@ export class ResponseEngine {
|
|
|
3293
3535
|
catch (err) {
|
|
3294
3536
|
logger.warn(`[ResponseEngine] Auto compact at task.start failed (non-fatal):`, err);
|
|
3295
3537
|
}
|
|
3538
|
+
finally {
|
|
3539
|
+
this.announcedCompactStarts.delete(session.id);
|
|
3540
|
+
}
|
|
3296
3541
|
}
|
|
3297
3542
|
async readLastModelCallContextUsage(sessionId, agentSessionId) {
|
|
3298
3543
|
try {
|
|
@@ -3359,17 +3604,28 @@ export class ResponseEngine {
|
|
|
3359
3604
|
const owningAgent = this.agentRegistry?.resolveByChannel(message.channel);
|
|
3360
3605
|
const projectPath = owningAgent?.projectPath || process.cwd();
|
|
3361
3606
|
const resolvedBaseagent = message.baseagent || owningAgent?.baseagent || (!this.agentRegistry ? this.inferPrimaryBaseagent() : undefined);
|
|
3362
|
-
|
|
3607
|
+
// 优先复用 MessageBridge 入队前已解析的身份,避免二次独立解析产生角色漂移
|
|
3608
|
+
// (曾观测到瞬时竞态下把 owner 误判为 none,导致合法用户被静默拒绝)。
|
|
3609
|
+
// 仅当消息不经 MessageBridge.register()(如 trigger 直接投递)时才回退重新解析。
|
|
3610
|
+
const resolvedIdentity = message.resolvedIdentity ?? this.inferDirectSessionIdentity(message, owningAgent);
|
|
3363
3611
|
if (!resolvedBaseagent) {
|
|
3364
3612
|
throw new Error(`[ResponseEngine] resolveSession: baseagent could not be determined (message.baseagent=${message.baseagent}, owningAgent=${owningAgent?.name || 'none'})`);
|
|
3365
3613
|
}
|
|
3366
3614
|
// 话题创建权限守卫已统一移至 MessageBridge.canCreateThreadSession(enqueue 前拦截),
|
|
3367
3615
|
// 此处不再重复检查——bridge 层拒绝后消息根本不会到达 processMessage。
|
|
3368
|
-
//
|
|
3369
|
-
|
|
3370
|
-
|
|
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);
|
|
3371
3621
|
if (bound) {
|
|
3372
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;
|
|
3373
3629
|
if (bound.threadId) {
|
|
3374
3630
|
const absoluteProjectPath = path.isAbsolute(bound.projectPath)
|
|
3375
3631
|
? bound.projectPath : path.resolve(process.cwd(), bound.projectPath);
|
|
@@ -3378,14 +3634,22 @@ export class ResponseEngine {
|
|
|
3378
3634
|
const switched = await this.sessionManager.switchToSession(bound.channel, bound.channelId, bound.id);
|
|
3379
3635
|
if (switched) {
|
|
3380
3636
|
this.ensureSessionBaseagent(switched, resolvedBaseagent);
|
|
3637
|
+
if (resolvedIdentity)
|
|
3638
|
+
switched.identity = resolvedIdentity;
|
|
3381
3639
|
const absoluteProjectPath = path.isAbsolute(switched.projectPath)
|
|
3382
3640
|
? switched.projectPath : path.resolve(process.cwd(), switched.projectPath);
|
|
3383
3641
|
return { session: switched, absoluteProjectPath };
|
|
3384
3642
|
}
|
|
3643
|
+
if (message.boundSessionId) {
|
|
3644
|
+
throw new Error(`[ResponseEngine] Bound session ${bound.id} could not be activated`);
|
|
3645
|
+
}
|
|
3385
3646
|
logger.warn(`[ResponseEngine] switchToSession failed for bound session ${bound.id}, falling back to latest`);
|
|
3386
3647
|
}
|
|
3387
3648
|
else {
|
|
3388
|
-
|
|
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`);
|
|
3389
3653
|
}
|
|
3390
3654
|
}
|
|
3391
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);
|
|
@@ -3444,6 +3708,7 @@ export class ResponseEngine {
|
|
|
3444
3708
|
const statsChannelKey = session.channel === 'daemon' ? session.channel : (session.metadata?.channelKey || session.channel);
|
|
3445
3709
|
const agentNameForStats = this.agentRegistry?.resolveByChannel(statsChannelKey)?.name ?? '<unknown>';
|
|
3446
3710
|
let hasReceivedText = false;
|
|
3711
|
+
let hasProjectedCurrentReplyText = false;
|
|
3447
3712
|
let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
|
|
3448
3713
|
let madeProgress = false;
|
|
3449
3714
|
let completeResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
|
|
@@ -3457,6 +3722,7 @@ export class ResponseEngine {
|
|
|
3457
3722
|
const finalizeResult = () => ({
|
|
3458
3723
|
...completeResult,
|
|
3459
3724
|
hasReceivedText,
|
|
3725
|
+
hasProjectedCurrentReplyText,
|
|
3460
3726
|
inputAccepted,
|
|
3461
3727
|
assistantUuid,
|
|
3462
3728
|
protocolIncompleteReason,
|
|
@@ -3583,6 +3849,19 @@ export class ResponseEngine {
|
|
|
3583
3849
|
continue;
|
|
3584
3850
|
}
|
|
3585
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
|
+
});
|
|
3586
3865
|
this.eventBus.publish({
|
|
3587
3866
|
type: 'runner:task-started',
|
|
3588
3867
|
sessionId: session.id,
|
|
@@ -3622,6 +3901,10 @@ export class ResponseEngine {
|
|
|
3622
3901
|
continue;
|
|
3623
3902
|
}
|
|
3624
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);
|
|
3625
3908
|
this.eventBus.publish({
|
|
3626
3909
|
type: 'runner:task-notification',
|
|
3627
3910
|
sessionId: session.id,
|
|
@@ -3656,6 +3939,16 @@ export class ResponseEngine {
|
|
|
3656
3939
|
continue;
|
|
3657
3940
|
}
|
|
3658
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
|
+
}
|
|
3659
3952
|
this.eventBus.publish({
|
|
3660
3953
|
type: 'runner:task-progress',
|
|
3661
3954
|
sessionId: session.id,
|
|
@@ -3678,13 +3971,21 @@ export class ResponseEngine {
|
|
|
3678
3971
|
if (!isCurrentlyBackground) {
|
|
3679
3972
|
// 流式文本
|
|
3680
3973
|
if (event.type === 'text') {
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
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
|
+
}
|
|
3684
3982
|
lastReplyText += event.text;
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
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;
|
|
3688
3989
|
}
|
|
3689
3990
|
}
|
|
3690
3991
|
// compact 完成
|
|
@@ -3692,7 +3993,7 @@ export class ResponseEngine {
|
|
|
3692
3993
|
madeProgress = true;
|
|
3693
3994
|
this.eventBus.publish({ type: 'runner:compact-complete', sessionId: session.id, preTokens: event.preTokens });
|
|
3694
3995
|
if (!shouldSuppress()) {
|
|
3695
|
-
|
|
3996
|
+
await this.emitOperationalNotice(renderer, '\ud83d\udca1 会话压缩完成,继续执行...', 'info', 'compact');
|
|
3696
3997
|
}
|
|
3697
3998
|
}
|
|
3698
3999
|
// 子任务进度
|
|
@@ -3718,6 +4019,7 @@ export class ResponseEngine {
|
|
|
3718
4019
|
}
|
|
3719
4020
|
// 重置最后回复追踪
|
|
3720
4021
|
lastReplyText = '';
|
|
4022
|
+
hasProjectedCurrentReplyText = false;
|
|
3721
4023
|
this.eventBus.publish({
|
|
3722
4024
|
type: 'tool:use',
|
|
3723
4025
|
sessionId: session.id,
|
|
@@ -3830,7 +4132,7 @@ export class ResponseEngine {
|
|
|
3830
4132
|
// 但用户主动中断(新消息打断 或 /stop 命令)时不显示错误提示
|
|
3831
4133
|
// 上下文过长的错误留给外层 isPromptTooLong 触发 auto-compact,不在此处输出
|
|
3832
4134
|
const interruptReason = this.interruptedSessions.get(session.id);
|
|
3833
|
-
const isUserInterrupt = interruptReason
|
|
4135
|
+
const isUserInterrupt = isNonErrorInterruptReason(interruptReason);
|
|
3834
4136
|
const isContextTooLong = event.terminalReason === 'prompt_too_long'
|
|
3835
4137
|
|| isContextTooLongText(event.errors?.join(' ') || '')
|
|
3836
4138
|
|| isContextTooLongText(lastReplyText);
|
|
@@ -3899,7 +4201,7 @@ export class ResponseEngine {
|
|
|
3899
4201
|
catch (error) {
|
|
3900
4202
|
// User interrupt (AbortError) is expected, log at info level
|
|
3901
4203
|
const catchInterruptReason = this.interruptedSessions.get(session.id);
|
|
3902
|
-
const catchIsUserInterrupt = catchInterruptReason
|
|
4204
|
+
const catchIsUserInterrupt = isNonErrorInterruptReason(catchInterruptReason);
|
|
3903
4205
|
if (catchInterruptReason === 'timeout') {
|
|
3904
4206
|
logger.info('[ResponseEngine] Stream closed by timeout barrier');
|
|
3905
4207
|
throw this.timeoutErrors.get(session.id) ?? new Error('SDK_TIMEOUT');
|