evolcore 0.0.39 → 0.0.40
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 +49 -0
- package/README.md +31 -20
- package/bin/ec-safe-output.js +14 -2
- package/dist/agents/baseagent-availability.js +1 -0
- package/dist/agents/claude-runner.js +32 -46
- package/dist/agents/codex-runner.js +51 -27
- package/dist/agents/ecagent-runner.js +18 -7
- package/dist/agents/gemini-runner.js +1 -2
- package/dist/agents/runner-types.js +28 -6
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/aid/control-aid.js +23 -98
- package/dist/aun/aid/ownership-operation.js +1 -3
- package/dist/aun/group/invite-service.js +6 -0
- package/dist/aun/msg/p2p.js +8 -12
- package/dist/channels/aun.js +66 -48
- package/dist/channels/dingtalk.js +8 -2
- package/dist/channels/feishu.js +6 -1
- package/dist/channels/wecom.js +7 -3
- package/dist/cli/agent-create.js +23 -4
- package/dist/cli/aid-bind-command.js +4 -1
- package/dist/cli/daemon-commands.js +242 -17
- package/dist/cli/index.js +25 -13
- package/dist/cli/memory-command.js +33 -14
- package/dist/cli/restart-monitor.js +35 -13
- package/dist/cli/task-context.js +1 -0
- package/dist/config/config-field-policy.js +1 -0
- package/dist/contracts/ecweb-service-protocol.js +44 -0
- package/dist/core/agent-runtime.js +6 -0
- package/dist/core/auth/agent-delegation.js +7 -4
- package/dist/core/auth/auth-gateway.js +3 -1
- package/dist/core/auth/authorization-audit.js +5 -1
- package/dist/core/auth/debug-context.js +5 -1
- package/dist/core/auth/operation-catalog.js +16 -0
- package/dist/core/command/command-handler.js +7 -1
- package/dist/core/command/menu-catalog.js +48 -12
- package/dist/core/command/menu-handler.js +72 -4
- package/dist/core/command/menu-protocol.js +20 -1
- package/dist/core/command/slash-gate.js +17 -3
- package/dist/core/command/slash-handler.js +90 -7
- package/dist/core/event-catalog.js +2 -0
- package/dist/core/interaction-router.js +2 -1
- package/dist/core/ipc-registration.js +26 -7
- package/dist/core/memory/batch-meta.js +2 -0
- package/dist/core/memory/consolidation-context.js +42 -51
- package/dist/core/memory/consolidation-ops-records.js +39 -7
- package/dist/core/memory/consolidation-trigger.js +71 -64
- package/dist/core/memory/consolidation.js +226 -20
- package/dist/core/memory/memory-writer.js +160 -34
- package/dist/core/memory/relation-idle-monitor.js +287 -0
- package/dist/core/memory/relation-sweep.js +206 -0
- package/dist/core/message/aun-ipc-sender.js +5 -0
- package/dist/core/message/message-bridge.js +64 -55
- package/dist/core/message/message-log.js +17 -0
- package/dist/core/message/message-queue.js +6 -0
- package/dist/core/message/outbound.js +70 -0
- package/dist/core/message/peer-mode.js +7 -2
- package/dist/core/message/response-engine.js +341 -73
- package/dist/core/ownership/owner-resolver.js +384 -178
- package/dist/core/permission/ec-command-parser.js +19 -3
- package/dist/core/permission/tool-policy.js +7 -7
- package/dist/core/role/runtime-policy.js +64 -0
- package/dist/core/session/session-manager.js +25 -5
- package/dist/core/system-session/memory.js +3 -18
- package/dist/ecweb/basic-settings-service.js +709 -0
- package/dist/ecweb/providers-service.js +172 -0
- package/dist/index.js +92 -4
- package/dist/ipc.js +78 -11
- package/dist/stats/db.js +33 -0
- package/dist/stats/index.js +2 -2
- package/dist/stats/query.js +246 -0
- package/dist/stats/writer.js +41 -1
- package/dist/trigger/application-service.js +12 -0
- package/dist/trigger/authorization.js +2 -2
- package/dist/trigger/history.js +36 -0
- package/dist/trigger/scheduler.js +3 -0
- package/dist/utils/ecweb-supervisor.js +31 -10
- package/dist/utils/error-utils.js +8 -4
- package/dist/utils/evolcore-version.js +3 -2
- package/dist/utils/host-info.js +52 -0
- package/ecagent/dist/harness/agent-harness.js +4 -1
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/agent.md +1 -1
- package/kits/docs/evolcore/aid.md +29 -12
- package/kits/docs/evolcore/config.md +9 -7
- package/kits/docs/evolcore/memory-architecture.md +3 -4
- package/kits/docs/evolcore/memory-consolidation.md +28 -7
- package/kits/docs/evolcore/memory-tags.md +1 -1
- package/kits/docs/evolcore/memory.md +1 -1
- package/kits/docs/evolcore/menu-client.md +1 -1
- package/kits/docs/evolcore/menu-protocol.md +6 -0
- package/kits/docs/evolcore/msg.md +1 -1
- package/kits/eck_manifest.json +3 -3
- package/kits/rules/04-relation.md +3 -2
- package/kits/schemas/agent-config.schema.13.json +5 -1
- package/kits/templates/memory-consolidation/generalize.md +32 -4
- package/kits/templates/memory-consolidation/notes.md +2 -2
- package/kits/templates/memory-consolidation/promote.md +10 -2
- package/kits/templates/system-fragments/baseagent.md +5 -0
- package/kits/templates/system-fragments/memory.md +12 -3
- package/kits/templates/system-fragments/session.md +1 -1
- package/kits/{rules/07-memory-and-thread.md → templates/system-fragments/thread.md} +17 -80
- package/package.json +3 -3
|
@@ -47,7 +47,7 @@ export { buildEnvelope, sendInteractionPayload } from './message-utils.js';
|
|
|
47
47
|
import { constrainResolvedModelForRole, isModelAllowedForRoleBaseagent } from '../model/model-permission.js';
|
|
48
48
|
import { eligibleFallbackModels } from '../model/model-fallback.js';
|
|
49
49
|
import { RetryScheduler } from './retry-scheduler.js';
|
|
50
|
-
import { constrainRuntimePermissionMode, resolveRuntimeStringField } from '../role/runtime-policy.js';
|
|
50
|
+
import { assertSessionFullAccessAuthorization, constrainRuntimePermissionMode, resolveRuntimeStringField } from '../role/runtime-policy.js';
|
|
51
51
|
import { resolveEffective, resolveEffectiveFieldWithSource } from '../../config/config-manager.js';
|
|
52
52
|
import { resolveAgentLifecycle } from '../../config/lifecycle.js';
|
|
53
53
|
import { authorizationConfigRevision, checkRoleAccess, getFirstStaticAgentOwner, listStaticAgentAdmins, listStaticAgentOwners, resolvePeerRoleDetail, roleToSessionIdentity, } from '../../config/peer-role-resolver.js';
|
|
@@ -84,6 +84,7 @@ export class AsyncQuestionService {
|
|
|
84
84
|
deps;
|
|
85
85
|
registered = new Map();
|
|
86
86
|
deliveries = new Map();
|
|
87
|
+
blockingWaiters = new Map();
|
|
87
88
|
constructor(deps) {
|
|
88
89
|
this.deps = deps;
|
|
89
90
|
}
|
|
@@ -115,10 +116,11 @@ export class AsyncQuestionService {
|
|
|
115
116
|
const value = JSON.parse(fs.readFileSync(this.file(selfAid, id), 'utf8'));
|
|
116
117
|
if (value.version !== 1 || value.selfAid !== selfAid || value.id !== id
|
|
117
118
|
|| !value.sessionId || !value.providerThreadId || !value.itemId
|
|
118
|
-
|| !value.origin?.peerId || !value.channelKey
|
|
119
|
+
|| !value.origin?.peerId || !value.channelKey
|
|
120
|
+
|| (value.expiresAt !== undefined && !Number.isFinite(value.expiresAt)))
|
|
119
121
|
return undefined;
|
|
120
122
|
parseAsyncUserQuestions([value.question]);
|
|
121
|
-
return value;
|
|
123
|
+
return { ...value, blocking: value.blocking === true };
|
|
122
124
|
}
|
|
123
125
|
catch (error) {
|
|
124
126
|
if (error.code !== 'ENOENT')
|
|
@@ -126,6 +128,25 @@ export class AsyncQuestionService {
|
|
|
126
128
|
return undefined;
|
|
127
129
|
}
|
|
128
130
|
}
|
|
131
|
+
waitForBlocking(id) {
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
this.blockingWaiters.set(id, { resolve, reject });
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
resolveBlocking(id) {
|
|
137
|
+
const waiter = this.blockingWaiters.get(id);
|
|
138
|
+
if (!waiter)
|
|
139
|
+
return;
|
|
140
|
+
this.blockingWaiters.delete(id);
|
|
141
|
+
waiter.resolve();
|
|
142
|
+
}
|
|
143
|
+
rejectBlocking(id, reason) {
|
|
144
|
+
const waiter = this.blockingWaiters.get(id);
|
|
145
|
+
if (!waiter)
|
|
146
|
+
return;
|
|
147
|
+
this.blockingWaiters.delete(id);
|
|
148
|
+
waiter.reject(new Error(`用户交互已结束(${reason})`));
|
|
149
|
+
}
|
|
129
150
|
async present(session, request, message, adapter, envelope) {
|
|
130
151
|
if (!session.selfAID || !message.peerId)
|
|
131
152
|
throw new Error('Async question requires an authenticated route');
|
|
@@ -158,13 +179,19 @@ export class AsyncQuestionService {
|
|
|
158
179
|
threadId: sessionTopicThreadId(session), selfAID: session.selfAID,
|
|
159
180
|
},
|
|
160
181
|
envelope: { ...envelope, sessionId: session.id, replyContext, delivery: replyContext.delivery },
|
|
161
|
-
channelKey: adapter.channelKey,
|
|
182
|
+
channelKey: adapter.channelKey,
|
|
183
|
+
createdAt: Date.now(),
|
|
184
|
+
...(question.blocking === true ? {} : { expiresAt: Date.now() + QUESTION_TTL_MS }),
|
|
185
|
+
blocking: question.blocking !== false,
|
|
162
186
|
status: 'pending',
|
|
163
187
|
};
|
|
164
188
|
if (!existing)
|
|
165
189
|
this.save(record);
|
|
166
190
|
records.push(record);
|
|
167
191
|
}
|
|
192
|
+
const blockingWaits = records
|
|
193
|
+
.filter(record => record.blocking)
|
|
194
|
+
.map(record => ({ record, promise: this.waitForBlocking(record.id) }));
|
|
168
195
|
// Persist the complete batch before dispatch; one failed card must not lose the other questions.
|
|
169
196
|
const errors = [];
|
|
170
197
|
for (const record of records) {
|
|
@@ -173,10 +200,14 @@ export class AsyncQuestionService {
|
|
|
173
200
|
}
|
|
174
201
|
catch (error) {
|
|
175
202
|
errors.push(error);
|
|
203
|
+
this.rejectBlocking(record.id, '卡片投递失败');
|
|
176
204
|
}
|
|
177
205
|
}
|
|
178
|
-
if (errors.length)
|
|
206
|
+
if (errors.length) {
|
|
207
|
+
await Promise.allSettled(blockingWaits.map(wait => wait.promise));
|
|
179
208
|
throw new AggregateError(errors, 'Async question delivery failed; pending questions retained');
|
|
209
|
+
}
|
|
210
|
+
await Promise.all(blockingWaits.map(wait => wait.promise));
|
|
180
211
|
}
|
|
181
212
|
async restore(adapter, selfAid) {
|
|
182
213
|
let names;
|
|
@@ -197,7 +228,7 @@ export class AsyncQuestionService {
|
|
|
197
228
|
continue;
|
|
198
229
|
if (this.registered.get(record.id) === adapter)
|
|
199
230
|
continue;
|
|
200
|
-
if (record.expiresAt <= Date.now()) {
|
|
231
|
+
if (record.expiresAt !== undefined && record.expiresAt <= Date.now()) {
|
|
201
232
|
fs.unlinkSync(this.file(selfAid, record.id));
|
|
202
233
|
continue;
|
|
203
234
|
}
|
|
@@ -257,18 +288,24 @@ export class AsyncQuestionService {
|
|
|
257
288
|
}
|
|
258
289
|
}
|
|
259
290
|
async deliverOnce(record, adapter) {
|
|
260
|
-
if (record.expiresAt <= Date.now() || !(await this.validSession(record))) {
|
|
291
|
+
if ((record.expiresAt !== undefined && record.expiresAt <= Date.now()) || !(await this.validSession(record))) {
|
|
261
292
|
record.status = 'cancelled';
|
|
262
293
|
record.reason = 'expired_or_binding_changed';
|
|
263
294
|
this.save(record);
|
|
295
|
+
this.rejectBlocking(record.id, record.reason);
|
|
264
296
|
return;
|
|
265
297
|
}
|
|
266
298
|
const options = record.question.options ?? [];
|
|
267
299
|
const interaction = {
|
|
268
300
|
type: 'interaction', id: record.id, sessionId: record.sessionId, channelId: record.origin.channelId,
|
|
269
|
-
initiatorId: record.origin.peerId,
|
|
301
|
+
initiatorId: record.origin.peerId,
|
|
302
|
+
...(record.blocking ? { blocking: true } : { blocking: false }),
|
|
303
|
+
...(record.expiresAt !== undefined ? { expiresAt: record.expiresAt } : {}),
|
|
270
304
|
kind: {
|
|
271
305
|
kind: 'action', title: '请补充信息', body: record.question.title, bodyFormat: 'plain',
|
|
306
|
+
...(record.question.recommendedOption
|
|
307
|
+
? { recommendedOption: `opt-${options.indexOf(record.question.recommendedOption)}` }
|
|
308
|
+
: {}),
|
|
272
309
|
buttons: options.map((label, index) => ({ key: `opt-${index}`, label })),
|
|
273
310
|
allowCustomInput: true,
|
|
274
311
|
},
|
|
@@ -282,13 +319,24 @@ export class AsyncQuestionService {
|
|
|
282
319
|
this.registered.set(record.id, adapter);
|
|
283
320
|
this.deps.router.register(record.id, record.sessionId, async (action, values) => {
|
|
284
321
|
const current = this.read(record.selfAid, record.id);
|
|
285
|
-
if (!current || current.status !== 'pending'
|
|
322
|
+
if (!current || current.status !== 'pending'
|
|
323
|
+
|| (current.expiresAt !== undefined && current.expiresAt <= Date.now())) {
|
|
286
324
|
await this.deps.router.cancel(record.id, 'expired_or_resolved');
|
|
287
325
|
return false;
|
|
288
326
|
}
|
|
289
327
|
const answer = this.answerText(current.question, action, values);
|
|
290
|
-
if (!answer)
|
|
328
|
+
if (!answer) {
|
|
329
|
+
if (action === 'cancel' || action === 'dismiss' || values?.behavior === 'dismiss') {
|
|
330
|
+
current.status = 'cancelled';
|
|
331
|
+
current.reason = 'user_cancelled';
|
|
332
|
+
this.save(current);
|
|
333
|
+
this.registered.delete(record.id);
|
|
334
|
+
this.rejectBlocking(record.id, current.reason);
|
|
335
|
+
await adapter.invalidateInteraction?.(record.id, current.reason);
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
291
338
|
return false;
|
|
339
|
+
}
|
|
292
340
|
const session = await this.validSession(current);
|
|
293
341
|
if (!session) {
|
|
294
342
|
await this.deps.router.cancel(record.id, 'binding_or_authorization_changed');
|
|
@@ -305,6 +353,13 @@ export class AsyncQuestionService {
|
|
|
305
353
|
current.status = 'answered';
|
|
306
354
|
this.save(current);
|
|
307
355
|
this.registered.delete(record.id);
|
|
356
|
+
this.resolveBlocking(record.id);
|
|
357
|
+
try {
|
|
358
|
+
await adapter.invalidateInteraction?.(record.id, 'answered');
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
logger.debug(`[AsyncQuestions] card invalidation after answer failed: request=${record.id} error=${String(error)}`);
|
|
362
|
+
}
|
|
308
363
|
logger.info(`[AsyncQuestions] answer accepted: session=${record.sessionId} request=${record.id}`);
|
|
309
364
|
return true;
|
|
310
365
|
}
|
|
@@ -312,12 +367,16 @@ export class AsyncQuestionService {
|
|
|
312
367
|
current.status = 'uncertain';
|
|
313
368
|
current.reason = 'submission_failed';
|
|
314
369
|
this.save(current);
|
|
370
|
+
this.rejectBlocking(record.id, '答案提交失败');
|
|
315
371
|
logger.warn(`[AsyncQuestions] answer submission failed: request=${record.id} error=${String(error)}`);
|
|
316
372
|
throw error;
|
|
317
373
|
}
|
|
318
374
|
}, {
|
|
319
375
|
initiatorId: record.origin.peerId, initiatorChannelKey: adapter.channelKey,
|
|
320
|
-
blocking:
|
|
376
|
+
blocking: record.blocking,
|
|
377
|
+
retainOnRejected: true,
|
|
378
|
+
fallbackCommand: 'ask',
|
|
379
|
+
...(record.expiresAt !== undefined ? { timeoutMs: Math.max(1, record.expiresAt - Date.now()) } : {}),
|
|
321
380
|
onCancel: async (reason) => {
|
|
322
381
|
this.registered.delete(record.id);
|
|
323
382
|
if (reason === 'daemon_restart')
|
|
@@ -328,6 +387,7 @@ export class AsyncQuestionService {
|
|
|
328
387
|
current.reason = reason;
|
|
329
388
|
this.save(current);
|
|
330
389
|
}
|
|
390
|
+
this.rejectBlocking(record.id, reason);
|
|
331
391
|
await adapter.invalidateInteraction?.(record.id, reason);
|
|
332
392
|
},
|
|
333
393
|
});
|
|
@@ -432,6 +492,7 @@ const TOOL_INPUT_DIAGNOSTIC_MAX_LENGTH = 96_000;
|
|
|
432
492
|
const TOOL_INPUT_DIAGNOSTIC_MAX_FINDINGS = 3;
|
|
433
493
|
const TOOL_INPUT_DIAGNOSTIC_MAX_SHELL_DEPTH = 4;
|
|
434
494
|
const TOOL_INPUT_DIAGNOSTIC_TIMEOUT_MS = 1_000;
|
|
495
|
+
const PROACTIVE_CIRCUIT_BREAK_DRAIN_TIMEOUT_MS = 2_000;
|
|
435
496
|
function isToolResultFailure(event) {
|
|
436
497
|
return event.isError === true
|
|
437
498
|
|| event.executionState === 'failed'
|
|
@@ -871,12 +932,13 @@ class RetryAttemptTimeoutError extends Error {
|
|
|
871
932
|
function createRetryAttemptTimeout(timeoutMs) {
|
|
872
933
|
let timer;
|
|
873
934
|
let settled = false;
|
|
935
|
+
let paused = false;
|
|
874
936
|
let rejectTimeout;
|
|
875
937
|
const promise = new Promise((_, reject) => {
|
|
876
938
|
rejectTimeout = reject;
|
|
877
939
|
});
|
|
878
940
|
const arm = () => {
|
|
879
|
-
if (settled)
|
|
941
|
+
if (settled || paused)
|
|
880
942
|
return;
|
|
881
943
|
if (timer)
|
|
882
944
|
clearTimeout(timer);
|
|
@@ -891,8 +953,23 @@ function createRetryAttemptTimeout(timeoutMs) {
|
|
|
891
953
|
return {
|
|
892
954
|
promise,
|
|
893
955
|
reset: arm,
|
|
956
|
+
pause: () => {
|
|
957
|
+
if (settled || paused)
|
|
958
|
+
return;
|
|
959
|
+
paused = true;
|
|
960
|
+
if (timer)
|
|
961
|
+
clearTimeout(timer);
|
|
962
|
+
timer = undefined;
|
|
963
|
+
},
|
|
964
|
+
resume: () => {
|
|
965
|
+
if (settled || !paused)
|
|
966
|
+
return;
|
|
967
|
+
paused = false;
|
|
968
|
+
arm();
|
|
969
|
+
},
|
|
894
970
|
clear: () => {
|
|
895
971
|
settled = true;
|
|
972
|
+
paused = false;
|
|
896
973
|
if (timer)
|
|
897
974
|
clearTimeout(timer);
|
|
898
975
|
timer = undefined;
|
|
@@ -979,7 +1056,8 @@ function isNonErrorInterruptReason(reason) {
|
|
|
979
1056
|
return reason === 'new_message'
|
|
980
1057
|
|| reason === 'stop'
|
|
981
1058
|
|| reason === 'recalled'
|
|
982
|
-
|| reason === 'daemon_restart'
|
|
1059
|
+
|| reason === 'daemon_restart'
|
|
1060
|
+
|| reason === 'proactive_circuit_break';
|
|
983
1061
|
}
|
|
984
1062
|
function isExpectedTerminalInterrupt(reason) {
|
|
985
1063
|
return isAbortTerminalReason(reason);
|
|
@@ -1109,7 +1187,7 @@ function getStreamRawErrorText(result, fallback) {
|
|
|
1109
1187
|
fullTextLooksLikeDiagnostic ? fullText : undefined,
|
|
1110
1188
|
fallbackLooksLikeDiagnostic ? fallback : undefined,
|
|
1111
1189
|
];
|
|
1112
|
-
return candidates.find((part) => typeof part === 'string' && part.trim().length > 0)
|
|
1190
|
+
return candidates.find((part) => typeof part === 'string' && part.trim().length > 0);
|
|
1113
1191
|
}
|
|
1114
1192
|
function getStreamErrorMessage(result, includeEmoji = false) {
|
|
1115
1193
|
const raw = getStreamErrorText(result);
|
|
@@ -1366,7 +1444,13 @@ export class ResponseEngine {
|
|
|
1366
1444
|
agentName: error.evolagentName,
|
|
1367
1445
|
chatmode: this.resolveEffectiveChatmodeForSession(session, channelKey, channelId),
|
|
1368
1446
|
replyContext: sessionReplyContext,
|
|
1369
|
-
}), {
|
|
1447
|
+
}), {
|
|
1448
|
+
kind: 'system.error',
|
|
1449
|
+
text,
|
|
1450
|
+
raw: error.message,
|
|
1451
|
+
subtype: 'baseagent_unavailable',
|
|
1452
|
+
recoverable: true,
|
|
1453
|
+
});
|
|
1370
1454
|
}
|
|
1371
1455
|
resolveEffectiveChatmodeForSession(session, channelKey, channelId) {
|
|
1372
1456
|
const peerType = session.metadata?.peerType;
|
|
@@ -2333,6 +2417,22 @@ export class ResponseEngine {
|
|
|
2333
2417
|
&& !!message.bgSessionMeta?.runId;
|
|
2334
2418
|
}
|
|
2335
2419
|
bgSessionTerminalMessages = new WeakSet();
|
|
2420
|
+
/**
|
|
2421
|
+
* 后台会话本次运行的模型:解析出请求模型时写 requestedModel,runner 回报结果后写
|
|
2422
|
+
* actualModel,三个 bg-session 终态事件发布时读出。按入站 message 归属,随消息回收,
|
|
2423
|
+
* 不落盘、不跨会话;只服务终态事件,其它路径不要读。
|
|
2424
|
+
*/
|
|
2425
|
+
bgSessionModels = new WeakMap();
|
|
2426
|
+
recordBgSessionModels(message, models) {
|
|
2427
|
+
if (message.source !== 'bg-session')
|
|
2428
|
+
return;
|
|
2429
|
+
const current = this.bgSessionModels.get(message) ?? {};
|
|
2430
|
+
this.bgSessionModels.set(message, {
|
|
2431
|
+
...current,
|
|
2432
|
+
...(models.requestedModel ? { requestedModel: models.requestedModel } : {}),
|
|
2433
|
+
...(models.actualModel ? { actualModel: models.actualModel } : {}),
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2336
2436
|
claimBgSessionTerminal(message) {
|
|
2337
2437
|
const meta = message.bgSessionMeta;
|
|
2338
2438
|
if (message.source !== 'bg-session' || !meta?.runId || this.bgSessionTerminalMessages.has(message)) {
|
|
@@ -2352,6 +2452,7 @@ export class ResponseEngine {
|
|
|
2352
2452
|
runId,
|
|
2353
2453
|
sessionId: meta.boundSessionId ?? '',
|
|
2354
2454
|
messageId: opts.messageId ?? message.messageId ?? runId,
|
|
2455
|
+
...this.bgSessionModels.get(message),
|
|
2355
2456
|
error,
|
|
2356
2457
|
targetChannel: message.channel,
|
|
2357
2458
|
targetChannelId: message.channelId,
|
|
@@ -2369,6 +2470,7 @@ export class ResponseEngine {
|
|
|
2369
2470
|
runId,
|
|
2370
2471
|
sessionId: meta.boundSessionId ?? '',
|
|
2371
2472
|
messageId: message.messageId ?? runId,
|
|
2473
|
+
...this.bgSessionModels.get(message),
|
|
2372
2474
|
reason,
|
|
2373
2475
|
targetChannel: message.channel,
|
|
2374
2476
|
targetChannelId: message.channelId,
|
|
@@ -2387,6 +2489,7 @@ export class ResponseEngine {
|
|
|
2387
2489
|
sessionId: meta.boundSessionId ?? '',
|
|
2388
2490
|
messageId,
|
|
2389
2491
|
durationMs,
|
|
2492
|
+
...this.bgSessionModels.get(message),
|
|
2390
2493
|
targetChannel: message.channel,
|
|
2391
2494
|
targetChannelId: message.channelId,
|
|
2392
2495
|
causation: causation ?? message.causation,
|
|
@@ -2429,7 +2532,7 @@ export class ResponseEngine {
|
|
|
2429
2532
|
* them immediately before every Runner invocation, including retries.
|
|
2430
2533
|
*/
|
|
2431
2534
|
async assertCurrentFullAccessExecutionAuthorization(authorization, agent) {
|
|
2432
|
-
await refreshOwnershipForAuthorization(authorization.agentAid);
|
|
2535
|
+
await refreshOwnershipForAuthorization(authorization.agentAid, { allowTransient: true });
|
|
2433
2536
|
if (authorization.permissionMode !== 'fullaccess'
|
|
2434
2537
|
|| authorization.processRole !== 'fullaccess-run'
|
|
2435
2538
|
|| authorization.dataScope !== 'daemon'
|
|
@@ -2444,6 +2547,12 @@ export class ResponseEngine {
|
|
|
2444
2547
|
if (authorization.source === 'agent-policy') {
|
|
2445
2548
|
assertAgentPolicyAuthorization(authorization);
|
|
2446
2549
|
}
|
|
2550
|
+
else if (authorization.source === 'session-policy') {
|
|
2551
|
+
const currentSession = this.sessionManager.getSessionById
|
|
2552
|
+
? await this.sessionManager.getSessionById(authorization.sessionId ?? '')
|
|
2553
|
+
: undefined;
|
|
2554
|
+
assertSessionFullAccessAuthorization(authorization, currentSession);
|
|
2555
|
+
}
|
|
2447
2556
|
else if (authorization.source !== 'fullaccess-command' && authorization.source !== 'trigger') {
|
|
2448
2557
|
throw new Error('fullaccess execution authorization has an unsupported source');
|
|
2449
2558
|
}
|
|
@@ -2686,7 +2795,7 @@ export class ResponseEngine {
|
|
|
2686
2795
|
* 处理消息(主入口)
|
|
2687
2796
|
*/
|
|
2688
2797
|
async processMessage(message) {
|
|
2689
|
-
const fallbackFullAccessAudit = this.resolveFullAccessAuditCandidate(message);
|
|
2798
|
+
const fallbackFullAccessAudit = await this.resolveFullAccessAuditCandidate(message);
|
|
2690
2799
|
let outerFailureReason;
|
|
2691
2800
|
try {
|
|
2692
2801
|
await this.processMessageWithMonitors(message);
|
|
@@ -2707,7 +2816,9 @@ export class ResponseEngine {
|
|
|
2707
2816
|
event: fallbackFullAccessAudit.authorization.source === 'trigger'
|
|
2708
2817
|
? 'fullaccess.trigger.execution.ended'
|
|
2709
2818
|
: fallbackFullAccessAudit.authorization.source === 'agent-policy'
|
|
2710
|
-
? 'fullaccess.agent-policy.execution.ended'
|
|
2819
|
+
? 'fullaccess.agent-policy.execution.ended'
|
|
2820
|
+
: fallbackFullAccessAudit.authorization.source === 'session-policy'
|
|
2821
|
+
? 'fullaccess.session-policy.execution.ended' : 'fullaccess.execution.ended',
|
|
2711
2822
|
source: fallbackFullAccessAudit.authorization.source,
|
|
2712
2823
|
actorId: fallbackFullAccessAudit.authorization.authorizedBy,
|
|
2713
2824
|
processRole: fallbackFullAccessAudit.processRole,
|
|
@@ -2774,7 +2885,10 @@ export class ResponseEngine {
|
|
|
2774
2885
|
const accessAdapter = accessChannelInfo?.adapter;
|
|
2775
2886
|
const accessAdapterSelfAid = typeof accessAdapter?._selfAid === 'function' ? accessAdapter._selfAid() : undefined;
|
|
2776
2887
|
const selfAidForAccess = accessAdapterSelfAid || message.selfAID || session.selfAID || undefined;
|
|
2777
|
-
await refreshOwnershipForAuthorization(selfAidForAccess, {
|
|
2888
|
+
await refreshOwnershipForAuthorization(selfAidForAccess, {
|
|
2889
|
+
maxAgeMs: OWNERSHIP_CACHE_MAX_AGE_MS,
|
|
2890
|
+
allowTransient: true,
|
|
2891
|
+
});
|
|
2778
2892
|
if (message.handoffDelivery?.direction === 'target'
|
|
2779
2893
|
&& this.handoffRuntime
|
|
2780
2894
|
&& selfAidForAccess) {
|
|
@@ -2823,9 +2937,10 @@ export class ResponseEngine {
|
|
|
2823
2937
|
// ── 角色访问控制检查:读取该用户角色的 allowAccess 配置,false 则拦截并回复权限不足 ──
|
|
2824
2938
|
const userRole = session.identity?.role || 'none';
|
|
2825
2939
|
const isInternalHandoff = message.source === 'handoff';
|
|
2826
|
-
const fullAccessAuditCandidate = this.resolveFullAccessAuditCandidate(message);
|
|
2940
|
+
const fullAccessAuditCandidate = await this.resolveFullAccessAuditCandidate(message);
|
|
2827
2941
|
const isCurrentFullAccessCommand = (fullAccessAuditCandidate?.authorization.source === 'fullaccess-command'
|
|
2828
|
-
|| fullAccessAuditCandidate?.authorization.source === 'agent-policy'
|
|
2942
|
+
|| fullAccessAuditCandidate?.authorization.source === 'agent-policy'
|
|
2943
|
+
|| fullAccessAuditCandidate?.authorization.source === 'session-policy')
|
|
2829
2944
|
&& fullAccessAuditCandidate.processRole === 'fullaccess-run';
|
|
2830
2945
|
const isCurrentFullAccessTrigger = fullAccessAuditCandidate?.authorization.source === 'trigger'
|
|
2831
2946
|
&& fullAccessAuditCandidate.processRole === 'fullaccess-run';
|
|
@@ -2976,6 +3091,35 @@ export class ResponseEngine {
|
|
|
2976
3091
|
activityRecorder?.call(channelInfo.adapter, triggerAttemptId);
|
|
2977
3092
|
}
|
|
2978
3093
|
};
|
|
3094
|
+
let totalExecutionDeadline;
|
|
3095
|
+
let totalExecutionPausedRemaining;
|
|
3096
|
+
const pauseTotalExecutionTimer = () => {
|
|
3097
|
+
if (totalExecutionTimer === undefined || totalExecutionDeadline === undefined)
|
|
3098
|
+
return;
|
|
3099
|
+
totalExecutionPausedRemaining = Math.max(0, totalExecutionDeadline - Date.now());
|
|
3100
|
+
clearTimeout(totalExecutionTimer);
|
|
3101
|
+
totalExecutionTimer = undefined;
|
|
3102
|
+
};
|
|
3103
|
+
const resumeTotalExecutionTimer = () => {
|
|
3104
|
+
if (totalExecutionTimer !== undefined || totalExecutionPausedRemaining === undefined)
|
|
3105
|
+
return;
|
|
3106
|
+
const remaining = totalExecutionPausedRemaining;
|
|
3107
|
+
totalExecutionPausedRemaining = undefined;
|
|
3108
|
+
totalExecutionDeadline = Date.now() + remaining;
|
|
3109
|
+
totalExecutionTimer = setTimeout(() => {
|
|
3110
|
+
logger.warn(`[ResponseEngine] Total execution timeout after ${totalExecutionMs}ms, stream: ${streamKey}`);
|
|
3111
|
+
rejectAfterInterruptBarrier(new Error('TOTAL_EXECUTION_TIMEOUT'));
|
|
3112
|
+
}, remaining);
|
|
3113
|
+
totalExecutionTimer.unref?.();
|
|
3114
|
+
};
|
|
3115
|
+
timeoutControl.pauseForInteraction = () => {
|
|
3116
|
+
monitor?.pause();
|
|
3117
|
+
pauseTotalExecutionTimer();
|
|
3118
|
+
};
|
|
3119
|
+
timeoutControl.resumeForInteraction = () => {
|
|
3120
|
+
monitor?.resume();
|
|
3121
|
+
resumeTotalExecutionTimer();
|
|
3122
|
+
};
|
|
2979
3123
|
// Cache background status to avoid async call inside setInterval
|
|
2980
3124
|
const isBackground = this.isBackgroundSession(session, message.channel, message.channelId);
|
|
2981
3125
|
const timeoutPromise = new Promise((_, reject) => {
|
|
@@ -3028,6 +3172,7 @@ export class ResponseEngine {
|
|
|
3028
3172
|
const totalExecutionPromise = totalExecutionMs === undefined
|
|
3029
3173
|
? undefined
|
|
3030
3174
|
: new Promise((_, reject) => {
|
|
3175
|
+
totalExecutionDeadline = Date.now() + totalExecutionMs;
|
|
3031
3176
|
totalExecutionTimer = setTimeout(() => {
|
|
3032
3177
|
logger.warn(`[ResponseEngine] Total execution timeout after ${totalExecutionMs}ms, stream: ${streamKey}`);
|
|
3033
3178
|
rejectAfterInterruptBarrier(new Error('TOTAL_EXECUTION_TIMEOUT'));
|
|
@@ -3095,12 +3240,14 @@ export class ResponseEngine {
|
|
|
3095
3240
|
this.activeTimeoutHandlers.delete(session.id);
|
|
3096
3241
|
}
|
|
3097
3242
|
}
|
|
3098
|
-
resolveFullAccessAuditCandidate(message) {
|
|
3243
|
+
async resolveFullAccessAuditCandidate(message) {
|
|
3099
3244
|
const commandAuthorization = message.executionPermissionOverride;
|
|
3100
3245
|
if (commandAuthorization?.permissionMode === 'fullaccess'
|
|
3101
3246
|
&& commandAuthorization.processRole === 'fullaccess-run'
|
|
3102
3247
|
&& commandAuthorization.dataScope === 'daemon'
|
|
3103
|
-
&& (commandAuthorization.source === 'fullaccess-command'
|
|
3248
|
+
&& (commandAuthorization.source === 'fullaccess-command'
|
|
3249
|
+
|| commandAuthorization.source === 'agent-policy'
|
|
3250
|
+
|| commandAuthorization.source === 'session-policy')
|
|
3104
3251
|
&& typeof commandAuthorization.authorizedBy === 'string'
|
|
3105
3252
|
&& commandAuthorization.authorizedBy.length > 0) {
|
|
3106
3253
|
let currentOwner = false;
|
|
@@ -3108,6 +3255,12 @@ export class ResponseEngine {
|
|
|
3108
3255
|
currentOwner = new Set(getDaemonOwners()).has(commandAuthorization.authorizedBy);
|
|
3109
3256
|
if (commandAuthorization.source === 'agent-policy')
|
|
3110
3257
|
assertAgentPolicyAuthorization(commandAuthorization);
|
|
3258
|
+
if (commandAuthorization.source === 'session-policy') {
|
|
3259
|
+
const session = this.sessionManager.getSessionById
|
|
3260
|
+
? await this.sessionManager.getSessionById(commandAuthorization.sessionId ?? '')
|
|
3261
|
+
: undefined;
|
|
3262
|
+
assertSessionFullAccessAuthorization(commandAuthorization, session);
|
|
3263
|
+
}
|
|
3111
3264
|
}
|
|
3112
3265
|
catch {
|
|
3113
3266
|
currentOwner = false;
|
|
@@ -3575,17 +3728,23 @@ export class ResponseEngine {
|
|
|
3575
3728
|
if (!timeoutControl.claimTerminal())
|
|
3576
3729
|
return;
|
|
3577
3730
|
const recoverable = timeoutControl.recoverable === true;
|
|
3731
|
+
const rawErrorText = getErrorRawText(timeoutError);
|
|
3578
3732
|
const isTotalExecutionTimeout = timeoutError.message === 'TOTAL_EXECUTION_TIMEOUT';
|
|
3579
3733
|
const totalExecutionMs = timeoutError instanceof ExecutionTimeoutError ? timeoutError.timeoutMs : this.totalExecutionLimitMs();
|
|
3580
3734
|
const statusPayload = timeoutError instanceof OrphanedTargetSessionError ? {
|
|
3581
3735
|
kind: 'status.error',
|
|
3582
|
-
metadata: {
|
|
3736
|
+
metadata: {
|
|
3737
|
+
errorType: timeoutError.reason,
|
|
3738
|
+
message: '任务执行状态丢失,已结束本次执行。',
|
|
3739
|
+
...(rawErrorText ? { raw: rawErrorText } : {}),
|
|
3740
|
+
},
|
|
3583
3741
|
} : {
|
|
3584
3742
|
kind: 'status.timeout',
|
|
3585
3743
|
metadata: isTotalExecutionTimeout
|
|
3586
3744
|
? { ...(totalExecutionMs === undefined ? {} : { totalExecutionMs }),
|
|
3587
|
-
...(timeoutError instanceof ExecutionTimeoutError ? { reason: timeoutError.reason } : {})
|
|
3588
|
-
|
|
3745
|
+
...(timeoutError instanceof ExecutionTimeoutError ? { reason: timeoutError.reason } : {}),
|
|
3746
|
+
...(rawErrorText ? { raw: rawErrorText } : {}) }
|
|
3747
|
+
: { idleSec: getLastIdleSec?.() || undefined, ...(rawErrorText ? { raw: rawErrorText } : {}) },
|
|
3589
3748
|
};
|
|
3590
3749
|
const idleSec = getLastIdleSec?.() || 0;
|
|
3591
3750
|
const userMessage = timeoutError instanceof OrphanedTargetSessionError
|
|
@@ -3612,7 +3771,7 @@ export class ResponseEngine {
|
|
|
3612
3771
|
kind: 'result.error',
|
|
3613
3772
|
text: userMessage,
|
|
3614
3773
|
recoverable,
|
|
3615
|
-
...(
|
|
3774
|
+
...(rawErrorText ? { raw: rawErrorText } : {}),
|
|
3616
3775
|
subtype: normalizeCoreErrorSubtype(timeoutError, undefined, isTotalExecutionTimeout ? 'timeout' : 'timeout'),
|
|
3617
3776
|
reason: isExecutionFailure(timeoutError)
|
|
3618
3777
|
? normalizeCoreErrorReason(timeoutError.reason)
|
|
@@ -3674,6 +3833,46 @@ export class ResponseEngine {
|
|
|
3674
3833
|
let fullAccessExecutionFailed = false;
|
|
3675
3834
|
let fullAccessExecutionFailureReason;
|
|
3676
3835
|
let activePermissionContext;
|
|
3836
|
+
let proactiveCircuitBreakRequested = false;
|
|
3837
|
+
let proactiveCircuitBreakInterruptPromise;
|
|
3838
|
+
let proactiveCircuitBreakTimer;
|
|
3839
|
+
const interruptProactiveCircuitBreak = () => {
|
|
3840
|
+
if (proactiveCircuitBreakInterruptPromise)
|
|
3841
|
+
return proactiveCircuitBreakInterruptPromise;
|
|
3842
|
+
if (!proactiveCircuitBreakRequested
|
|
3843
|
+
|| this.interruptedSessions.get(session.id) !== 'proactive_circuit_break'
|
|
3844
|
+
|| (turnLease && !this.turnCoordinator.isCurrent(turnLease)))
|
|
3845
|
+
return Promise.resolve();
|
|
3846
|
+
this.interruptedSessions.set(session.id, 'proactive_circuit_break');
|
|
3847
|
+
if (proactiveCircuitBreakTimer) {
|
|
3848
|
+
clearTimeout(proactiveCircuitBreakTimer);
|
|
3849
|
+
proactiveCircuitBreakTimer = undefined;
|
|
3850
|
+
}
|
|
3851
|
+
const interruptPromise = (async () => {
|
|
3852
|
+
await agent.interrupt(session.id).catch(error => {
|
|
3853
|
+
logger.debug(`[ResponseEngine] circuit break interrupt failed: session=${session.id} error=${error instanceof Error ? error.message : String(error)}`);
|
|
3854
|
+
});
|
|
3855
|
+
})();
|
|
3856
|
+
proactiveCircuitBreakInterruptPromise = interruptPromise;
|
|
3857
|
+
return interruptPromise;
|
|
3858
|
+
};
|
|
3859
|
+
const requestProactiveCircuitBreak = () => {
|
|
3860
|
+
if (proactiveCircuitBreakRequested)
|
|
3861
|
+
return;
|
|
3862
|
+
proactiveCircuitBreakRequested = true;
|
|
3863
|
+
this.interruptedSessions.set(session.id, 'proactive_circuit_break');
|
|
3864
|
+
void adapter.send(withEnvelopeReplyContext(envelope, taskReplyContext()), {
|
|
3865
|
+
kind: 'result.text',
|
|
3866
|
+
text: '⚠️ 任务执行长时间不反馈,已被自动中断。请重新发言继续。',
|
|
3867
|
+
isFinal: true,
|
|
3868
|
+
}).catch(error => {
|
|
3869
|
+
logger.warn(`[ResponseEngine] circuit break channel send failed: session=${session.id} error=${error instanceof Error ? error.message : String(error)}`);
|
|
3870
|
+
});
|
|
3871
|
+
proactiveCircuitBreakTimer = setTimeout(() => {
|
|
3872
|
+
void interruptProactiveCircuitBreak();
|
|
3873
|
+
}, PROACTIVE_CIRCUIT_BREAK_DRAIN_TIMEOUT_MS);
|
|
3874
|
+
proactiveCircuitBreakTimer.unref?.();
|
|
3875
|
+
};
|
|
3677
3876
|
try {
|
|
3678
3877
|
const isBackground = this.isBackgroundSession(session, message.channel, message.channelId);
|
|
3679
3878
|
// 记录收到消息
|
|
@@ -4127,6 +4326,15 @@ export class ResponseEngine {
|
|
|
4127
4326
|
? systemSessionModule.runnerLaunch(normalizeBaseagent(agent.name).canonical, systemSessionContext).sandbox
|
|
4128
4327
|
: undefined;
|
|
4129
4328
|
const pureSessionPolicyHook = systemSessionMarker ? undefined : modePolicyHook;
|
|
4329
|
+
const purePreToolUsePolicyHook = pureSessionPolicyHook
|
|
4330
|
+
? (toolName, toolInput) => proactiveCircuitBreakRequested
|
|
4331
|
+
? {
|
|
4332
|
+
block: true,
|
|
4333
|
+
policyCode: 'proactive_circuit_break',
|
|
4334
|
+
reason: '任务已触发主动模式熔断,工具调用已停止',
|
|
4335
|
+
}
|
|
4336
|
+
: pureSessionPolicyHook(toolName, toolInput)
|
|
4337
|
+
: undefined;
|
|
4130
4338
|
// 路径授权(仅用户会话):身份就是角色名;运行时授予(ec ctl path grant)按 sessionId 再并一层。
|
|
4131
4339
|
const pathGrantIdentity = systemSessionMarker ? undefined : (peerRole || undefined);
|
|
4132
4340
|
const grantedReadRoots = (() => {
|
|
@@ -4168,7 +4376,8 @@ export class ResponseEngine {
|
|
|
4168
4376
|
permissionMode: effectivePermissionMode,
|
|
4169
4377
|
recordExecutionAnomaly,
|
|
4170
4378
|
pauseController: taskPauseController,
|
|
4171
|
-
preToolUsePolicyHook:
|
|
4379
|
+
preToolUsePolicyHook: purePreToolUsePolicyHook,
|
|
4380
|
+
onProactiveCircuitBreak: requestProactiveCircuitBreak,
|
|
4172
4381
|
systemSessionCheck,
|
|
4173
4382
|
...(systemSessionModule && systemSessionContext
|
|
4174
4383
|
? {
|
|
@@ -4278,6 +4487,13 @@ export class ResponseEngine {
|
|
|
4278
4487
|
if (!pluginHook)
|
|
4279
4488
|
return undefined;
|
|
4280
4489
|
return (toolName, toolInput, meta) => {
|
|
4490
|
+
if (proactiveCircuitBreakRequested) {
|
|
4491
|
+
return {
|
|
4492
|
+
block: true,
|
|
4493
|
+
policyCode: 'proactive_circuit_break',
|
|
4494
|
+
reason: '任务已触发主动模式熔断,工具调用已停止',
|
|
4495
|
+
};
|
|
4496
|
+
}
|
|
4281
4497
|
const stBefore = modeState.get('proactive');
|
|
4282
4498
|
const wasFirstPending = stBefore && !stBefore.firstToolDone;
|
|
4283
4499
|
const result = pluginHook(toolName, toolInput, meta);
|
|
@@ -4287,19 +4503,9 @@ export class ResponseEngine {
|
|
|
4287
4503
|
if (result?.block) {
|
|
4288
4504
|
// 拦截事件(首工具非表态 / 工具进展汇报未完成)
|
|
4289
4505
|
snapshot.set(session.id, taskId, { policyHook: { triggered: true, blocked: true, toolName } });
|
|
4290
|
-
//
|
|
4291
|
-
// F5修复:向渠道发一条人类可读的错误说明,避免静默中断。
|
|
4506
|
+
// 熔断后拒绝新的工具调用,当前调用收尾后再中断流。
|
|
4292
4507
|
if (result.policyCode === 'proactive_circuit_break') {
|
|
4293
|
-
|
|
4294
|
-
kind: 'result.text',
|
|
4295
|
-
text: '⚠️ 任务执行遇到循环障碍,已自动中断。请重新描述你的需求。',
|
|
4296
|
-
isFinal: true,
|
|
4297
|
-
}).catch(err => {
|
|
4298
|
-
logger.warn(`[ResponseEngine] circuit break channel send failed: session=${session.id} error=${err instanceof Error ? err.message : String(err)}`);
|
|
4299
|
-
});
|
|
4300
|
-
void agent.interrupt(session.id).catch(error => {
|
|
4301
|
-
logger.debug(`[ResponseEngine] circuit break interrupt failed: session=${session.id} error=${error instanceof Error ? error.message : String(error)}`);
|
|
4302
|
-
});
|
|
4508
|
+
requestProactiveCircuitBreak();
|
|
4303
4509
|
return result;
|
|
4304
4510
|
}
|
|
4305
4511
|
const errorMsg = `⚠️ proactive 模式违规:${result.reason ?? '请先发送必要说明'}。请重新执行正确的命令。`;
|
|
@@ -4390,7 +4596,7 @@ export class ResponseEngine {
|
|
|
4390
4596
|
if (commandFullAccess.permissionMode !== 'fullaccess'
|
|
4391
4597
|
|| commandFullAccess.processRole !== 'fullaccess-run'
|
|
4392
4598
|
|| commandFullAccess.dataScope !== 'daemon'
|
|
4393
|
-
|| (commandFullAccess.source !== 'fullaccess-command' && commandFullAccess.source !== 'agent-policy')
|
|
4599
|
+
|| (commandFullAccess.source !== 'fullaccess-command' && commandFullAccess.source !== 'agent-policy' && commandFullAccess.source !== 'session-policy')
|
|
4394
4600
|
|| !commandFullAccess.authorizedBy) {
|
|
4395
4601
|
throw new Error('fullaccess command authorization is missing or malformed');
|
|
4396
4602
|
}
|
|
@@ -4403,6 +4609,10 @@ export class ResponseEngine {
|
|
|
4403
4609
|
|| authenticatedActor.operatorId !== commandFullAccess.policyActor?.operatorId)) {
|
|
4404
4610
|
throw new Error('Agent fullaccess policy authorization is missing its Agent identity');
|
|
4405
4611
|
}
|
|
4612
|
+
if (commandFullAccess.source === 'session-policy'
|
|
4613
|
+
&& (commandFullAccess.sessionId !== session.id || configChatType !== 'private')) {
|
|
4614
|
+
throw new Error('Session fullaccess policy authorization is missing its EC session identity');
|
|
4615
|
+
}
|
|
4406
4616
|
fullAccessAuthorization = commandFullAccess;
|
|
4407
4617
|
}
|
|
4408
4618
|
else {
|
|
@@ -4413,7 +4623,7 @@ export class ResponseEngine {
|
|
|
4413
4623
|
|| !message.triggerMeta.authorizedBy) {
|
|
4414
4624
|
throw new Error('fullaccess Trigger override lacks trusted scheduler identity or approval provenance');
|
|
4415
4625
|
}
|
|
4416
|
-
await refreshOwnershipForAuthorization(selfAid);
|
|
4626
|
+
await refreshOwnershipForAuthorization(selfAid, { allowTransient: true });
|
|
4417
4627
|
const currentOwners = new Set(getDaemonOwners());
|
|
4418
4628
|
if (!currentOwners.has(message.triggerMeta.authorizedBy)) {
|
|
4419
4629
|
throw new Error('fullaccess Trigger approval is no longer owned');
|
|
@@ -4554,6 +4764,8 @@ export class ResponseEngine {
|
|
|
4554
4764
|
}
|
|
4555
4765
|
agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
|
|
4556
4766
|
primaryModel = modelOverride?.model || agentModel;
|
|
4767
|
+
// runner 还没跑起来就终止时,终态事件至少带上解析出的模型
|
|
4768
|
+
this.recordBgSessionModels(message, { requestedModel: primaryModel });
|
|
4557
4769
|
if (!hasExplicitTriggerModel && primaryModel) {
|
|
4558
4770
|
const fallbackResolution = await eligibleFallbackModels({
|
|
4559
4771
|
selfAid: selfAid || undefined,
|
|
@@ -5095,7 +5307,11 @@ export class ResponseEngine {
|
|
|
5095
5307
|
chatType: configChatType,
|
|
5096
5308
|
selfAid,
|
|
5097
5309
|
peerId: configActorId || undefined,
|
|
5098
|
-
peerKey
|
|
5310
|
+
// 系统会话(如记忆整理)的消息 peerId 是后台伪对端 `bg:<kind>`,推出来的 peerKey 不是
|
|
5311
|
+
// 它要操作的关系;目标关系由发起方写在 session.metadata.peerKey,与 system-session
|
|
5312
|
+
// 模块的读写边界同源。ec memory 写盘读的是这里,必须用它。
|
|
5313
|
+
// 局部 peerKey(delegation grant / stats / 响应模式)保持伪对端不变,二者此处有意不一致。
|
|
5314
|
+
peerKey: systemSessionContext?.peerKey ?? peerKey,
|
|
5099
5315
|
peerName: peerName || undefined,
|
|
5100
5316
|
peerType: message.peerType || session.metadata?.peerType || undefined,
|
|
5101
5317
|
// 系统会话没有角色
|
|
@@ -5183,7 +5399,9 @@ export class ResponseEngine {
|
|
|
5183
5399
|
event: fullAccessAuthorization.source === 'trigger'
|
|
5184
5400
|
? 'fullaccess.trigger.execution.started'
|
|
5185
5401
|
: fullAccessAuthorization.source === 'agent-policy'
|
|
5186
|
-
? 'fullaccess.agent-policy.execution.started'
|
|
5402
|
+
? 'fullaccess.agent-policy.execution.started'
|
|
5403
|
+
: fullAccessAuthorization.source === 'session-policy'
|
|
5404
|
+
? 'fullaccess.session-policy.execution.started' : 'fullaccess.execution.started',
|
|
5187
5405
|
source: fullAccessAuthorization.source,
|
|
5188
5406
|
actorId: fullAccessAuthorization.authorizedBy,
|
|
5189
5407
|
processRole: fullAccessAuditProcessRole,
|
|
@@ -5234,6 +5452,8 @@ export class ResponseEngine {
|
|
|
5234
5452
|
this.activeRunnerModels.set(session.id, debugRunnerModel);
|
|
5235
5453
|
else
|
|
5236
5454
|
this.activeRunnerModels.delete(session.id);
|
|
5455
|
+
// 每次尝试前覆盖:备用模型切换后,请求模型以最后一次尝试为准
|
|
5456
|
+
this.recordBgSessionModels(message, { requestedModel: debugRunnerModel });
|
|
5237
5457
|
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, runAgentSessionId, runnerImages, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
|
|
5238
5458
|
// The turn may be superseded while the runner is still creating its
|
|
5239
5459
|
// backend query. Replay the interrupt after runQuery resolves, when
|
|
@@ -5279,28 +5499,51 @@ export class ResponseEngine {
|
|
|
5279
5499
|
} : undefined, taskCausation, turnLease, effectivePermissionMode, selfAid, async (request) => {
|
|
5280
5500
|
if (!this.asyncQuestions)
|
|
5281
5501
|
throw new Error('Async question service is unavailable');
|
|
5502
|
+
const blocking = request.questions.some(question => question.blocking === true);
|
|
5282
5503
|
if (permissionContext.approvalInteractionPolicy === 'deny') {
|
|
5283
5504
|
logger.info(`[AsyncQuestions] no interactive recipient for unattended task: session=${session.id}`);
|
|
5505
|
+
if (blocking)
|
|
5506
|
+
throw new Error('阻塞式用户问题无法在无人值守任务中继续');
|
|
5284
5507
|
return;
|
|
5285
5508
|
}
|
|
5286
|
-
|
|
5509
|
+
if (blocking) {
|
|
5510
|
+
attemptTimeout?.pause();
|
|
5511
|
+
timeoutControl?.pauseForInteraction?.();
|
|
5512
|
+
}
|
|
5287
5513
|
try {
|
|
5514
|
+
if (blocking)
|
|
5515
|
+
await agent.interrupt(session.id);
|
|
5516
|
+
await renderer.flush(false);
|
|
5288
5517
|
await this.asyncQuestions.present(session, request, message, adapter, envelope);
|
|
5289
5518
|
}
|
|
5290
5519
|
catch (error) {
|
|
5291
|
-
|
|
5292
|
-
|
|
5520
|
+
if (blocking)
|
|
5521
|
+
throw error;
|
|
5522
|
+
// Non-blocking business input can be restored after reconnect/restart.
|
|
5293
5523
|
logger.warn(`[AsyncQuestions] presentation failed: session=${session.id} error=${String(error)}`);
|
|
5294
5524
|
}
|
|
5525
|
+
finally {
|
|
5526
|
+
if (blocking) {
|
|
5527
|
+
attemptTimeout?.resume();
|
|
5528
|
+
timeoutControl?.resumeForInteraction?.();
|
|
5529
|
+
}
|
|
5530
|
+
}
|
|
5531
|
+
}, async () => {
|
|
5532
|
+
if (proactiveCircuitBreakRequested)
|
|
5533
|
+
await interruptProactiveCircuitBreak();
|
|
5295
5534
|
});
|
|
5296
5535
|
streamResult = attemptTimeout
|
|
5297
5536
|
? await Promise.race([processAttempt, attemptTimeout.promise])
|
|
5298
5537
|
: await processAttempt;
|
|
5538
|
+
// runner 回报的实际模型;之后无论成功、失败还是被打断,终态事件都能带出
|
|
5539
|
+
this.recordBgSessionModels(message, {
|
|
5540
|
+
actualModel: streamResult.lastModelCall?.model || streamResult.model,
|
|
5541
|
+
});
|
|
5299
5542
|
// Activation can complete either before runQuery returns (Codex)
|
|
5300
5543
|
// or while its event stream is consumed (Claude/Gemini/Ecagent).
|
|
5301
5544
|
// Always reconcile from strict latest before any logical retry.
|
|
5302
5545
|
await refreshTopicBindingForLogicalRetry();
|
|
5303
|
-
if (!streamResult.isError) {
|
|
5546
|
+
if (!streamResult.isError && !isExpectedTerminalInterrupt(streamResult.terminalReason)) {
|
|
5304
5547
|
const protocolDecision = this.turnCoordinator.evaluateCommit(turnLease, this.buildTurnCommitEvidence(session, streamResult));
|
|
5305
5548
|
const protocolReason = protocolDecision.ok ? undefined : protocolDecision.reason;
|
|
5306
5549
|
// Codex app-server turn/start appends its input before runQuery
|
|
@@ -5518,7 +5761,7 @@ export class ResponseEngine {
|
|
|
5518
5761
|
// 上下文过长但无法 auto-compact(无 session ID 或 agent 不支持),显示友好提示
|
|
5519
5762
|
await this.emitOperationalNotice(renderer, getContextTooLongHint(agent), 'warn', 'context-too-long');
|
|
5520
5763
|
}
|
|
5521
|
-
if (!streamResult.isError) {
|
|
5764
|
+
if (!streamResult.isError && !isExpectedTerminalInterrupt(streamResult.terminalReason)) {
|
|
5522
5765
|
const commitEvidence = this.buildTurnCommitEvidence(session, streamResult);
|
|
5523
5766
|
const commitDecision = this.turnCoordinator.evaluateCommit(turnLease, commitEvidence);
|
|
5524
5767
|
if (!commitDecision.ok && commitDecision.reason === 'stale_generation') {
|
|
@@ -5715,6 +5958,7 @@ export class ResponseEngine {
|
|
|
5715
5958
|
await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_turn', taskCausation);
|
|
5716
5959
|
return;
|
|
5717
5960
|
}
|
|
5961
|
+
const terminalInterrupt = isExpectedTerminalInterrupt(streamResult.terminalReason);
|
|
5718
5962
|
const streamRecoverable = !permissionTerminalFailure
|
|
5719
5963
|
&& !isExpectedTerminalInterrupt(streamResult.terminalReason)
|
|
5720
5964
|
&& isRetryableError(new Error(getStreamErrorText(streamResult)));
|
|
@@ -5757,7 +6001,7 @@ export class ResponseEngine {
|
|
|
5757
6001
|
this.activeTaskRuntimeContexts.delete(session.id);
|
|
5758
6002
|
this.activeRunnerModels.delete(session.id); // F1修复
|
|
5759
6003
|
}
|
|
5760
|
-
this.agentDelegationRegistry?.revokeTask(session.id, taskId, streamResult.isError ? 'failed' : 'completed');
|
|
6004
|
+
this.agentDelegationRegistry?.revokeTask(session.id, taskId, terminalInterrupt ? 'interrupted' : streamResult.isError ? 'failed' : 'completed');
|
|
5761
6005
|
logger.info(`[ResponseEngine] session ${session.id} processing cleared task=${taskId}`);
|
|
5762
6006
|
import('../../utils/msg-diag.js').then(m => m.diagLog('task_done', { aid: session.selfAID ?? '', msgId: message.messageId ?? '', taskId, text: message.content ?? '' })).catch(() => { });
|
|
5763
6007
|
// 降级模型回复末尾追加标记(代码层硬注入,不依赖模型输出)
|
|
@@ -5800,7 +6044,6 @@ export class ResponseEngine {
|
|
|
5800
6044
|
throw tagged;
|
|
5801
6045
|
}
|
|
5802
6046
|
}
|
|
5803
|
-
const terminalInterrupt = isExpectedTerminalInterrupt(streamResult.terminalReason);
|
|
5804
6047
|
if (!streamResult.isError && !terminalInterrupt) {
|
|
5805
6048
|
const commitDecision = await this.turnCoordinator.commit(session, turnLease, this.buildTurnCommitEvidence(session, streamResult));
|
|
5806
6049
|
if (!commitDecision.ok) {
|
|
@@ -5828,8 +6071,10 @@ export class ResponseEngine {
|
|
|
5828
6071
|
const interruptReason = this.interruptedSessions.get(session.id);
|
|
5829
6072
|
if (streamResult.isError || terminalInterrupt) {
|
|
5830
6073
|
// Agent 流正常结束但任务结果失败(权限被拒、max turns、工具链失败等)
|
|
5831
|
-
const errorSummary = streamResult.
|
|
5832
|
-
|
|
6074
|
+
const errorSummary = streamResult.subtype === 'proactive_circuit_break'
|
|
6075
|
+
? '主动模式门禁触发熔断,任务已中断'
|
|
6076
|
+
: streamResult.errors?.join('; ')
|
|
6077
|
+
|| (terminalInterrupt ? '用户交互未确认,任务已暂停' : '任务执行失败');
|
|
5833
6078
|
const userErrorSummary = streamResult.subtype === 'protocol_incomplete'
|
|
5834
6079
|
? errorSummary
|
|
5835
6080
|
: streamHitContextLimit(streamResult)
|
|
@@ -5852,6 +6097,7 @@ export class ResponseEngine {
|
|
|
5852
6097
|
errorType: rawSubtype,
|
|
5853
6098
|
recoverable: streamRecoverable,
|
|
5854
6099
|
message: userErrorSummary,
|
|
6100
|
+
...(streamRawErrorText ? { raw: streamRawErrorText } : {}),
|
|
5855
6101
|
durationMs: Date.now() - startTime,
|
|
5856
6102
|
numTurns: streamResult.numTurns,
|
|
5857
6103
|
toolCallCount: streamResult.toolUseCount ?? 0,
|
|
@@ -6162,6 +6408,7 @@ export class ResponseEngine {
|
|
|
6162
6408
|
finalText: streamResult.lastReplyText || undefined,
|
|
6163
6409
|
durationMs: Date.now() - startTime,
|
|
6164
6410
|
agentName: agentNameForStats,
|
|
6411
|
+
agentAid: runnerSelfAid,
|
|
6165
6412
|
numTurns: streamResult.numTurns,
|
|
6166
6413
|
encrypted: message.encrypted ?? message.replyContext?.metadata?.encrypted,
|
|
6167
6414
|
timestamp: Date.now(),
|
|
@@ -6180,7 +6427,7 @@ export class ResponseEngine {
|
|
|
6180
6427
|
// 所有 message.send 成功后统一写入 messages.jsonl,此处不再重复写入。
|
|
6181
6428
|
}
|
|
6182
6429
|
const isFinallyBackground = this.isBackgroundSession(session, message.channel, message.channelId);
|
|
6183
|
-
if (isFinallyBackground && !streamResult.isError) {
|
|
6430
|
+
if (isFinallyBackground && !streamResult.isError && !terminalInterrupt) {
|
|
6184
6431
|
if (!this.turnCoordinator.canPublish(turnLease))
|
|
6185
6432
|
return;
|
|
6186
6433
|
this.messageCache.addEvent(session.id, {
|
|
@@ -6272,20 +6519,26 @@ export class ResponseEngine {
|
|
|
6272
6519
|
: failureReason, recoverable);
|
|
6273
6520
|
}
|
|
6274
6521
|
const totalExecutionMs = error instanceof ExecutionTimeoutError ? error.timeoutMs : this.totalExecutionLimitMs();
|
|
6522
|
+
const rawErrorText = getErrorRawText(error);
|
|
6275
6523
|
const procStatus = errType === ErrorType.SDK_TIMEOUT || isTotalExecutionTimeout ? 'timeout'
|
|
6276
6524
|
: errType === ErrorType.STREAM_ERROR ? 'interrupted'
|
|
6277
6525
|
: 'error';
|
|
6278
6526
|
const daemonTrigger = this.isTrustedDaemonTrigger(message);
|
|
6279
6527
|
const statusPayload = error instanceof OrphanedTargetSessionError
|
|
6280
|
-
? { kind: 'status.error', metadata: {
|
|
6528
|
+
? { kind: 'status.error', metadata: {
|
|
6529
|
+
errorType: error.reason,
|
|
6530
|
+
message: '任务执行状态丢失,已结束本次执行。',
|
|
6531
|
+
...(rawErrorText ? { raw: rawErrorText } : {}),
|
|
6532
|
+
} }
|
|
6281
6533
|
: procStatus === 'timeout'
|
|
6282
6534
|
? { kind: 'status.timeout', metadata: isTotalExecutionTimeout
|
|
6283
6535
|
? { ...(totalExecutionMs === undefined ? {} : { totalExecutionMs }),
|
|
6284
|
-
...(error instanceof ExecutionTimeoutError ? { reason: error.reason } : {})
|
|
6285
|
-
|
|
6536
|
+
...(error instanceof ExecutionTimeoutError ? { reason: error.reason } : {}),
|
|
6537
|
+
...(rawErrorText ? { raw: rawErrorText } : {}) }
|
|
6538
|
+
: { idleSec: getLastIdleSec?.() || undefined, ...(rawErrorText ? { raw: rawErrorText } : {}) } }
|
|
6286
6539
|
: procStatus === 'interrupted'
|
|
6287
6540
|
? { kind: 'status.interrupted', metadata: { reason: 'stream_error' } }
|
|
6288
|
-
: { kind: 'status.error' };
|
|
6541
|
+
: { kind: 'status.error', metadata: rawErrorText ? { raw: rawErrorText } : undefined };
|
|
6289
6542
|
// 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送中断/错误提示
|
|
6290
6543
|
if (!isUserInterrupt && ownsTimeoutTerminal && !daemonTrigger) {
|
|
6291
6544
|
await adapter.send(envelope, statusPayload).catch(() => { });
|
|
@@ -6404,13 +6657,15 @@ export class ResponseEngine {
|
|
|
6404
6657
|
: modelFallbackExhaustedMessage
|
|
6405
6658
|
? ErrorType.MODEL_UNAVAILABLE
|
|
6406
6659
|
: normalizeCoreErrorSubtype(error, undefined, isTotalExecutionTimeout || isTimeout ? 'timeout' : undefined);
|
|
6407
|
-
const
|
|
6660
|
+
const distinctRawErrorText = rawErrorText && !userMessage.includes(rawErrorText)
|
|
6661
|
+
? rawErrorText
|
|
6662
|
+
: undefined;
|
|
6408
6663
|
const errorPayload = {
|
|
6409
6664
|
kind: 'result.error',
|
|
6410
6665
|
text: userMessage,
|
|
6411
6666
|
recoverable,
|
|
6412
6667
|
subtype: errorSubtype,
|
|
6413
|
-
...(
|
|
6668
|
+
...(distinctRawErrorText ? { raw: distinctRawErrorText } : {}),
|
|
6414
6669
|
...(errorReason ? { reason: errorReason } : {}),
|
|
6415
6670
|
};
|
|
6416
6671
|
if (!isUserInterrupt && daemonTrigger && procStatus === 'error') {
|
|
@@ -6423,6 +6678,7 @@ export class ResponseEngine {
|
|
|
6423
6678
|
metadata: {
|
|
6424
6679
|
errorType: errType,
|
|
6425
6680
|
message: userMessage,
|
|
6681
|
+
...(rawErrorText ? { raw: rawErrorText } : {}),
|
|
6426
6682
|
recoverable,
|
|
6427
6683
|
durationMs: Date.now() - startTime,
|
|
6428
6684
|
numTurns: streamResult.numTurns,
|
|
@@ -6457,6 +6713,8 @@ export class ResponseEngine {
|
|
|
6457
6713
|
}
|
|
6458
6714
|
}
|
|
6459
6715
|
finally {
|
|
6716
|
+
if (proactiveCircuitBreakTimer)
|
|
6717
|
+
clearTimeout(proactiveCircuitBreakTimer);
|
|
6460
6718
|
if (taskActivityStore) {
|
|
6461
6719
|
try {
|
|
6462
6720
|
taskActivityStore.updateStatus(taskId, taskActivityStatus);
|
|
@@ -6497,7 +6755,9 @@ export class ResponseEngine {
|
|
|
6497
6755
|
event: fullAccessAuthorization.source === 'trigger'
|
|
6498
6756
|
? 'fullaccess.trigger.execution.ended'
|
|
6499
6757
|
: fullAccessAuthorization.source === 'agent-policy'
|
|
6500
|
-
? 'fullaccess.agent-policy.execution.ended'
|
|
6758
|
+
? 'fullaccess.agent-policy.execution.ended'
|
|
6759
|
+
: fullAccessAuthorization.source === 'session-policy'
|
|
6760
|
+
? 'fullaccess.session-policy.execution.ended' : 'fullaccess.execution.ended',
|
|
6501
6761
|
source: fullAccessAuthorization.source,
|
|
6502
6762
|
actorId: fullAccessAuthorization.authorizedBy,
|
|
6503
6763
|
processRole: fullAccessAuditProcessRole,
|
|
@@ -6777,7 +7037,7 @@ export class ResponseEngine {
|
|
|
6777
7037
|
*/
|
|
6778
7038
|
async processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress, proactive,
|
|
6779
7039
|
/** [迁移点4/5] 响应模式插件 + 状态,用于调 onToolUse/onComplete 钩子 */
|
|
6780
|
-
modeHooks, causation, turnLease, permissionMode, proactiveSelfAid, onAsyncQuestion) {
|
|
7040
|
+
modeHooks, causation, turnLease, permissionMode, proactiveSelfAid, onAsyncQuestion, onToolCallsSettled) {
|
|
6781
7041
|
// Per-session agent name for stats bucketing
|
|
6782
7042
|
const statsChannelKey = session.channel === 'daemon' ? session.channel : (session.metadata?.channelKey || session.channel);
|
|
6783
7043
|
const agentNameForStats = this.agentRegistry?.resolveByChannel(statsChannelKey)?.name ?? '<unknown>';
|
|
@@ -6799,17 +7059,23 @@ export class ResponseEngine {
|
|
|
6799
7059
|
const openToolUseIds = new Set();
|
|
6800
7060
|
const anonymousToolUseIds = new Map();
|
|
6801
7061
|
const anonymousOpenIdByCorrelation = new Map();
|
|
6802
|
-
const finalizeResult = () =>
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
7062
|
+
const finalizeResult = () => {
|
|
7063
|
+
const circuitBreakInterrupted = this.interruptedSessions.get(session.id) === 'proactive_circuit_break';
|
|
7064
|
+
return {
|
|
7065
|
+
...completeResult,
|
|
7066
|
+
...(circuitBreakInterrupted
|
|
7067
|
+
? { isError: false, subtype: 'proactive_circuit_break', terminalReason: 'aborted_tools', errors: undefined }
|
|
7068
|
+
: {}),
|
|
7069
|
+
hasReceivedText,
|
|
7070
|
+
hasProjectedCurrentReplyText,
|
|
7071
|
+
inputAccepted,
|
|
7072
|
+
assistantUuid,
|
|
7073
|
+
protocolIncompleteReason: circuitBreakInterrupted ? undefined : protocolIncompleteReason,
|
|
7074
|
+
textCount,
|
|
7075
|
+
toolUseCount,
|
|
7076
|
+
openToolUseIds: [...openToolUseIds],
|
|
7077
|
+
};
|
|
7078
|
+
};
|
|
6813
7079
|
// 追踪最后一轮 assistant 回复文本(tool_use 之后的纯文本)
|
|
6814
7080
|
let lastReplyText = '';
|
|
6815
7081
|
// 上游合成错误帧的原文(runner 标记 synthetic 的 text)。与正文通道分开存放:
|
|
@@ -7451,6 +7717,8 @@ export class ResponseEngine {
|
|
|
7451
7717
|
else if (!toolResultFailed && !shouldSuppress()) {
|
|
7452
7718
|
renderer.addToolResult(event.name || '\u5de5\u5177', true, event.result, undefined, event.callId, event.durationMs, cachedDesc, event, event.assistantUuid);
|
|
7453
7719
|
}
|
|
7720
|
+
if (openToolUseIds.size === 0)
|
|
7721
|
+
await onToolCallsSettled?.();
|
|
7454
7722
|
}
|
|
7455
7723
|
// 非终态运行时告警走 status notify,不进入 activity/thought 历史。
|
|
7456
7724
|
if (event.type === 'notice' && event.severity === 'warn') {
|