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
|
@@ -21,6 +21,9 @@ export class MessageQueue {
|
|
|
21
21
|
interceptors = new Map();
|
|
22
22
|
mutedAgents = new Set(); // 禁言的 agent:消息照常入队,但不取出给大模型
|
|
23
23
|
persistencePath;
|
|
24
|
+
dedupPath;
|
|
25
|
+
dedupTtlMs;
|
|
26
|
+
durableMessageIds = new Map();
|
|
24
27
|
activeBatches = new Map();
|
|
25
28
|
processingStartTime = new Map(); // queueKey → 处理开始时间戳
|
|
26
29
|
queueKeyToSessionKey = new Map(); // queueKey → sessionKey(human-readable 格式)
|
|
@@ -32,6 +35,10 @@ export class MessageQueue {
|
|
|
32
35
|
constructor(handler, options) {
|
|
33
36
|
this.handler = handler;
|
|
34
37
|
this.persistencePath = options?.persistencePath;
|
|
38
|
+
this.dedupPath = options?.dedupPath
|
|
39
|
+
?? (this.persistencePath ? `${this.persistencePath}.dedup.json` : undefined);
|
|
40
|
+
this.dedupTtlMs = Math.max(60_000, options?.dedupTtlMs ?? 7 * 24 * 60 * 60 * 1000);
|
|
41
|
+
this.restoreDurableDedup();
|
|
35
42
|
}
|
|
36
43
|
/** 暴露逻辑队列桥接,供响应模式系统注入队列工厂 */
|
|
37
44
|
getLogicalQueueBridge() {
|
|
@@ -108,16 +115,22 @@ export class MessageQueue {
|
|
|
108
115
|
/**
|
|
109
116
|
* 检查消息是否应该处理(去重)
|
|
110
117
|
*
|
|
111
|
-
* 去重 key = `${sessionKey}:${messageId}`,而非裸 messageId。
|
|
118
|
+
* 去重 key = `${selfAID}:${sessionKey}:${messageId}`,而非裸 messageId。
|
|
112
119
|
* MessageQueue 是进程级单例,被所有 evolagent 共享。AUN 群广播时同一条群消息
|
|
113
120
|
* 会投递给群里每个 evolagent,它们 messageId 相同但 session 不同,必须各处理一次。
|
|
114
|
-
* 裸 messageId
|
|
121
|
+
* 裸 messageId 或仅按 session 去重都会让先入队的 agent 吞掉其他 agent 的消息。
|
|
115
122
|
*/
|
|
116
|
-
|
|
123
|
+
dedupKey(sessionKey, messageId, selfAID) {
|
|
124
|
+
return `${selfAID ?? ''}:${sessionKey}:${messageId}`;
|
|
125
|
+
}
|
|
126
|
+
shouldProcess(sessionKey, message, selfAID) {
|
|
117
127
|
if (!message.messageId)
|
|
118
128
|
return true; // 无 ID 的消息不去重
|
|
119
|
-
const
|
|
120
|
-
|
|
129
|
+
const scope = selfAID ?? message.selfAID;
|
|
130
|
+
const dedupKey = this.dedupKey(sessionKey, message.messageId, scope);
|
|
131
|
+
if (this.recentMessageIds.has(dedupKey)
|
|
132
|
+
|| this.durableMessageIds.has(dedupKey)
|
|
133
|
+
|| this.hasEnqueuedMessage(sessionKey, message.messageId, scope)) {
|
|
121
134
|
logger.debug(`[Queue] Duplicate message ${dedupKey}, skipping`);
|
|
122
135
|
return false;
|
|
123
136
|
}
|
|
@@ -125,17 +138,69 @@ export class MessageQueue {
|
|
|
125
138
|
setTimeout(() => this.recentMessageIds.delete(dedupKey), this.DEDUP_WINDOW);
|
|
126
139
|
return true;
|
|
127
140
|
}
|
|
128
|
-
|
|
141
|
+
rememberDurably(sessionKey, message, selfAID) {
|
|
142
|
+
if (!this.dedupPath || !message.messageId)
|
|
143
|
+
return;
|
|
144
|
+
this.durableMessageIds.set(this.dedupKey(sessionKey, message.messageId, selfAID ?? message.selfAID), Date.now());
|
|
145
|
+
this.persistDurableDedup();
|
|
146
|
+
}
|
|
147
|
+
restoreDurableDedup() {
|
|
148
|
+
if (!this.dedupPath || !fs.existsSync(this.dedupPath))
|
|
149
|
+
return;
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
try {
|
|
152
|
+
const raw = JSON.parse(fs.readFileSync(this.dedupPath, 'utf8'));
|
|
153
|
+
const entries = Array.isArray(raw)
|
|
154
|
+
? raw
|
|
155
|
+
: raw && typeof raw === 'object' && Array.isArray(raw.entries)
|
|
156
|
+
? raw.entries
|
|
157
|
+
: [];
|
|
158
|
+
let changed = false;
|
|
159
|
+
for (const entry of entries) {
|
|
160
|
+
const key = typeof entry?.key === 'string' ? entry.key : undefined;
|
|
161
|
+
const seenAt = typeof entry?.seenAt === 'number' ? entry.seenAt : undefined;
|
|
162
|
+
if (!key || seenAt === undefined || !Number.isFinite(seenAt) || now - seenAt > this.dedupTtlMs) {
|
|
163
|
+
changed = true;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
this.durableMessageIds.set(key, seenAt);
|
|
167
|
+
}
|
|
168
|
+
if (changed)
|
|
169
|
+
this.persistDurableDedup();
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
logger.warn(`[Queue] Failed to restore durable dedup ledger: ${error instanceof Error ? error.message : String(error)}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
persistDurableDedup() {
|
|
176
|
+
if (!this.dedupPath)
|
|
177
|
+
return;
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
for (const [key, seenAt] of this.durableMessageIds) {
|
|
180
|
+
if (now - seenAt > this.dedupTtlMs)
|
|
181
|
+
this.durableMessageIds.delete(key);
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
fs.mkdirSync(path.dirname(this.dedupPath), { recursive: true });
|
|
185
|
+
const tmp = `${this.dedupPath}.tmp-${process.pid}-${Date.now()}`;
|
|
186
|
+
fs.writeFileSync(tmp, JSON.stringify({ version: 1, updatedAt: now, entries: [...this.durableMessageIds].map(([key, seenAt]) => ({ key, seenAt })) }));
|
|
187
|
+
fs.renameSync(tmp, this.dedupPath);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
logger.warn(`[Queue] Failed to persist durable dedup ledger: ${error instanceof Error ? error.message : String(error)}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
hasEnqueuedMessage(sessionKey, messageId, selfAID) {
|
|
129
194
|
for (const [queueKey, queue] of this.queues) {
|
|
130
195
|
if (!this.matchesSession(queueKey, sessionKey))
|
|
131
196
|
continue;
|
|
132
|
-
if (queue.some(item => this.
|
|
197
|
+
if (queue.some(item => this.partsOf(item).some(part => this.partMatchesMessage(part, messageId, selfAID, queueKey))))
|
|
133
198
|
return true;
|
|
134
199
|
}
|
|
135
200
|
for (const [queueKey, active] of this.activeBatches) {
|
|
136
201
|
if (!this.matchesSession(queueKey, sessionKey))
|
|
137
202
|
continue;
|
|
138
|
-
if (this.
|
|
203
|
+
if (this.partsOf(active).some(part => this.partMatchesMessage(part, messageId, selfAID, queueKey)))
|
|
139
204
|
return true;
|
|
140
205
|
}
|
|
141
206
|
return false;
|
|
@@ -143,7 +208,7 @@ export class MessageQueue {
|
|
|
143
208
|
forgetProcessed(sessionKey, message) {
|
|
144
209
|
if (!message.messageId)
|
|
145
210
|
return;
|
|
146
|
-
this.recentMessageIds.delete(
|
|
211
|
+
this.recentMessageIds.delete(this.dedupKey(sessionKey, message.messageId, message.selfAID));
|
|
147
212
|
}
|
|
148
213
|
/**
|
|
149
214
|
* 检查队列 key 是否属于指定 sessionKey
|
|
@@ -160,6 +225,10 @@ export class MessageQueue {
|
|
|
160
225
|
const prefix = selfAID ? `${selfAID}::` : '';
|
|
161
226
|
return `${prefix}${sessionKey}::${normalized}`;
|
|
162
227
|
}
|
|
228
|
+
selfAIDFromQueueKey(queueKey) {
|
|
229
|
+
const parts = queueKey.split('::');
|
|
230
|
+
return parts.length >= 3 ? parts[0] : undefined;
|
|
231
|
+
}
|
|
163
232
|
sessionKeyFromQueueKey(queueKey) {
|
|
164
233
|
// 格式:selfAID::sessionKey::projectPath 或 sessionKey::projectPath(旧格式兼容)
|
|
165
234
|
const parts = queueKey.split('::');
|
|
@@ -181,6 +250,7 @@ export class MessageQueue {
|
|
|
181
250
|
role: item.role,
|
|
182
251
|
resolve: item.resolve,
|
|
183
252
|
reject: item.reject,
|
|
253
|
+
enqueuedAt: item.enqueuedAt ?? item.message.enqueuedAt,
|
|
184
254
|
};
|
|
185
255
|
}
|
|
186
256
|
itemFromPart(part) {
|
|
@@ -191,6 +261,7 @@ export class MessageQueue {
|
|
|
191
261
|
role: part.role,
|
|
192
262
|
resolve: part.resolve,
|
|
193
263
|
reject: part.reject,
|
|
264
|
+
enqueuedAt: part.enqueuedAt ?? part.message.enqueuedAt,
|
|
194
265
|
};
|
|
195
266
|
}
|
|
196
267
|
partsOf(item) {
|
|
@@ -207,21 +278,37 @@ export class MessageQueue {
|
|
|
207
278
|
.map(part => part.message.messageId)
|
|
208
279
|
.filter((id) => !!id);
|
|
209
280
|
}
|
|
281
|
+
partMatchesMessage(part, messageId, selfAID, queueKey) {
|
|
282
|
+
if (part.message.messageId !== messageId)
|
|
283
|
+
return false;
|
|
284
|
+
if (selfAID === undefined)
|
|
285
|
+
return true;
|
|
286
|
+
const messageScope = part.message.selfAID ?? (queueKey ? this.selfAIDFromQueueKey(queueKey) : undefined);
|
|
287
|
+
return messageScope === selfAID;
|
|
288
|
+
}
|
|
210
289
|
peerIdsFor(item) {
|
|
211
290
|
return this.partsOf(item).map(part => part.message.peerId);
|
|
212
291
|
}
|
|
213
|
-
serializePart(part,
|
|
292
|
+
serializePart(part, fallbackEnqueuedAt) {
|
|
293
|
+
const enqueuedAt = part.enqueuedAt ?? part.message.enqueuedAt ?? fallbackEnqueuedAt ?? Date.now();
|
|
294
|
+
const message = part.message.enqueuedAt === enqueuedAt
|
|
295
|
+
? part.message
|
|
296
|
+
: { ...part.message, enqueuedAt };
|
|
214
297
|
return {
|
|
215
|
-
message
|
|
298
|
+
message,
|
|
216
299
|
projectPath: part.projectPath,
|
|
217
300
|
agentName: part.agentName,
|
|
218
301
|
role: part.role,
|
|
219
302
|
enqueuedAt,
|
|
220
303
|
};
|
|
221
304
|
}
|
|
222
|
-
serializeItem(item,
|
|
305
|
+
serializeItem(item, fallbackEnqueuedAt) {
|
|
306
|
+
const enqueuedAt = item.enqueuedAt ?? item.message.enqueuedAt ?? fallbackEnqueuedAt ?? Date.now();
|
|
307
|
+
const message = item.message.enqueuedAt === enqueuedAt
|
|
308
|
+
? item.message
|
|
309
|
+
: { ...item.message, enqueuedAt };
|
|
223
310
|
return {
|
|
224
|
-
message
|
|
311
|
+
message,
|
|
225
312
|
projectPath: item.projectPath,
|
|
226
313
|
agentName: item.agentName,
|
|
227
314
|
role: item.role,
|
|
@@ -230,17 +317,25 @@ export class MessageQueue {
|
|
|
230
317
|
};
|
|
231
318
|
}
|
|
232
319
|
restoredPart(item) {
|
|
320
|
+
const enqueuedAt = item.enqueuedAt ?? item.message.enqueuedAt;
|
|
321
|
+
const message = enqueuedAt === undefined
|
|
322
|
+
? this.normalizeRestoredMessage(item.message)
|
|
323
|
+
: this.normalizeRestoredMessage({ ...item.message, enqueuedAt });
|
|
233
324
|
return {
|
|
234
|
-
message
|
|
325
|
+
message,
|
|
235
326
|
projectPath: item.projectPath,
|
|
236
327
|
agentName: item.agentName || DEFAULT_AGENT_NAME,
|
|
237
328
|
role: item.role,
|
|
238
329
|
resolve: () => { },
|
|
239
330
|
reject: () => { },
|
|
331
|
+
enqueuedAt,
|
|
240
332
|
};
|
|
241
333
|
}
|
|
242
334
|
restoreSubmittedPart(item) {
|
|
243
|
-
const
|
|
335
|
+
const sourceEnqueuedAt = item.enqueuedAt ?? item.message.enqueuedAt;
|
|
336
|
+
const source = this.normalizeRestoredMessage(sourceEnqueuedAt === undefined
|
|
337
|
+
? item.message
|
|
338
|
+
: { ...item.message, enqueuedAt: sourceEnqueuedAt });
|
|
244
339
|
const message = {
|
|
245
340
|
...source,
|
|
246
341
|
content: RESTART_RESUME_CONTENT,
|
|
@@ -272,6 +367,7 @@ export class MessageQueue {
|
|
|
272
367
|
role: item.role,
|
|
273
368
|
resolve: () => { },
|
|
274
369
|
reject: () => { },
|
|
370
|
+
enqueuedAt: sourceEnqueuedAt,
|
|
275
371
|
};
|
|
276
372
|
}
|
|
277
373
|
normalizeRestoredMessage(message) {
|
|
@@ -299,9 +395,53 @@ export class MessageQueue {
|
|
|
299
395
|
role: this.highestRole(parts),
|
|
300
396
|
resolve: () => parts.forEach(part => part.resolve()),
|
|
301
397
|
reject: (error) => parts.forEach(part => part.reject(error)),
|
|
398
|
+
enqueuedAt: this.earliestPartEnqueuedAt(parts),
|
|
302
399
|
parts,
|
|
303
400
|
};
|
|
304
401
|
}
|
|
402
|
+
firstEnqueuedAt(item) {
|
|
403
|
+
const parts = 'parts' in item && item.parts ? item.parts : [item];
|
|
404
|
+
let value;
|
|
405
|
+
for (const part of parts) {
|
|
406
|
+
const candidate = part.enqueuedAt ?? part.message.enqueuedAt;
|
|
407
|
+
if (candidate === undefined)
|
|
408
|
+
continue;
|
|
409
|
+
value = value === undefined ? candidate : Math.min(value, candidate);
|
|
410
|
+
}
|
|
411
|
+
return value;
|
|
412
|
+
}
|
|
413
|
+
earliestEnqueuedAt(items) {
|
|
414
|
+
let value;
|
|
415
|
+
for (const item of items) {
|
|
416
|
+
const candidate = this.firstEnqueuedAt(item);
|
|
417
|
+
if (candidate === undefined)
|
|
418
|
+
continue;
|
|
419
|
+
value = value === undefined ? candidate : Math.min(value, candidate);
|
|
420
|
+
}
|
|
421
|
+
return value;
|
|
422
|
+
}
|
|
423
|
+
earliestPartEnqueuedAt(parts) {
|
|
424
|
+
let value;
|
|
425
|
+
for (const part of parts) {
|
|
426
|
+
const candidate = part.enqueuedAt ?? part.message.enqueuedAt;
|
|
427
|
+
if (candidate === undefined)
|
|
428
|
+
continue;
|
|
429
|
+
value = value === undefined ? candidate : Math.min(value, candidate);
|
|
430
|
+
}
|
|
431
|
+
return value;
|
|
432
|
+
}
|
|
433
|
+
firstReceivedAt(items) {
|
|
434
|
+
let value;
|
|
435
|
+
for (const item of items) {
|
|
436
|
+
for (const part of this.partsOf(item)) {
|
|
437
|
+
const candidate = part.message.receivedAt ?? part.message.timestamp;
|
|
438
|
+
if (candidate === undefined)
|
|
439
|
+
continue;
|
|
440
|
+
value = value === undefined ? candidate : Math.min(value, candidate);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return value;
|
|
444
|
+
}
|
|
305
445
|
highestRole(items) {
|
|
306
446
|
const rank = {
|
|
307
447
|
none: 0,
|
|
@@ -431,19 +571,18 @@ export class MessageQueue {
|
|
|
431
571
|
try {
|
|
432
572
|
const buckets = [];
|
|
433
573
|
const activeBuckets = [];
|
|
434
|
-
const enqueuedAt = Date.now();
|
|
435
574
|
for (const [queueKey, queue] of this.queues.entries()) {
|
|
436
575
|
if (queue.length === 0)
|
|
437
576
|
continue;
|
|
438
577
|
buckets.push({
|
|
439
578
|
queueKey,
|
|
440
|
-
items: queue.map(item => this.serializeItem(item
|
|
579
|
+
items: queue.map(item => this.serializeItem(item)),
|
|
441
580
|
});
|
|
442
581
|
}
|
|
443
582
|
for (const [queueKey, item] of this.activeBatches.entries()) {
|
|
444
583
|
activeBuckets.push({
|
|
445
584
|
queueKey,
|
|
446
|
-
items: [this.serializeItem(item
|
|
585
|
+
items: [this.serializeItem(item)],
|
|
447
586
|
});
|
|
448
587
|
}
|
|
449
588
|
fs.mkdirSync(path.dirname(this.persistencePath), { recursive: true });
|
|
@@ -496,13 +635,15 @@ export class MessageQueue {
|
|
|
496
635
|
if (parts.length === 0)
|
|
497
636
|
continue;
|
|
498
637
|
const rawParts = parts
|
|
499
|
-
.sort((a, b) => (a.message.
|
|
638
|
+
.sort((a, b) => (a.message.receivedAt ?? a.message.timestamp ?? 0)
|
|
639
|
+
- (b.message.receivedAt ?? b.message.timestamp ?? 0));
|
|
500
640
|
for (const part of rawParts) {
|
|
501
641
|
const rawItem = this.itemFromPart(part);
|
|
502
642
|
queue.push(rawItem);
|
|
503
643
|
if (rawItem.message.messageId) {
|
|
504
|
-
const dedupKey =
|
|
644
|
+
const dedupKey = this.dedupKey(sessionKey, rawItem.message.messageId, rawItem.message.selfAID);
|
|
505
645
|
this.recentMessageIds.add(dedupKey);
|
|
646
|
+
this.durableMessageIds.set(dedupKey, Date.now());
|
|
506
647
|
setTimeout(() => this.recentMessageIds.delete(dedupKey), this.DEDUP_WINDOW);
|
|
507
648
|
}
|
|
508
649
|
}
|
|
@@ -538,13 +679,15 @@ export class MessageQueue {
|
|
|
538
679
|
: this.itemFromPart(first.message.handoffDelivery
|
|
539
680
|
? this.restoredPart(first)
|
|
540
681
|
: this.restoreSubmittedPart(first));
|
|
541
|
-
submittedItem.message.timestamp = first.message.timestamp;
|
|
682
|
+
submittedItem.message.timestamp = first.message.receivedAt ?? first.message.timestamp;
|
|
683
|
+
submittedItem.message.enqueuedAt = first.enqueuedAt ?? first.message.enqueuedAt;
|
|
542
684
|
queue.push(submittedItem);
|
|
543
685
|
for (const part of rawParts) {
|
|
544
686
|
if (!part.message.messageId)
|
|
545
687
|
continue;
|
|
546
|
-
const dedupKey =
|
|
688
|
+
const dedupKey = this.dedupKey(sessionKey, part.message.messageId, part.message.selfAID);
|
|
547
689
|
this.recentMessageIds.add(dedupKey);
|
|
690
|
+
this.durableMessageIds.set(dedupKey, Date.now());
|
|
548
691
|
setTimeout(() => this.recentMessageIds.delete(dedupKey), this.DEDUP_WINDOW);
|
|
549
692
|
}
|
|
550
693
|
restored += rawParts.length;
|
|
@@ -564,6 +707,27 @@ export class MessageQueue {
|
|
|
564
707
|
logger.warn('[Queue] Ignoring unsupported persisted queue file');
|
|
565
708
|
return 0;
|
|
566
709
|
}
|
|
710
|
+
// Version-1 snapshots did not carry per-item enqueue times. Preserve
|
|
711
|
+
// their last durable snapshot time instead of making restored history
|
|
712
|
+
// look newly queued at the moment of daemon restart.
|
|
713
|
+
const legacyEnqueuedAt = typeof parsed.updatedAt === 'number' && Number.isFinite(parsed.updatedAt)
|
|
714
|
+
? parsed.updatedAt
|
|
715
|
+
: Date.now();
|
|
716
|
+
for (const bucket of [...parsed.queues, ...(Array.isArray(parsed.active) ? parsed.active : [])]) {
|
|
717
|
+
for (const item of bucket.items ?? []) {
|
|
718
|
+
if (!item?.message)
|
|
719
|
+
continue;
|
|
720
|
+
if (item.enqueuedAt === undefined && item.message?.enqueuedAt === undefined)
|
|
721
|
+
item.enqueuedAt = legacyEnqueuedAt;
|
|
722
|
+
for (const part of item.parts ?? []) {
|
|
723
|
+
if (!part?.message)
|
|
724
|
+
continue;
|
|
725
|
+
if (part.enqueuedAt === undefined && part.message?.enqueuedAt === undefined) {
|
|
726
|
+
part.enqueuedAt = item.enqueuedAt ?? legacyEnqueuedAt;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
567
731
|
const restoredKeys = new Set();
|
|
568
732
|
if (Array.isArray(parsed.active)) {
|
|
569
733
|
for (const bucket of parsed.active) {
|
|
@@ -574,6 +738,7 @@ export class MessageQueue {
|
|
|
574
738
|
restored += this.restoreBucket(bucket, restoredKeys);
|
|
575
739
|
}
|
|
576
740
|
this.persistQueuesImmediate();
|
|
741
|
+
this.persistDurableDedup();
|
|
577
742
|
if (restored > 0) {
|
|
578
743
|
logger.info(`[Queue] Restored ${restored} persisted message(s) from ${this.persistencePath}`);
|
|
579
744
|
}
|
|
@@ -617,8 +782,13 @@ export class MessageQueue {
|
|
|
617
782
|
if (options?.signal?.aborted) {
|
|
618
783
|
return Promise.reject(abortError(options.signal.reason));
|
|
619
784
|
}
|
|
785
|
+
if (options?.selfAID && message.selfAID !== options.selfAID) {
|
|
786
|
+
// The queue option is the authoritative physical-copy scope. A shared
|
|
787
|
+
// broadcast object must be cloned per agent before dedup/cancel logic.
|
|
788
|
+
message = { ...message, selfAID: options.selfAID };
|
|
789
|
+
}
|
|
620
790
|
// 消息去重检查
|
|
621
|
-
if (!this.shouldProcess(sessionKey, message)) {
|
|
791
|
+
if (!this.shouldProcess(sessionKey, message, options?.selfAID)) {
|
|
622
792
|
options?.onPersisted?.();
|
|
623
793
|
return Promise.resolve();
|
|
624
794
|
}
|
|
@@ -676,7 +846,18 @@ export class MessageQueue {
|
|
|
676
846
|
const queue = this.queues.get(queueKey);
|
|
677
847
|
const queuedResolve = options?.resolveOnPersist ? cleanupAbort : resolveOnce;
|
|
678
848
|
const queuedReject = options?.resolveOnPersist ? cleanupAbort : rejectOnce;
|
|
679
|
-
|
|
849
|
+
// Capture the physical enqueue instant once. It must not be regenerated
|
|
850
|
+
// on every persistence snapshot, otherwise old messages appear fresh.
|
|
851
|
+
const enqueuedAt = Date.now();
|
|
852
|
+
// Clone at the physical queue boundary. The same inbound object may be
|
|
853
|
+
// broadcast to multiple agents; mutating it would make all copies share
|
|
854
|
+
// the last queue's enqueue timestamp.
|
|
855
|
+
message = {
|
|
856
|
+
...message,
|
|
857
|
+
enqueuedAt,
|
|
858
|
+
timestamp: message.receivedAt ?? message.timestamp ?? enqueuedAt,
|
|
859
|
+
};
|
|
860
|
+
const queuedItem = { message, projectPath, agentName, role: options?.role, resolve: queuedResolve, reject: queuedReject, enqueuedAt };
|
|
680
861
|
queue.push(queuedItem);
|
|
681
862
|
this.logicalQueue.enqueue(queueKey, message); // 通知逻辑队列记录出队顺序
|
|
682
863
|
abortHandler = () => {
|
|
@@ -693,7 +874,7 @@ export class MessageQueue {
|
|
|
693
874
|
this.persistQueuesImmediate();
|
|
694
875
|
}
|
|
695
876
|
else if (message.messageId) {
|
|
696
|
-
void this.cancelActiveAndWait(message.messageId).finally(() => {
|
|
877
|
+
void this.cancelActiveAndWait(message.messageId, message.selfAID).finally(() => {
|
|
697
878
|
this.forgetProcessed(sessionKey, message);
|
|
698
879
|
settleReject(error);
|
|
699
880
|
});
|
|
@@ -715,6 +896,10 @@ export class MessageQueue {
|
|
|
715
896
|
rejectOnce(new Error('failed to persist message queue'));
|
|
716
897
|
return;
|
|
717
898
|
}
|
|
899
|
+
// A duplicate must not re-enter a runner after the queue entry has
|
|
900
|
+
// survived its write-ahead snapshot. Keep this separate from the queue
|
|
901
|
+
// snapshot because the latter is removed as soon as the task completes.
|
|
902
|
+
this.rememberDurably(sessionKey, message, options?.selfAID);
|
|
718
903
|
try {
|
|
719
904
|
options?.onPersisted?.();
|
|
720
905
|
}
|
|
@@ -851,6 +1036,7 @@ export class MessageQueue {
|
|
|
851
1036
|
role: this.highestRole(rawParts),
|
|
852
1037
|
resolve: () => rawParts.forEach(part => part.resolve()),
|
|
853
1038
|
reject: (error) => rawParts.forEach(part => part.reject(error)),
|
|
1039
|
+
enqueuedAt: this.earliestPartEnqueuedAt(rawParts),
|
|
854
1040
|
parts: rawParts,
|
|
855
1041
|
};
|
|
856
1042
|
const batchRole = this.commonRole(rawItems);
|
|
@@ -866,6 +1052,7 @@ export class MessageQueue {
|
|
|
866
1052
|
const activeState = {
|
|
867
1053
|
baseagent: merged.message.baseagent,
|
|
868
1054
|
peerId: this.uniquePeerId(rawItems),
|
|
1055
|
+
selfAID: merged.message.selfAID ?? this.selfAIDFromQueueKey(queueKey),
|
|
869
1056
|
messageIds: activeMessageIds,
|
|
870
1057
|
};
|
|
871
1058
|
this.activeStates.set(queueKey, activeState);
|
|
@@ -991,7 +1178,7 @@ export class MessageQueue {
|
|
|
991
1178
|
const parts = this.partsOf(item);
|
|
992
1179
|
let ts = Number.POSITIVE_INFINITY;
|
|
993
1180
|
for (const part of parts)
|
|
994
|
-
ts = Math.min(ts, part.message.timestamp ?? 0);
|
|
1181
|
+
ts = Math.min(ts, part.message.receivedAt ?? part.message.timestamp ?? 0);
|
|
995
1182
|
return Number.isFinite(ts) ? ts : 0;
|
|
996
1183
|
}
|
|
997
1184
|
/**
|
|
@@ -1073,6 +1260,8 @@ export class MessageQueue {
|
|
|
1073
1260
|
sameDevice: m.sameDevice, sameNetwork: m.sameNetwork, sameEgressIp: m.sameEgressIp,
|
|
1074
1261
|
encrypted: m.encrypted,
|
|
1075
1262
|
content: m.content, timestamp: m.timestamp,
|
|
1263
|
+
receivedAt: m.receivedAt,
|
|
1264
|
+
gatewaySeq: m.gatewaySeq,
|
|
1076
1265
|
causation: m.causation,
|
|
1077
1266
|
images: m.images && m.images.length > 0 ? m.images : undefined,
|
|
1078
1267
|
mentions: m.mentions && m.mentions.length > 0 ? m.mentions : undefined,
|
|
@@ -1090,6 +1279,8 @@ export class MessageQueue {
|
|
|
1090
1279
|
break;
|
|
1091
1280
|
}
|
|
1092
1281
|
}
|
|
1282
|
+
const firstReceivedAt = this.firstReceivedAt(items);
|
|
1283
|
+
const earliestEnqueuedAt = this.earliestEnqueuedAt(items);
|
|
1093
1284
|
const merged = {
|
|
1094
1285
|
...last.message,
|
|
1095
1286
|
content: contents.join('\n'),
|
|
@@ -1098,6 +1289,9 @@ export class MessageQueue {
|
|
|
1098
1289
|
mentions: allMentions.length > 0 ? allMentions : undefined,
|
|
1099
1290
|
messageId: latestMessageId,
|
|
1100
1291
|
sourceMessageIds: sourceMessageIds.length > 0 ? sourceMessageIds : undefined,
|
|
1292
|
+
receivedAt: firstReceivedAt,
|
|
1293
|
+
timestamp: firstReceivedAt ?? last.message.timestamp,
|
|
1294
|
+
enqueuedAt: earliestEnqueuedAt,
|
|
1101
1295
|
// 密文优先:合并批次内任一条密文,则整轮(interactive 自动回复的单值出站)按密文。
|
|
1102
1296
|
// 任一条 encrypted===true 即 true;否则若有明确的 false 则 false;全 undefined(非 aun)则 undefined。
|
|
1103
1297
|
encrypted: subMessages.some(s => s.encrypted === true)
|
|
@@ -1122,6 +1316,7 @@ export class MessageQueue {
|
|
|
1122
1316
|
agentName: last.agentName,
|
|
1123
1317
|
resolve: () => { }, // 由调用方管理
|
|
1124
1318
|
reject: () => { },
|
|
1319
|
+
enqueuedAt: earliestEnqueuedAt,
|
|
1125
1320
|
};
|
|
1126
1321
|
}
|
|
1127
1322
|
getQueueLength(sessionKey) {
|
|
@@ -1157,14 +1352,14 @@ export class MessageQueue {
|
|
|
1157
1352
|
}
|
|
1158
1353
|
return false;
|
|
1159
1354
|
}
|
|
1160
|
-
cancel(messageId) {
|
|
1161
|
-
for (const queue of this.queues.
|
|
1162
|
-
const idx = queue.findIndex(q => this.
|
|
1355
|
+
cancel(messageId, selfAID) {
|
|
1356
|
+
for (const [queueKey, queue] of this.queues.entries()) {
|
|
1357
|
+
const idx = queue.findIndex(q => this.partsOf(q).some(part => this.partMatchesMessage(part, messageId, selfAID, queueKey)));
|
|
1163
1358
|
if (idx === -1)
|
|
1164
1359
|
continue;
|
|
1165
1360
|
const target = queue[idx];
|
|
1166
1361
|
const parts = this.partsOf(target);
|
|
1167
|
-
const partIdx = parts.findIndex(part => part
|
|
1362
|
+
const partIdx = parts.findIndex(part => this.partMatchesMessage(part, messageId, selfAID, queueKey));
|
|
1168
1363
|
if (partIdx === -1)
|
|
1169
1364
|
continue;
|
|
1170
1365
|
if (parts.length === 1) {
|
|
@@ -1186,26 +1381,31 @@ export class MessageQueue {
|
|
|
1186
1381
|
* 撤回正在执行的消息:如果 messageId 正在处理中,触发 interrupt。
|
|
1187
1382
|
* @returns true 如果找到并触发了中断
|
|
1188
1383
|
*/
|
|
1189
|
-
cancelActive(messageId) {
|
|
1190
|
-
const cancellation = this.beginActiveCancellation(messageId);
|
|
1384
|
+
cancelActive(messageId, selfAID) {
|
|
1385
|
+
const cancellation = this.beginActiveCancellation(messageId, selfAID);
|
|
1191
1386
|
if (cancellation.barrier)
|
|
1192
1387
|
void cancellation.barrier;
|
|
1193
1388
|
return cancellation.found;
|
|
1194
1389
|
}
|
|
1195
1390
|
/** Cancel an active message and wait until the runner interrupt barrier settles. */
|
|
1196
|
-
async cancelActiveAndWait(messageId) {
|
|
1197
|
-
const cancellation = this.beginActiveCancellation(messageId);
|
|
1391
|
+
async cancelActiveAndWait(messageId, selfAID) {
|
|
1392
|
+
const cancellation = this.beginActiveCancellation(messageId, selfAID);
|
|
1198
1393
|
await cancellation.barrier;
|
|
1199
1394
|
return cancellation.found;
|
|
1200
1395
|
}
|
|
1201
|
-
beginActiveCancellation(messageId) {
|
|
1396
|
+
beginActiveCancellation(messageId, selfAID) {
|
|
1202
1397
|
for (const [queueKey, activeState] of this.activeStates.entries()) {
|
|
1203
1398
|
if (!activeState.messageIds.has(messageId))
|
|
1204
1399
|
continue;
|
|
1400
|
+
if (selfAID !== undefined && activeState.selfAID !== selfAID)
|
|
1401
|
+
continue;
|
|
1205
1402
|
const active = this.activeBatches.get(queueKey);
|
|
1206
|
-
if (active && this.
|
|
1403
|
+
if (active && !this.partsOf(active).some(part => this.partMatchesMessage(part, messageId, selfAID, queueKey))) {
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
if (active && this.partsOf(active).some(part => this.partMatchesMessage(part, messageId, selfAID, queueKey))) {
|
|
1207
1407
|
const parts = this.partsOf(active);
|
|
1208
|
-
const remaining = parts.filter(part => part
|
|
1408
|
+
const remaining = parts.filter(part => !this.partMatchesMessage(part, messageId, selfAID, queueKey));
|
|
1209
1409
|
if (remaining.length === 0) {
|
|
1210
1410
|
this.activeBatches.delete(queueKey);
|
|
1211
1411
|
this.processingStartTime.delete(queueKey);
|
|
@@ -1439,9 +1639,14 @@ export class MessageQueue {
|
|
|
1439
1639
|
buildSnapshot(status, queueKey, sessionKey, item) {
|
|
1440
1640
|
const { channelType, channelId } = this.parseSessionKey(sessionKey);
|
|
1441
1641
|
const startTime = this.processingStartTime.get(queueKey);
|
|
1642
|
+
const receivedAt = item.message.receivedAt ?? item.message.timestamp;
|
|
1643
|
+
const enqueuedAt = this.firstEnqueuedAt(item);
|
|
1442
1644
|
const elapsedMs = status === 'active' && startTime != null
|
|
1443
1645
|
? Date.now() - startTime
|
|
1444
1646
|
: undefined;
|
|
1647
|
+
const queueWaitMs = enqueuedAt == null
|
|
1648
|
+
? undefined
|
|
1649
|
+
: Math.max(0, (status === 'active' && startTime != null ? startTime : Date.now()) - enqueuedAt);
|
|
1445
1650
|
return {
|
|
1446
1651
|
status,
|
|
1447
1652
|
sessionKey,
|
|
@@ -1451,9 +1656,26 @@ export class MessageQueue {
|
|
|
1451
1656
|
peerName: item.message.peerName,
|
|
1452
1657
|
preview: this.truncatePreview(item.message.content),
|
|
1453
1658
|
messageId: item.message.messageId,
|
|
1659
|
+
receivedAt,
|
|
1660
|
+
gatewaySeq: item.message.gatewaySeq,
|
|
1661
|
+
enqueuedAt,
|
|
1662
|
+
queueWaitMs,
|
|
1663
|
+
processingStartedAt: status === 'active' ? startTime : undefined,
|
|
1454
1664
|
elapsedMs,
|
|
1455
1665
|
};
|
|
1456
1666
|
}
|
|
1667
|
+
matchesTimeRange(item, range) {
|
|
1668
|
+
if (!range || (range.after === undefined && range.before === undefined))
|
|
1669
|
+
return true;
|
|
1670
|
+
const value = item.message.receivedAt ?? item.message.timestamp ?? item.message.enqueuedAt ?? this.firstEnqueuedAt(item);
|
|
1671
|
+
if (value === undefined)
|
|
1672
|
+
return false;
|
|
1673
|
+
if (range.after !== undefined && value <= range.after)
|
|
1674
|
+
return false;
|
|
1675
|
+
if (range.before !== undefined && value >= range.before)
|
|
1676
|
+
return false;
|
|
1677
|
+
return true;
|
|
1678
|
+
}
|
|
1457
1679
|
/**
|
|
1458
1680
|
* 通过 human-readable sessionKey 反查 sessionId(cli interrupt 用)
|
|
1459
1681
|
*/
|
|
@@ -1469,7 +1691,7 @@ export class MessageQueue {
|
|
|
1469
1691
|
* @param sessionId - Session.id(meta_YYYYMMDD_TS)
|
|
1470
1692
|
* @returns 仅返回 pending 状态的消息
|
|
1471
1693
|
*/
|
|
1472
|
-
getQueueItemsBySession(sessionId) {
|
|
1694
|
+
getQueueItemsBySession(sessionId, range) {
|
|
1473
1695
|
const result = [];
|
|
1474
1696
|
for (const [queueKey, queue] of this.queues.entries()) {
|
|
1475
1697
|
if (!this.matchesSession(queueKey, sessionId))
|
|
@@ -1477,6 +1699,8 @@ export class MessageQueue {
|
|
|
1477
1699
|
const sessionKey = this.queueKeyToSessionKey.get(queueKey)
|
|
1478
1700
|
|| this.sessionKeyFromQueueKey(queueKey);
|
|
1479
1701
|
for (const item of queue) {
|
|
1702
|
+
if (!this.matchesTimeRange(item, range))
|
|
1703
|
+
continue;
|
|
1480
1704
|
result.push(this.buildSnapshot('pending', queueKey, sessionKey, item));
|
|
1481
1705
|
}
|
|
1482
1706
|
}
|
|
@@ -1487,13 +1711,15 @@ export class MessageQueue {
|
|
|
1487
1711
|
* @param agentName - agent 内部名称
|
|
1488
1712
|
* @returns 返回所有状态(active + pending)的消息
|
|
1489
1713
|
*/
|
|
1490
|
-
getQueueItemsByAgent(agentName) {
|
|
1714
|
+
getQueueItemsByAgent(agentName, range) {
|
|
1491
1715
|
const result = [];
|
|
1492
1716
|
// active 项
|
|
1493
1717
|
for (const [queueKey, item] of this.activeBatches.entries()) {
|
|
1494
1718
|
const itemAgent = item.agentName || DEFAULT_AGENT_NAME;
|
|
1495
1719
|
if (itemAgent !== agentName)
|
|
1496
1720
|
continue;
|
|
1721
|
+
if (!this.matchesTimeRange(item, range))
|
|
1722
|
+
continue;
|
|
1497
1723
|
const sessionKey = this.queueKeyToSessionKey.get(queueKey)
|
|
1498
1724
|
|| this.sessionKeyFromQueueKey(queueKey);
|
|
1499
1725
|
result.push(this.buildSnapshot('active', queueKey, sessionKey, item));
|
|
@@ -1504,6 +1730,8 @@ export class MessageQueue {
|
|
|
1504
1730
|
const itemAgent = item.agentName || DEFAULT_AGENT_NAME;
|
|
1505
1731
|
if (itemAgent !== agentName)
|
|
1506
1732
|
continue;
|
|
1733
|
+
if (!this.matchesTimeRange(item, range))
|
|
1734
|
+
continue;
|
|
1507
1735
|
const sessionKey = this.queueKeyToSessionKey.get(queueKey)
|
|
1508
1736
|
|| this.sessionKeyFromQueueKey(queueKey);
|
|
1509
1737
|
result.push(this.buildSnapshot('pending', queueKey, sessionKey, item));
|