evolcore 0.0.12 → 0.0.13
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 +22 -0
- package/bin/ec-safe-output.js +26 -7
- package/dist/agents/claude-runner.js +53 -7
- package/dist/agents/codex-app-server-client.js +53 -5
- package/dist/agents/codex-runner.js +104 -59
- package/dist/agents/ecagent-runner.js +4 -1
- package/dist/aun/aid/control-aid.js +36 -13
- package/dist/aun/aid/store.js +13 -7
- package/dist/aun/group-fs-download.js +56 -0
- package/dist/aun/group-identity.js +1 -2
- package/dist/aun/msg/group.js +11 -33
- package/dist/aun/msg/index.js +1 -2
- package/dist/aun/msg/managed-operation.js +116 -55
- package/dist/aun/msg/upload.js +16 -1
- package/dist/aun/outbox.js +31 -2
- package/dist/aun/rpc/client.js +66 -0
- package/dist/aun/rpc/index.js +1 -2
- package/dist/aun/storage/{upload.js → client.js} +11 -2
- package/dist/aun/storage/index.js +1 -2
- package/dist/channels/aun.js +371 -136
- package/dist/cli/agent.js +9 -96
- package/dist/cli/cli-argv.js +10 -0
- package/dist/cli/config.js +9 -17
- package/dist/cli/ctl-command.js +1 -1
- package/dist/cli/daemon-commands.js +39 -24
- package/dist/cli/fs-command.js +19 -10
- package/dist/cli/init.js +77 -8
- package/dist/cli/queue-command.js +42 -0
- package/dist/cli/restart-monitor.js +3 -14
- package/dist/cli/trigger-command.js +7 -0
- package/dist/config/access-policy.js +1 -2
- package/dist/config/builtin-roles.js +5 -0
- package/dist/config/config-manager.js +21 -1
- package/dist/config/peer-role-resolver.js +6 -2
- package/dist/config/role-migration-startup.js +53 -0
- package/dist/config/role-store.js +1 -1
- package/dist/core/auth/agent-delegation.js +1 -1
- package/dist/core/auth/auth-gateway.js +29 -1
- package/dist/core/auth/authorization-audit.js +15 -0
- package/dist/core/auth/operation-authorizer.js +1 -0
- package/dist/core/auth/operation-catalog.js +9 -0
- package/dist/core/bootstrap-messages.js +9 -2
- package/dist/core/bootstrap-service.js +1 -0
- package/dist/core/command/command-handler.js +198 -51
- package/dist/core/command/menu-catalog.js +4 -0
- package/dist/core/command/menu-handler.js +85 -18
- package/dist/core/command/menu-protocol.js +23 -2
- package/dist/core/command/slash-handler.js +58 -10
- package/dist/core/event-catalog.js +6 -0
- package/dist/core/handoff/runtime.js +12 -1
- package/dist/core/handoff/store.js +5 -1
- package/dist/core/interaction-router.js +10 -0
- package/dist/core/message/im-renderer.js +19 -0
- package/dist/core/message/message-bridge.js +63 -11
- package/dist/core/message/message-log.js +2 -0
- package/dist/core/message/message-queue.js +267 -39
- package/dist/core/message/message-utils.js +81 -4
- package/dist/core/message/response-engine.js +107 -41
- package/dist/core/message/stream-debouncer.js +10 -2
- package/dist/core/permission/approval-gateway.js +1 -0
- package/dist/core/permission/ec-command-parser.js +93 -14
- package/dist/core/permission/index.js +1 -1
- package/dist/core/permission/mode.js +15 -0
- package/dist/core/permission/readonly-shell-query.js +148 -8
- package/dist/core/permission/sandbox-runtime.js +21 -0
- package/dist/core/permission/tool-policy.js +249 -27
- package/dist/core/protected-paths.js +107 -2
- package/dist/core/session/session-manager.js +21 -2
- package/dist/core/session/session-mapper.js +20 -1
- package/dist/eck/detect.js +18 -1
- package/dist/eck/group-rules-sync.js +15 -44
- package/dist/index.js +161 -86
- package/dist/ipc.js +16 -3
- package/dist/paths.js +3 -0
- package/dist/trigger/feedback.js +64 -11
- package/dist/trigger/manager.js +21 -1
- package/dist/trigger/parser.js +14 -2
- package/dist/trigger/scheduler.js +1 -0
- package/dist/trigger/validation.js +67 -6
- package/dist/utils/ecweb-supervisor.js +332 -0
- package/dist/utils/error-utils.js +10 -0
- package/kits/docs/evolcore/trigger.md +17 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/templates/roles/admin.json +5 -0
- package/kits/templates/roles/member.json +7 -1
- package/kits/templates/roles/visitor.json +2 -0
- package/package.json +1 -1
- package/dist/aun/msg/payload-type.js +0 -27
- package/dist/aun/rpc/caller.js +0 -42
- package/dist/aun/rpc/connection.js +0 -25
- package/dist/aun/storage/manage.js +0 -10
- package/dist/config/access-policy-domain.js +0 -18
- package/dist/config/config-batch-get.js +0 -11
- package/dist/config/owner-policy.js +0 -4
- package/dist/config/role-config-v4-startup.js +0 -32
- package/dist/config/role-config-v5-startup.js +0 -27
- package/dist/core/auth/trigger-authorization.js +0 -15
- package/dist/core/interaction-registration.js +0 -10
- package/dist/core/permission/execution-sandbox.js +0 -16
- package/dist/core/relation/peer-key.js +0 -1
- package/dist/core/session/session-key.js +0 -24
- package/dist/eck/baseagent-caps.js +0 -18
- package/dist/eck/rules-loader.js +0 -28
|
@@ -9,6 +9,10 @@ import { renderCommandCardAsText, renderActionAsText } from '../interaction-rout
|
|
|
9
9
|
*/
|
|
10
10
|
export function buildEnvelope(opts) {
|
|
11
11
|
const taskId = opts.taskId ?? `interaction-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
12
|
+
const delivery = opts.delivery ?? opts.replyContext?.delivery;
|
|
13
|
+
const replyContext = opts.replyContext
|
|
14
|
+
? { ...opts.replyContext, ...(delivery ? { delivery } : {}) }
|
|
15
|
+
: undefined;
|
|
12
16
|
return {
|
|
13
17
|
taskId,
|
|
14
18
|
operationId: opts.operationId ?? taskId,
|
|
@@ -17,11 +21,86 @@ export function buildEnvelope(opts) {
|
|
|
17
21
|
channelId: opts.channelId,
|
|
18
22
|
agentName: opts.agentName ?? '<unknown>',
|
|
19
23
|
chatmode: opts.chatmode ?? 'interactive',
|
|
20
|
-
|
|
24
|
+
delivery,
|
|
25
|
+
replyContext,
|
|
21
26
|
causation: opts.causation,
|
|
22
27
|
timestamp: opts.timestamp ?? Date.now(),
|
|
23
28
|
};
|
|
24
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Replace an envelope's reply context while keeping its transport route
|
|
32
|
+
* authoritative at the top level. A per-send context may intentionally move
|
|
33
|
+
* a reply to another target (for example owner approval in a private chat).
|
|
34
|
+
*/
|
|
35
|
+
export function withEnvelopeReplyContext(envelope, replyContext) {
|
|
36
|
+
const delivery = replyContext?.delivery ?? envelope.delivery;
|
|
37
|
+
return {
|
|
38
|
+
...envelope,
|
|
39
|
+
...(delivery ? { delivery } : {}),
|
|
40
|
+
...(replyContext
|
|
41
|
+
? {
|
|
42
|
+
replyContext: {
|
|
43
|
+
...replyContext,
|
|
44
|
+
...(delivery ? { delivery } : {}),
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
: {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Runtime validation for routes persisted in envelopes, sessions, and outbox entries. */
|
|
51
|
+
export function isDeliveryTarget(value) {
|
|
52
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
53
|
+
return false;
|
|
54
|
+
const route = value;
|
|
55
|
+
if (route.chatType === 'private')
|
|
56
|
+
return true;
|
|
57
|
+
return route.chatType === 'group'
|
|
58
|
+
&& typeof route.groupId === 'string'
|
|
59
|
+
&& route.groupId.trim().length > 0;
|
|
60
|
+
}
|
|
61
|
+
/** Validate a route and ensure group routes address the enclosing channel. */
|
|
62
|
+
export function isDeliveryTargetForChannel(value, channelId) {
|
|
63
|
+
return isDeliveryTarget(value)
|
|
64
|
+
&& (value.chatType === 'private' || value.groupId === channelId);
|
|
65
|
+
}
|
|
66
|
+
/** Compare normalized private/group delivery routes. */
|
|
67
|
+
export function sameDeliveryTarget(left, right) {
|
|
68
|
+
return left.chatType === right.chatType
|
|
69
|
+
&& (left.chatType === 'private'
|
|
70
|
+
|| (right.chatType === 'group' && left.groupId === right.groupId));
|
|
71
|
+
}
|
|
72
|
+
/** Resolve an outbound route from trusted, persisted session state. */
|
|
73
|
+
export function deliveryTargetFromSession(session) {
|
|
74
|
+
const channelType = session.channelType || session.channel?.split('#', 1)[0];
|
|
75
|
+
const strictAun = channelType?.toLowerCase() === 'aun';
|
|
76
|
+
if (session.chatType === 'group') {
|
|
77
|
+
const groupId = session.metadata?.groupId;
|
|
78
|
+
if (strictAun && (!groupId || groupId !== session.channelId)) {
|
|
79
|
+
const code = groupId ? 'AUN_OUTBOUND_ROUTE_MISMATCH' : 'AUN_OUTBOUND_ROUTE_REQUIRED';
|
|
80
|
+
throw Object.assign(new Error(`AUN group session has no valid persisted groupId for channelId=${session.channelId}`), { code });
|
|
81
|
+
}
|
|
82
|
+
return { chatType: 'group', groupId: groupId || session.channelId };
|
|
83
|
+
}
|
|
84
|
+
if (session.chatType === 'private')
|
|
85
|
+
return { chatType: 'private' };
|
|
86
|
+
if (strictAun) {
|
|
87
|
+
throw Object.assign(new Error(`AUN session has no persisted chatType for channelId=${session.channelId}`), {
|
|
88
|
+
code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return { chatType: 'private' };
|
|
92
|
+
}
|
|
93
|
+
/** Preserve channel-specific reply fields while filling the session route. */
|
|
94
|
+
export function replyContextFromSession(session, context) {
|
|
95
|
+
return {
|
|
96
|
+
...(session.metadata?.replyContext ?? {}),
|
|
97
|
+
...(context ?? {}),
|
|
98
|
+
// Session chatType/groupId is the persisted routing authority. The nested
|
|
99
|
+
// reply context can outlive a route correction, so only an explicit
|
|
100
|
+
// per-send context may override it.
|
|
101
|
+
delivery: context?.delivery ?? deliveryTargetFromSession(session),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
25
104
|
/**
|
|
26
105
|
* 交互请求的默认回退文本
|
|
27
106
|
* 当调用方未显式提供时使用,根据交互类型选择合适的渲染器。
|
|
@@ -60,9 +139,7 @@ export async function sendInteractionPayload(adapter, envelope, interaction, fal
|
|
|
60
139
|
fallbackText: text || undefined,
|
|
61
140
|
};
|
|
62
141
|
try {
|
|
63
|
-
const enriched = replyCtx
|
|
64
|
-
? { ...envelope, replyContext: replyCtx }
|
|
65
|
-
: envelope;
|
|
142
|
+
const enriched = replyCtx ? withEnvelopeReplyContext(envelope, replyCtx) : envelope;
|
|
66
143
|
const receipt = await adapter.send(enriched, payload);
|
|
67
144
|
if (receipt?.status === 'suppressed')
|
|
68
145
|
return false;
|
|
@@ -22,7 +22,7 @@ import { createRootCausation, deriveCausation, normalizeCausation } from '../cau
|
|
|
22
22
|
import { recordCausationLink, recordCausationSpan } from '../causation/audit.js';
|
|
23
23
|
import { normalizeBaseagent } from '../../agents/baseagent.js';
|
|
24
24
|
import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
|
|
25
|
-
import { buildEnvelope } from './message-utils.js';
|
|
25
|
+
import { buildEnvelope, replyContextFromSession, withEnvelopeReplyContext } from './message-utils.js';
|
|
26
26
|
import { isSystemOrServicePeer, resolveChatMode } from './peer-mode.js';
|
|
27
27
|
import { AGENT_DELEGATION_TOKEN_ENV } from '../auth/agent-delegation.js';
|
|
28
28
|
import { hasTrustedPrincipal } from '../auth/authenticated-actor.js';
|
|
@@ -136,6 +136,7 @@ const RETRY_EXHAUSTED_MARK = Symbol('evolcore.retryExhausted');
|
|
|
136
136
|
const RETRY_MADE_PROGRESS_MARK = Symbol('evolcore.retryMadeProgress');
|
|
137
137
|
const RETRY_HEALTH_RECORDED_MARK = Symbol('evolcore.retryHealthRecorded');
|
|
138
138
|
const MODEL_FALLBACK_EXHAUSTED_MARK = Symbol('evolcore.modelFallbackExhausted');
|
|
139
|
+
const RETRY_INPUT_ALREADY_SUBMITTED_MARK = Symbol('evolcore.retryInputAlreadySubmitted');
|
|
139
140
|
function markRetryExhausted(error, retries) {
|
|
140
141
|
if (error && typeof error === 'object') {
|
|
141
142
|
error[RETRY_EXHAUSTED_MARK] = retries;
|
|
@@ -176,6 +177,21 @@ function getModelFallbackExhaustedMessage(error) {
|
|
|
176
177
|
: undefined;
|
|
177
178
|
return typeof message === 'string' ? message : undefined;
|
|
178
179
|
}
|
|
180
|
+
function markRetryInputAlreadySubmitted(error) {
|
|
181
|
+
if (error && typeof error === 'object') {
|
|
182
|
+
error[RETRY_INPUT_ALREADY_SUBMITTED_MARK] = true;
|
|
183
|
+
// A terminal delivery guard, rather than any buffered activity, owns the
|
|
184
|
+
// final user-visible explanation.
|
|
185
|
+
delete error._errorAlreadySent;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function wasRetryInputAlreadySubmitted(error) {
|
|
189
|
+
return !!(error && typeof error === 'object' && error[RETRY_INPUT_ALREADY_SUBMITTED_MARK] === true);
|
|
190
|
+
}
|
|
191
|
+
function formatRetryInputAlreadySubmittedMessage(error) {
|
|
192
|
+
const reason = getErrorMessage(error, undefined, false) || 'Codex 暂时不可用';
|
|
193
|
+
return `⚠️ Codex 已接收本次消息,但本轮未能确认完成。为避免在会话中重复写入同一消息,系统未自动重试。请检查会话状态后重新发送。\n原因:${reason}`;
|
|
194
|
+
}
|
|
179
195
|
function isNonErrorInterruptReason(reason) {
|
|
180
196
|
return reason === 'new_message'
|
|
181
197
|
|| reason === 'stop'
|
|
@@ -492,7 +508,7 @@ export class ResponseEngine {
|
|
|
492
508
|
return;
|
|
493
509
|
const available = error.availableBaseagents.length ? error.availableBaseagents.join(', ') : '(none)';
|
|
494
510
|
const text = `❌ 当前会话绑定的 baseagent 不可用: ${error.baseagent}\nAgent: ${error.evolagentName}\n可用: ${available}\n请使用 /baseagent 切换到可用后端。`;
|
|
495
|
-
const sessionReplyContext = { ...(replyContext
|
|
511
|
+
const sessionReplyContext = { ...replyContextFromSession(session, replyContext), sessionId: session.id };
|
|
496
512
|
await channelInfo.adapter.send(buildEnvelope({
|
|
497
513
|
taskId: `system-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`,
|
|
498
514
|
sessionId: session.id,
|
|
@@ -655,7 +671,7 @@ export class ResponseEngine {
|
|
|
655
671
|
agentName: agent.name,
|
|
656
672
|
chatmode: this.resolveEffectiveChatmodeForSession(session, channelKey, session.channelId),
|
|
657
673
|
replyContext: {
|
|
658
|
-
...(session
|
|
674
|
+
...replyContextFromSession(session),
|
|
659
675
|
sessionId: session.id,
|
|
660
676
|
threadId: session.threadId || session.metadata?.replyContext?.threadId,
|
|
661
677
|
},
|
|
@@ -937,7 +953,7 @@ export class ResponseEngine {
|
|
|
937
953
|
}
|
|
938
954
|
agent.registerStream(streamKey, retryStream);
|
|
939
955
|
return await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress, proactive, undefined, // 重试分支不调插件钩子
|
|
940
|
-
undefined, turnLease);
|
|
956
|
+
undefined, turnLease, opts.permissionMode);
|
|
941
957
|
}
|
|
942
958
|
// Dropping the whole session hides the root cause and can replay side
|
|
943
959
|
// effects in unattended trigger tasks. Surface an actionable failure
|
|
@@ -1199,7 +1215,7 @@ export class ResponseEngine {
|
|
|
1199
1215
|
logger.warn(`[ResponseEngine] Trigger execution identity missing: trigger=${message.triggerMeta?.triggerId ?? '<none>'}`);
|
|
1200
1216
|
const channelInfo = this.resolveChannelInfo(accessChannelKey);
|
|
1201
1217
|
if (channelInfo) {
|
|
1202
|
-
await channelInfo.adapter.send({
|
|
1218
|
+
await channelInfo.adapter.send(buildEnvelope({
|
|
1203
1219
|
taskId: `trigger-identity-missing-${Date.now()}`,
|
|
1204
1220
|
sessionId: session.id,
|
|
1205
1221
|
channel: accessChannelKey,
|
|
@@ -1208,7 +1224,7 @@ export class ResponseEngine {
|
|
|
1208
1224
|
chatmode: 'interactive',
|
|
1209
1225
|
replyContext: message.replyContext,
|
|
1210
1226
|
timestamp: Date.now(),
|
|
1211
|
-
}, {
|
|
1227
|
+
}), {
|
|
1212
1228
|
kind: 'system.error',
|
|
1213
1229
|
text: 'Trigger 创建者身份缺失,已拒绝执行',
|
|
1214
1230
|
subtype: 'trigger_identity_missing',
|
|
@@ -1230,7 +1246,7 @@ export class ResponseEngine {
|
|
|
1230
1246
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
1231
1247
|
if (channelInfo) {
|
|
1232
1248
|
try {
|
|
1233
|
-
await channelInfo.adapter.send({
|
|
1249
|
+
await channelInfo.adapter.send(buildEnvelope({
|
|
1234
1250
|
taskId: `access-denied-${Date.now()}`,
|
|
1235
1251
|
sessionId: session.id,
|
|
1236
1252
|
channel: channelKey,
|
|
@@ -1239,7 +1255,7 @@ export class ResponseEngine {
|
|
|
1239
1255
|
chatmode: 'interactive',
|
|
1240
1256
|
replyContext: message.replyContext,
|
|
1241
1257
|
timestamp: Date.now(),
|
|
1242
|
-
}, {
|
|
1258
|
+
}), {
|
|
1243
1259
|
kind: 'system.error',
|
|
1244
1260
|
text: '暂无权限访问本 agent,请联系 agent 管理员授权访问',
|
|
1245
1261
|
subtype: 'access_denied',
|
|
@@ -1497,7 +1513,12 @@ export class ResponseEngine {
|
|
|
1497
1513
|
}
|
|
1498
1514
|
/** 自动安全模式已禁用:仅保留错误计数,不再自动切换状态 */
|
|
1499
1515
|
async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, getLastIdleSec, outputState = { middleOutputMode: 'all' }, timeoutControl) {
|
|
1500
|
-
|
|
1516
|
+
// The model must see the time at which the adapter accepted the input.
|
|
1517
|
+
// Runner start is recorded separately so queue delay never rewrites input
|
|
1518
|
+
// chronology after a historical replay or a long backlog.
|
|
1519
|
+
const inboundTimestamp = message.receivedAt ?? message.enqueuedAt ?? message.timestamp ?? Date.now();
|
|
1520
|
+
const runnerStartedAt = Date.now();
|
|
1521
|
+
const messageId = `${message.channel}_${message.channelId}_${inboundTimestamp}`;
|
|
1501
1522
|
const channelKey = this.messageChannelKey(message, session);
|
|
1502
1523
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
1503
1524
|
const taskAgentAid = message.selfAID || session.selfAID;
|
|
@@ -1724,7 +1745,7 @@ export class ResponseEngine {
|
|
|
1724
1745
|
const taskReplyContext = () => {
|
|
1725
1746
|
const base = this.getReplyContext(message);
|
|
1726
1747
|
return {
|
|
1727
|
-
...(base
|
|
1748
|
+
...replyContextFromSession(session, base),
|
|
1728
1749
|
sessionId: session.id,
|
|
1729
1750
|
threadId: session.threadId || base?.threadId,
|
|
1730
1751
|
metadata: withTaskMetadata(base?.metadata),
|
|
@@ -1844,7 +1865,7 @@ export class ResponseEngine {
|
|
|
1844
1865
|
// Kept outside the task body so a thrown terminal error can preserve the
|
|
1845
1866
|
// work already performed in its trigger audit metadata.
|
|
1846
1867
|
let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
|
|
1847
|
-
let startTime =
|
|
1868
|
+
let startTime = runnerStartedAt;
|
|
1848
1869
|
let protocolReplayAttempts = 0;
|
|
1849
1870
|
try {
|
|
1850
1871
|
const isBackground = this.isBackgroundSession(session, message.channel, message.channelId);
|
|
@@ -1853,7 +1874,10 @@ export class ResponseEngine {
|
|
|
1853
1874
|
msgId: messageId,
|
|
1854
1875
|
sessionId: session.id,
|
|
1855
1876
|
dir: 'inbound',
|
|
1856
|
-
status: 'received'
|
|
1877
|
+
status: 'received',
|
|
1878
|
+
receivedAt: message.receivedAt ?? inboundTimestamp,
|
|
1879
|
+
enqueuedAt: message.enqueuedAt,
|
|
1880
|
+
processingStartedAt: runnerStartedAt,
|
|
1857
1881
|
});
|
|
1858
1882
|
this.eventBus.publish({
|
|
1859
1883
|
type: 'message:received',
|
|
@@ -1862,7 +1886,10 @@ export class ResponseEngine {
|
|
|
1862
1886
|
channelId: message.channelId,
|
|
1863
1887
|
content: message.content,
|
|
1864
1888
|
agentName: agentNameForStats,
|
|
1865
|
-
timestamp:
|
|
1889
|
+
timestamp: inboundTimestamp,
|
|
1890
|
+
receivedAt: message.receivedAt ?? inboundTimestamp,
|
|
1891
|
+
enqueuedAt: message.enqueuedAt,
|
|
1892
|
+
processingStartedAt: runnerStartedAt,
|
|
1866
1893
|
});
|
|
1867
1894
|
// ── 硬上限检查:超限直接返回提示,不调模型 ──
|
|
1868
1895
|
{
|
|
@@ -2007,7 +2034,7 @@ export class ResponseEngine {
|
|
|
2007
2034
|
opts.metadata = withTaskMetadata(opts.metadata);
|
|
2008
2035
|
if (payload.kind.startsWith('status.'))
|
|
2009
2036
|
this.touchAgentActivity(channelKey);
|
|
2010
|
-
const enrichedEnvelope =
|
|
2037
|
+
const enrichedEnvelope = withEnvelopeReplyContext(envelope, opts);
|
|
2011
2038
|
snapshot.pushOutbound(session.id, taskId, { kind: payload.kind, decision: 'sent' });
|
|
2012
2039
|
await adapter.send(enrichedEnvelope, payload);
|
|
2013
2040
|
},
|
|
@@ -2032,7 +2059,7 @@ export class ResponseEngine {
|
|
|
2032
2059
|
const capturedReplyContext = taskReplyContext();
|
|
2033
2060
|
// 设置权限审批的消息发送回调(指向当前渠道)
|
|
2034
2061
|
const permissionPrompt = async (text) => {
|
|
2035
|
-
await adapter.send(
|
|
2062
|
+
await adapter.send(withEnvelopeReplyContext(envelope, capturedReplyContext), { kind: 'result.text', text, isFinal: false });
|
|
2036
2063
|
};
|
|
2037
2064
|
const authChatType = configChatType;
|
|
2038
2065
|
const authConversationId = authChatType === 'group'
|
|
@@ -2046,15 +2073,16 @@ export class ResponseEngine {
|
|
|
2046
2073
|
: undefined;
|
|
2047
2074
|
const permissionAdapter = triggerOriginChannelInfo?.adapter ?? adapter;
|
|
2048
2075
|
const permissionChannelId = authorizationIdentity?.actorId ?? capturedChannelId;
|
|
2049
|
-
const permissionReplyContext = authorizationIdentity
|
|
2076
|
+
const permissionReplyContext = authorizationIdentity
|
|
2077
|
+
? { delivery: { chatType: 'private' } }
|
|
2078
|
+
: capturedReplyContext;
|
|
2050
2079
|
let permissionTerminalFailure;
|
|
2051
2080
|
const permissionPromptForOrigin = async (text) => {
|
|
2052
|
-
await permissionAdapter.send({
|
|
2081
|
+
await permissionAdapter.send(withEnvelopeReplyContext({
|
|
2053
2082
|
...envelope,
|
|
2054
2083
|
channel: authorizationIdentity?.originChannelKey ?? envelope.channel,
|
|
2055
2084
|
channelId: permissionChannelId,
|
|
2056
|
-
|
|
2057
|
-
}, { kind: 'result.text', text, isFinal: false });
|
|
2085
|
+
}, permissionReplyContext), { kind: 'result.text', text, isFinal: false });
|
|
2058
2086
|
};
|
|
2059
2087
|
agent.setSendPrompt(permissionPromptForOrigin);
|
|
2060
2088
|
const owningAgentForAuth = owningAgentForTask;
|
|
@@ -2200,7 +2228,7 @@ export class ResponseEngine {
|
|
|
2200
2228
|
},
|
|
2201
2229
|
},
|
|
2202
2230
|
permissionStateChanged: async (permissionEvent) => {
|
|
2203
|
-
const executionEnvelope =
|
|
2231
|
+
const executionEnvelope = withEnvelopeReplyContext(envelope, capturedReplyContext);
|
|
2204
2232
|
if (permissionEvent.state === 'waiting') {
|
|
2205
2233
|
const payload = {
|
|
2206
2234
|
kind: 'status.requires_action',
|
|
@@ -2214,12 +2242,11 @@ export class ResponseEngine {
|
|
|
2214
2242
|
};
|
|
2215
2243
|
await adapter.send(executionEnvelope, payload);
|
|
2216
2244
|
if (permissionAdapter !== adapter || permissionChannelId !== capturedChannelId) {
|
|
2217
|
-
await permissionAdapter.send({
|
|
2245
|
+
await permissionAdapter.send(withEnvelopeReplyContext({
|
|
2218
2246
|
...envelope,
|
|
2219
2247
|
channel: authorizationIdentity?.originChannelKey ?? envelope.channel,
|
|
2220
2248
|
channelId: permissionChannelId,
|
|
2221
|
-
|
|
2222
|
-
}, payload);
|
|
2249
|
+
}, permissionReplyContext), payload);
|
|
2223
2250
|
}
|
|
2224
2251
|
return;
|
|
2225
2252
|
}
|
|
@@ -2777,6 +2804,7 @@ export class ResponseEngine {
|
|
|
2777
2804
|
? (agent.resolveModelId(model) ?? model)
|
|
2778
2805
|
: model));
|
|
2779
2806
|
const retryAttemptTimeoutMs = this.retryAttemptTimeoutMs();
|
|
2807
|
+
const retryInputIsAtMostOnce = agent.retryInputSemantics === 'at_most_once';
|
|
2780
2808
|
let runAttempt = 1;
|
|
2781
2809
|
const recordRetryHealthError = async (retryError) => {
|
|
2782
2810
|
if (!policy.accumulateErrors(chatType, identityRole))
|
|
@@ -2813,14 +2841,17 @@ export class ResponseEngine {
|
|
|
2813
2841
|
const processAttempt = this.processEventStream(stream, session, agent, renderer, (eventType, toolName) => {
|
|
2814
2842
|
resetTimer(eventType, toolName);
|
|
2815
2843
|
attemptTimeout?.reset();
|
|
2816
|
-
}, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease);
|
|
2844
|
+
}, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease, effectivePermissionMode);
|
|
2817
2845
|
streamResult = attemptTimeout
|
|
2818
2846
|
? await Promise.race([processAttempt, attemptTimeout.promise])
|
|
2819
2847
|
: await processAttempt;
|
|
2820
2848
|
if (!streamResult.isError) {
|
|
2821
2849
|
const protocolDecision = this.turnCoordinator.evaluateCommit(turnLease, this.buildTurnCommitEvidence(session, streamResult));
|
|
2822
2850
|
const protocolReason = protocolDecision.ok ? undefined : protocolDecision.reason;
|
|
2823
|
-
|
|
2851
|
+
// Codex app-server turn/start appends its input before runQuery
|
|
2852
|
+
// resolves. Replaying here would create a second user turn.
|
|
2853
|
+
const safeProtocolReplay = !retryInputIsAtMostOnce
|
|
2854
|
+
&& isSafeProtocolReplayCandidate(streamResult, protocolReason);
|
|
2824
2855
|
const protocolRetry = retryScheduler.afterProtocolFailure(safeProtocolReplay ? protocolReason : undefined, safeProtocolReplay);
|
|
2825
2856
|
if (protocolRetry.kind === 'protocol_replay') {
|
|
2826
2857
|
protocolReplayAttempts = protocolRetry.attempt;
|
|
@@ -2865,6 +2896,16 @@ export class ResponseEngine {
|
|
|
2865
2896
|
permissionError._errorAlreadySent = true;
|
|
2866
2897
|
throw permissionError;
|
|
2867
2898
|
}
|
|
2899
|
+
const retryWouldReplaySubmittedInput = retryAttemptTimedOut
|
|
2900
|
+
|| isRetryableError(retryError)
|
|
2901
|
+
|| classifyError(retryError) === ErrorType.MODEL_UNAVAILABLE;
|
|
2902
|
+
if (retryInputIsAtMostOnce && streamRegistered && retryWouldReplaySubmittedInput) {
|
|
2903
|
+
await recordRetryHealthError(retryError);
|
|
2904
|
+
markRetryInputAlreadySubmitted(retryError);
|
|
2905
|
+
logger.warn(`[ResponseEngine] Not replaying already-submitted input: agent=${agent.name} `
|
|
2906
|
+
+ `session=${session.id} task=${taskId} attempt=${runAttempt}`);
|
|
2907
|
+
throw retryError;
|
|
2908
|
+
}
|
|
2868
2909
|
if (classifyError(retryError) === ErrorType.MODEL_UNAVAILABLE && !hasExplicitTriggerModel) {
|
|
2869
2910
|
if (probingPrimaryModel && previousFallbackState) {
|
|
2870
2911
|
const resumedFallback = previousFallbackState.activeFallback;
|
|
@@ -2958,6 +2999,7 @@ export class ResponseEngine {
|
|
|
2958
2999
|
resetTimer,
|
|
2959
3000
|
shouldSuppress,
|
|
2960
3001
|
proactive,
|
|
3002
|
+
permissionMode: effectivePermissionMode,
|
|
2961
3003
|
turnLease,
|
|
2962
3004
|
});
|
|
2963
3005
|
}
|
|
@@ -2999,6 +3041,7 @@ export class ResponseEngine {
|
|
|
2999
3041
|
resetTimer,
|
|
3000
3042
|
shouldSuppress,
|
|
3001
3043
|
proactive,
|
|
3044
|
+
permissionMode: effectivePermissionMode,
|
|
3002
3045
|
turnLease,
|
|
3003
3046
|
});
|
|
3004
3047
|
// 重试后仍然 prompt_too_long:显示友好提示
|
|
@@ -3070,7 +3113,7 @@ export class ResponseEngine {
|
|
|
3070
3113
|
return;
|
|
3071
3114
|
const isCurrentlyBackground = this.isBackgroundSession(session, message.channel, message.channelId);
|
|
3072
3115
|
if (!isCurrentlyBackground) {
|
|
3073
|
-
await adapter.send(
|
|
3116
|
+
await adapter.send(withEnvelopeReplyContext(envelope, capturedReplyContext), payload);
|
|
3074
3117
|
if (payload.kind === 'result.text') {
|
|
3075
3118
|
logger.info(`[ResponseEngine] afterProcess sent: task=${taskId} text="${payload.text.slice(0, 60)}"`);
|
|
3076
3119
|
}
|
|
@@ -3158,7 +3201,10 @@ export class ResponseEngine {
|
|
|
3158
3201
|
// 发送文件
|
|
3159
3202
|
logger.info(`[${adapter.channelName}] Sending file via ${targetInfo.adapter.channelName}: ${resolvedPath}`);
|
|
3160
3203
|
try {
|
|
3161
|
-
|
|
3204
|
+
const targetReplyContext = isCrossChannel
|
|
3205
|
+
? { delivery: { chatType: 'private' } }
|
|
3206
|
+
: capturedReplyContext;
|
|
3207
|
+
await targetInfo.adapter.send(buildEnvelope({ taskId, channel: targetInfo.adapter.channelName, channelId: targetChannelId, agentName: agentNameForStats, replyContext: targetReplyContext }), { kind: 'result.file', filePath: resolvedPath });
|
|
3162
3208
|
this.eventBus.publish({ type: 'runner:file-sent', sessionId: session.id, filePath: resolvedPath, channel: targetInfo.adapter.channelName });
|
|
3163
3209
|
sent++;
|
|
3164
3210
|
if (isCrossChannel) {
|
|
@@ -3681,6 +3727,7 @@ export class ResponseEngine {
|
|
|
3681
3727
|
// processEventStream 已通过 renderer 发过错误时也跳过
|
|
3682
3728
|
const retryExhaustedCount = getRetryExhaustedCount(error);
|
|
3683
3729
|
const modelFallbackExhaustedMessage = getModelFallbackExhaustedMessage(error);
|
|
3730
|
+
const retryInputAlreadySubmitted = wasRetryInputAlreadySubmitted(error);
|
|
3684
3731
|
if (isUserInterrupt) {
|
|
3685
3732
|
logger.info(`[ResponseEngine] User interrupt by new_message, skip sending error message`);
|
|
3686
3733
|
}
|
|
@@ -3692,13 +3739,15 @@ export class ResponseEngine {
|
|
|
3692
3739
|
const idleSec = getLastIdleSec?.() || 0;
|
|
3693
3740
|
const userMessage = retryExhaustedCount
|
|
3694
3741
|
? formatRetryableErrorFinalMessage(error, retryExhaustedCount)
|
|
3695
|
-
:
|
|
3696
|
-
?
|
|
3697
|
-
:
|
|
3698
|
-
?
|
|
3699
|
-
:
|
|
3700
|
-
?
|
|
3701
|
-
:
|
|
3742
|
+
: retryInputAlreadySubmitted
|
|
3743
|
+
? formatRetryInputAlreadySubmittedMessage(error)
|
|
3744
|
+
: modelFallbackExhaustedMessage
|
|
3745
|
+
? modelFallbackExhaustedMessage
|
|
3746
|
+
: isTotalExecutionTimeout
|
|
3747
|
+
? `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`
|
|
3748
|
+
: isTimeout
|
|
3749
|
+
? (idleSec > 0 ? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断` : '⚠️ 任务超时,已自动中断')
|
|
3750
|
+
: getErrorMessage(error, undefined);
|
|
3702
3751
|
// 获取 session 用于话题回复(如果 resolveSession 已执行)
|
|
3703
3752
|
let sendOpts;
|
|
3704
3753
|
try {
|
|
@@ -3715,14 +3764,14 @@ export class ResponseEngine {
|
|
|
3715
3764
|
const errorPayload = {
|
|
3716
3765
|
kind: 'result.error',
|
|
3717
3766
|
text: userMessage,
|
|
3718
|
-
reason: retryExhaustedCount ? 'retry_exhausted' : modelFallbackExhaustedMessage ? 'model_unavailable' : isTotalExecutionTimeout ? 'total_execution_timeout' : isTimeout ? 'timeout' : errType,
|
|
3767
|
+
reason: retryExhaustedCount ? 'retry_exhausted' : retryInputAlreadySubmitted ? 'input_already_submitted' : modelFallbackExhaustedMessage ? 'model_unavailable' : isTotalExecutionTimeout ? 'total_execution_timeout' : isTimeout ? 'timeout' : errType,
|
|
3719
3768
|
};
|
|
3720
3769
|
if (!isUserInterrupt && daemonTrigger && procStatus === 'error') {
|
|
3721
3770
|
// DaemonChannel settles a trigger on the first terminal payload. Put
|
|
3722
3771
|
// the complete failure metadata first so the audit retains tool
|
|
3723
3772
|
// counts and context-recovery diagnostics even for thrown errors
|
|
3724
3773
|
// (for example when compact itself fails).
|
|
3725
|
-
await adapter.send(
|
|
3774
|
+
await adapter.send(withEnvelopeReplyContext(envelope, sendOpts), {
|
|
3726
3775
|
kind: 'status.error',
|
|
3727
3776
|
metadata: {
|
|
3728
3777
|
errorType: errType,
|
|
@@ -3735,17 +3784,17 @@ export class ResponseEngine {
|
|
|
3735
3784
|
contextRecoveryAttempts: streamResult.contextRecoveryAttempts ?? 0,
|
|
3736
3785
|
},
|
|
3737
3786
|
}).catch(() => { });
|
|
3738
|
-
await adapter.send(
|
|
3787
|
+
await adapter.send(withEnvelopeReplyContext(envelope, sendOpts), errorPayload);
|
|
3739
3788
|
}
|
|
3740
3789
|
else {
|
|
3741
|
-
await adapter.send(
|
|
3790
|
+
await adapter.send(withEnvelopeReplyContext(envelope, sendOpts), errorPayload);
|
|
3742
3791
|
}
|
|
3743
3792
|
if (!isUserInterrupt && daemonTrigger && procStatus !== 'error') {
|
|
3744
|
-
await adapter.send(
|
|
3793
|
+
await adapter.send(withEnvelopeReplyContext(envelope, sendOpts), statusPayload).catch(() => { });
|
|
3745
3794
|
}
|
|
3746
3795
|
// Proactive 可观测:catch 块的基础设施错误也透传为 thought,保证按 task_id 聚合完整
|
|
3747
3796
|
if (isProactive && adapter.capabilities?.thought) {
|
|
3748
|
-
await adapter.send(
|
|
3797
|
+
await adapter.send(withEnvelopeReplyContext(envelope, sendOpts), {
|
|
3749
3798
|
kind: 'activity.batch',
|
|
3750
3799
|
items: [{
|
|
3751
3800
|
kind: 'notice',
|
|
@@ -3973,7 +4022,7 @@ export class ResponseEngine {
|
|
|
3973
4022
|
*/
|
|
3974
4023
|
async processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress, proactive,
|
|
3975
4024
|
/** [迁移点4/5] 响应模式插件 + 状态,用于调 onToolUse/onComplete 钩子 */
|
|
3976
|
-
modeHooks, causation, turnLease) {
|
|
4025
|
+
modeHooks, causation, turnLease, permissionMode) {
|
|
3977
4026
|
// Per-session agent name for stats bucketing
|
|
3978
4027
|
const statsChannelKey = session.channel === 'daemon' ? session.channel : (session.metadata?.channelKey || session.channel);
|
|
3979
4028
|
const agentNameForStats = this.agentRegistry?.resolveByChannel(statsChannelKey)?.name ?? '<unknown>';
|
|
@@ -4059,6 +4108,11 @@ export class ResponseEngine {
|
|
|
4059
4108
|
}
|
|
4060
4109
|
}
|
|
4061
4110
|
}
|
|
4111
|
+
else if (event.type === 'notice') {
|
|
4112
|
+
for (const callId of event.closedToolCallIds ?? []) {
|
|
4113
|
+
openToolUseIds.delete(callId);
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4062
4116
|
else if (event.type === 'complete') {
|
|
4063
4117
|
assistantUuid = event.assistantUuid ?? assistantUuid;
|
|
4064
4118
|
protocolIncompleteReason = event.protocolIncompleteReason ?? protocolIncompleteReason;
|
|
@@ -4295,6 +4349,9 @@ export class ResponseEngine {
|
|
|
4295
4349
|
sessionId: session.id,
|
|
4296
4350
|
toolName: event.name,
|
|
4297
4351
|
input: event.input,
|
|
4352
|
+
...(event.callId ? { callId: event.callId } : {}),
|
|
4353
|
+
...(session.selfAID ? { agentAid: session.selfAID } : {}),
|
|
4354
|
+
...(permissionMode ? { permissionMode } : {}),
|
|
4298
4355
|
timestamp: Date.now(),
|
|
4299
4356
|
causation,
|
|
4300
4357
|
});
|
|
@@ -4344,6 +4401,9 @@ export class ResponseEngine {
|
|
|
4344
4401
|
toolName: event.name,
|
|
4345
4402
|
isError: event.isError,
|
|
4346
4403
|
agentName: agentNameForStats,
|
|
4404
|
+
...(event.callId ? { callId: event.callId } : {}),
|
|
4405
|
+
...(session.selfAID ? { agentAid: session.selfAID } : {}),
|
|
4406
|
+
...(permissionMode ? { permissionMode } : {}),
|
|
4347
4407
|
timestamp: Date.now(),
|
|
4348
4408
|
causation,
|
|
4349
4409
|
});
|
|
@@ -4360,6 +4420,12 @@ export class ResponseEngine {
|
|
|
4360
4420
|
renderer.addToolResult(event.name || '\u5de5\u5177', true, event.result, undefined, event.callId, event.durationMs, cachedDesc);
|
|
4361
4421
|
}
|
|
4362
4422
|
}
|
|
4423
|
+
// 非终态运行时告警走 status notify,不进入 activity/thought 历史。
|
|
4424
|
+
if (event.type === 'notice' && event.severity === 'warn') {
|
|
4425
|
+
await renderer.sendWarningStatus(event.message, event.subtype, {
|
|
4426
|
+
...(event.missingItems !== undefined && { missingItems: event.missingItems }),
|
|
4427
|
+
});
|
|
4428
|
+
}
|
|
4363
4429
|
// 运行时错误(Codex: turn.failed / item error)
|
|
4364
4430
|
if (event.type === 'error') {
|
|
4365
4431
|
logger.warn(`[ResponseEngine] error event: ${event.errorType}: ${event.error}`);
|
|
@@ -56,9 +56,10 @@ export class StreamDebouncer {
|
|
|
56
56
|
* 如果窗口只剩这一条,整个窗口取消。
|
|
57
57
|
* @returns true 如果找到并移除
|
|
58
58
|
*/
|
|
59
|
-
cancel(messageId) {
|
|
59
|
+
cancel(messageId, selfAID) {
|
|
60
60
|
for (const [key, win] of this.pending) {
|
|
61
|
-
const idx = win.entries.findIndex(e => e.messageId === messageId
|
|
61
|
+
const idx = win.entries.findIndex(e => e.messageId === messageId
|
|
62
|
+
&& (selfAID === undefined || e.rest.selfAID === selfAID));
|
|
62
63
|
if (idx === -1)
|
|
63
64
|
continue;
|
|
64
65
|
// resolve 被撤回的那条(静默完成,不报错)
|
|
@@ -89,8 +90,13 @@ export class StreamDebouncer {
|
|
|
89
90
|
const contents = [];
|
|
90
91
|
const sourceMessageIds = [];
|
|
91
92
|
const seenSourceMessageIds = new Set();
|
|
93
|
+
let receivedAt;
|
|
92
94
|
for (const e of entries) {
|
|
93
95
|
contents.push(e.content);
|
|
96
|
+
const entryReceivedAt = e.rest.receivedAt ?? e.rest.timestamp;
|
|
97
|
+
if (entryReceivedAt !== undefined) {
|
|
98
|
+
receivedAt = receivedAt === undefined ? entryReceivedAt : Math.min(receivedAt, entryReceivedAt);
|
|
99
|
+
}
|
|
94
100
|
if (e.images)
|
|
95
101
|
allImages.push(...e.images);
|
|
96
102
|
if (e.mentions)
|
|
@@ -119,6 +125,8 @@ export class StreamDebouncer {
|
|
|
119
125
|
replyContext: last.replyContext,
|
|
120
126
|
messageId: latestMessageId,
|
|
121
127
|
sourceMessageIds: sourceMessageIds.length > 0 ? sourceMessageIds : undefined,
|
|
128
|
+
receivedAt,
|
|
129
|
+
timestamp: receivedAt ?? last.rest.timestamp,
|
|
122
130
|
};
|
|
123
131
|
const resolves = entries.map(e => e.resolve);
|
|
124
132
|
const rejects = entries.map(e => e.reject);
|