evolcore 0.0.20 → 0.0.22
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 +65 -0
- package/README.md +58 -9
- package/bin/codex-managed-hook.mjs +3 -0
- package/bin/install-codex-managed-hooks.mjs +3 -1
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +393 -108
- package/dist/agents/codex-app-server-client.js +41 -7
- package/dist/agents/codex-runner.js +1292 -220
- package/dist/agents/ecagent-runner.js +171 -61
- package/dist/agents/gemini-runner.js +130 -30
- package/dist/agents/request-identity.js +25 -0
- package/dist/agents/runner-types.js +19 -0
- package/dist/aun/aid/agentmd.js +59 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +72 -6
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +5 -0
- package/dist/aun/outbox.js +189 -80
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +618 -123
- package/dist/channels/daemon.js +6 -1
- package/dist/cli/agent-command.js +4 -3
- package/dist/cli/agent.js +66 -56
- package/dist/cli/aun-commands.js +177 -42
- package/dist/cli/command-log.js +10 -11
- package/dist/cli/contact.js +1 -0
- package/dist/cli/daemon-commands.js +98 -123
- package/dist/cli/init.js +27 -15
- package/dist/cli/task-context.js +50 -0
- package/dist/cli/trigger-command.js +14 -5
- package/dist/cli/watch-logs.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +19 -5
- package/dist/config/config-manager.js +167 -22
- package/dist/config/contact-book-store.js +25 -3
- package/dist/config/contact-operation-service.js +32 -1
- package/dist/config/contact-request-service.js +44 -0
- package/dist/config/daemon-services.js +186 -0
- package/dist/config/gateway-config.js +20 -9
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +151 -9
- package/dist/core/agent-application-service.js +279 -0
- package/dist/core/audit/log-integrity.js +102 -0
- package/dist/core/auth/agent-delegation.js +43 -1
- package/dist/core/auth/auth-gateway.js +41 -4
- package/dist/core/auth/authorization-audit.js +216 -8
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-messages.js +8 -0
- package/dist/core/bootstrap-service.js +99 -27
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +56 -16
- package/dist/core/command/command-handler.js +311 -44
- package/dist/core/command/connect-menu.js +3 -4
- package/dist/core/command/group-menu.js +5 -7
- package/dist/core/command/menu-handler.js +288 -80
- package/dist/core/command/menu-protocol.js +1 -1
- package/dist/core/command/role-menu.js +21 -11
- package/dist/core/command/slash-gate.js +85 -18
- package/dist/core/command/slash-handler.js +377 -36
- package/dist/core/data-migration.js +11 -1
- package/dist/core/event-catalog.js +37 -0
- package/dist/core/evolagent.js +4 -0
- package/dist/core/handoff/dispatcher.js +4 -0
- package/dist/core/handoff/runtime.js +33 -3
- package/dist/core/handoff/store.js +32 -9
- package/dist/core/inference/text-inference.js +7 -15
- package/dist/core/message/im-renderer.js +90 -87
- package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
- package/dist/core/message/message-bridge.js +184 -12
- package/dist/core/message/message-log.js +47 -7
- package/dist/core/message/message-queue.js +227 -16
- package/dist/core/message/message-utils.js +12 -5
- package/dist/core/message/response-engine.js +658 -109
- package/dist/core/message/send-receipt.js +1 -0
- package/dist/core/message/stream-debouncer.js +9 -2
- package/dist/core/model/model-catalog.js +23 -15
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/approval-gateway.js +180 -6
- package/dist/core/permission/ec-command-parser.js +627 -69
- package/dist/core/permission/mode.js +18 -3
- package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
- package/dist/core/permission/readonly-shell-query.js +263 -9
- package/dist/core/permission/sandbox-runtime.js +159 -1
- package/dist/core/permission/tool-error-code.js +12 -0
- package/dist/core/permission/tool-policy.js +618 -23
- package/dist/core/session/session-fs-store.js +154 -5
- package/dist/core/session/session-manager.js +329 -30
- package/dist/core/session/session-renew.js +37 -13
- package/dist/core/session/session-turn-coordinator.js +16 -5
- package/dist/eck/kit-renderer.js +1 -1
- package/dist/index.js +316 -50
- package/dist/ipc.js +459 -29
- package/dist/paths.js +82 -7
- package/dist/response-system/context-builder.js +1 -7
- package/dist/response-system/engines/v1/proactive-flow.js +7 -2
- package/dist/stats/price-resolver.js +4 -0
- package/dist/trigger/anomaly-store.js +1 -0
- package/dist/trigger/feedback.js +70 -7
- package/dist/trigger/history.js +79 -4
- package/dist/trigger/legacy-session-history.js +2 -2
- package/dist/trigger/parser.js +13 -3
- package/dist/trigger/scheduler.js +20 -3
- package/dist/trigger/validation.js +6 -1
- package/dist/utils/atomic-write.js +27 -0
- package/dist/utils/ecweb-utils.js +16 -2
- package/dist/utils/error-utils.js +4 -1
- package/dist/utils/logger.js +30 -6
- package/dist/utils/process-tree-stats.js +24 -4
- package/dist/utils/process-tree-worker.js +31 -0
- package/dist/utils/project-path.js +1 -2
- package/dist/utils/tool-summary.js +59 -0
- package/dist/utils/windows-shell-trust.js +201 -0
- package/kits/docs/INDEX.md +1 -1
- package/kits/docs/evolcore/INDEX.md +3 -3
- package/kits/docs/evolcore/agent-create.md +146 -0
- package/kits/docs/evolcore/agent.md +6 -0
- package/kits/docs/evolcore/contact.md +7 -1
- package/kits/docs/evolcore/group-collaboration.md +251 -0
- package/kits/docs/evolcore/group-rules.md +1 -19
- package/kits/docs/evolcore/group.md +3 -1
- package/kits/docs/evolcore/msg.md +16 -0
- package/kits/docs/evolcore/trigger.md +6 -3
- package/kits/docs/prompt-loading-architecture.md +6 -0
- package/kits/eck_message_manifest.json +6 -6
- package/kits/schemas/_meta.json +7 -4
- package/kits/schemas/agent-config.schema.11.json +13 -0
- package/kits/schemas/agent-config.schema.12.json +427 -0
- package/kits/schemas/daemon.schema.5.json +0 -1
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +15 -3
- package/kits/schemas/migrations/README.md +3 -1
- package/kits/schemas/relation-config.schema.8.json +13 -0
- package/kits/schemas/role-config.schema.1.json +1 -2
- package/kits/schemas/single-session.schema.3.json +32 -0
- package/kits/templates/message-fragments/item.md +1 -1
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +2 -1
- package/kits/templates/system-fragments/commands.md +2 -2
- package/package.json +6 -3
- package/skills/eclink/SKILL.md +2 -0
- package/dist/config/aun-gateway-config.js +0 -2
|
@@ -3,6 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
import { logger } from '../../utils/logger.js';
|
|
4
4
|
import { LogicalQueueBridge } from './logical-queue-bridge.js';
|
|
5
5
|
import { normalizeCausation } from '../causation/context.js';
|
|
6
|
+
import { KeyedFairMutex } from '../handoff/mutex.js';
|
|
6
7
|
const DEFAULT_AGENT_NAME = '<unknown>';
|
|
7
8
|
const RESTART_RESUME_CONTENT = 'evolcore 服务已重启,请继续之前未完成的任务。';
|
|
8
9
|
export class MessageQueue {
|
|
@@ -10,6 +11,9 @@ export class MessageQueue {
|
|
|
10
11
|
processing = new Set();
|
|
11
12
|
processingAgent = new Map(); // queueKey → agentName(处理中项目的 agent)
|
|
12
13
|
externalLocks = new Map();
|
|
14
|
+
prequeueReservations = new Map();
|
|
15
|
+
/** Serializes fast session commands while retaining the existing dequeue gate. */
|
|
16
|
+
sessionBarrierMutex = new KeyedFairMutex();
|
|
13
17
|
handler;
|
|
14
18
|
activeStates = new Map(); // queueKey → active task state
|
|
15
19
|
interruptCallback;
|
|
@@ -32,6 +36,7 @@ export class MessageQueue {
|
|
|
32
36
|
shutdownPromise;
|
|
33
37
|
/** 逻辑队列桥接:管理出队顺序(响应模式可注入自定义策略,默认 FIFO) */
|
|
34
38
|
logicalQueue = new LogicalQueueBridge();
|
|
39
|
+
enqueueSequence = 0;
|
|
35
40
|
constructor(handler, options) {
|
|
36
41
|
this.handler = handler;
|
|
37
42
|
this.persistencePath = options?.persistencePath;
|
|
@@ -83,6 +88,17 @@ export class MessageQueue {
|
|
|
83
88
|
this.markActiveTerminal(event.sessionId, event.reason || 'interrupted');
|
|
84
89
|
}
|
|
85
90
|
});
|
|
91
|
+
subscribe.call(eventBus, 'task:started', event => {
|
|
92
|
+
if (!('sessionId' in event) || !event.sessionId)
|
|
93
|
+
return;
|
|
94
|
+
const taskEvent = event;
|
|
95
|
+
for (const [queueKey, state] of this.activeStates) {
|
|
96
|
+
if (this.matchesSession(queueKey, taskEvent.sessionId)) {
|
|
97
|
+
state.taskId = taskEvent.taskId;
|
|
98
|
+
state.generation = taskEvent.generation;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
86
102
|
subscribe.call(eventBus, 'task:completed', event => {
|
|
87
103
|
if ('sessionId' in event && event.sessionId)
|
|
88
104
|
this.markActiveTerminal(event.sessionId, 'completed');
|
|
@@ -138,6 +154,42 @@ export class MessageQueue {
|
|
|
138
154
|
setTimeout(() => this.recentMessageIds.delete(dedupKey), this.DEDUP_WINDOW);
|
|
139
155
|
return true;
|
|
140
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Claim a stable inbound message for a fast command that bypasses the
|
|
159
|
+
* normal queue. The claim uses exactly the same durable ledger and key as
|
|
160
|
+
* ordinary enqueue deduplication, but deliberately does not enqueue or
|
|
161
|
+
* affect queue scheduling.
|
|
162
|
+
*/
|
|
163
|
+
claimInbound(sessionKey, messageId, selfAID) {
|
|
164
|
+
const normalizedMessageId = messageId.trim();
|
|
165
|
+
if (!normalizedMessageId) {
|
|
166
|
+
const error = new Error('inbound claim requires a stable messageId');
|
|
167
|
+
error.code = 'INBOUND_MESSAGE_ID_REQUIRED';
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
const key = this.dedupKey(sessionKey, normalizedMessageId, selfAID);
|
|
171
|
+
if (this.recentMessageIds.has(key)
|
|
172
|
+
|| this.durableMessageIds.has(key)
|
|
173
|
+
|| this.hasEnqueuedMessage(sessionKey, normalizedMessageId, selfAID)) {
|
|
174
|
+
return 'duplicate';
|
|
175
|
+
}
|
|
176
|
+
if (!this.dedupPath) {
|
|
177
|
+
const error = new Error('durable inbound dedup is unavailable');
|
|
178
|
+
error.code = 'INBOUND_DEDUP_UNAVAILABLE';
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
const seenAt = Date.now();
|
|
182
|
+
this.durableMessageIds.set(key, seenAt);
|
|
183
|
+
if (!this.persistDurableDedup()) {
|
|
184
|
+
this.durableMessageIds.delete(key);
|
|
185
|
+
const error = new Error('failed to persist inbound dedup claim');
|
|
186
|
+
error.code = 'INBOUND_DEDUP_PERSIST_FAILED';
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
this.recentMessageIds.add(key);
|
|
190
|
+
setTimeout(() => this.recentMessageIds.delete(key), this.DEDUP_WINDOW);
|
|
191
|
+
return 'claimed';
|
|
192
|
+
}
|
|
141
193
|
rememberDurably(sessionKey, message, selfAID) {
|
|
142
194
|
if (!this.dedupPath || !message.messageId)
|
|
143
195
|
return;
|
|
@@ -174,20 +226,64 @@ export class MessageQueue {
|
|
|
174
226
|
}
|
|
175
227
|
persistDurableDedup() {
|
|
176
228
|
if (!this.dedupPath)
|
|
177
|
-
return;
|
|
229
|
+
return false;
|
|
178
230
|
const now = Date.now();
|
|
179
231
|
for (const [key, seenAt] of this.durableMessageIds) {
|
|
180
232
|
if (now - seenAt > this.dedupTtlMs)
|
|
181
233
|
this.durableMessageIds.delete(key);
|
|
182
234
|
}
|
|
235
|
+
const tmp = `${this.dedupPath}.tmp-${process.pid}-${Date.now()}-${this.enqueueSequence}`;
|
|
236
|
+
let fd;
|
|
183
237
|
try {
|
|
184
238
|
fs.mkdirSync(path.dirname(this.dedupPath), { recursive: true });
|
|
185
|
-
const
|
|
186
|
-
|
|
239
|
+
const content = Buffer.from(JSON.stringify({
|
|
240
|
+
version: 1,
|
|
241
|
+
updatedAt: now,
|
|
242
|
+
entries: [...this.durableMessageIds].map(([key, seenAt]) => ({ key, seenAt })),
|
|
243
|
+
}));
|
|
244
|
+
fd = fs.openSync(tmp, 'w');
|
|
245
|
+
let offset = 0;
|
|
246
|
+
while (offset < content.length) {
|
|
247
|
+
const written = fs.writeSync(fd, content, offset, content.length - offset, null);
|
|
248
|
+
if (written <= 0)
|
|
249
|
+
throw new Error('dedup ledger write made no progress');
|
|
250
|
+
offset += written;
|
|
251
|
+
}
|
|
252
|
+
fs.fsyncSync(fd);
|
|
253
|
+
fs.closeSync(fd);
|
|
254
|
+
fd = undefined;
|
|
187
255
|
fs.renameSync(tmp, this.dedupPath);
|
|
256
|
+
const persistedFd = fs.openSync(this.dedupPath, 'r');
|
|
257
|
+
try {
|
|
258
|
+
fs.fsyncSync(persistedFd);
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
fs.closeSync(persistedFd);
|
|
262
|
+
}
|
|
263
|
+
if (process.platform !== 'win32') {
|
|
264
|
+
const directoryFd = fs.openSync(path.dirname(this.dedupPath), 'r');
|
|
265
|
+
try {
|
|
266
|
+
fs.fsyncSync(directoryFd);
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
fs.closeSync(directoryFd);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return true;
|
|
188
273
|
}
|
|
189
274
|
catch (error) {
|
|
190
275
|
logger.warn(`[Queue] Failed to persist durable dedup ledger: ${error instanceof Error ? error.message : String(error)}`);
|
|
276
|
+
if (fd !== undefined) {
|
|
277
|
+
try {
|
|
278
|
+
fs.closeSync(fd);
|
|
279
|
+
}
|
|
280
|
+
catch { }
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
fs.unlinkSync(tmp);
|
|
284
|
+
}
|
|
285
|
+
catch { }
|
|
286
|
+
return false;
|
|
191
287
|
}
|
|
192
288
|
}
|
|
193
289
|
hasEnqueuedMessage(sessionKey, messageId, selfAID) {
|
|
@@ -538,6 +634,8 @@ export class MessageQueue {
|
|
|
538
634
|
sessionId: sessionKey,
|
|
539
635
|
reason: 'daemon_restart',
|
|
540
636
|
agentName,
|
|
637
|
+
taskId: this.activeStates.get(queueKey)?.taskId,
|
|
638
|
+
generation: this.activeStates.get(queueKey)?.generation,
|
|
541
639
|
causation: item.message.causation,
|
|
542
640
|
});
|
|
543
641
|
barriers.push(this.triggerInterrupt(queueKey, sessionKey, this.activeStates.get(queueKey)?.baseagent, agentName, 'daemon_restart'));
|
|
@@ -768,6 +866,8 @@ export class MessageQueue {
|
|
|
768
866
|
shouldRestoreMessage(message, sessionId) {
|
|
769
867
|
// The Trigger scheduler records open runs as daemon_restart. Replaying their
|
|
770
868
|
// queued work afterwards would execute side effects for an already-failed run.
|
|
869
|
+
if (message.executionPermissionOverride)
|
|
870
|
+
return false;
|
|
771
871
|
if (message.source !== 'trigger' && !message.triggerMeta)
|
|
772
872
|
return true;
|
|
773
873
|
this.droppedTriggerSessionIds.add(sessionId);
|
|
@@ -794,9 +894,9 @@ export class MessageQueue {
|
|
|
794
894
|
}
|
|
795
895
|
// 拦截器检查:AskUserQuestion 等场景的一次性消息拦截
|
|
796
896
|
const interceptor = this.interceptors.get(sessionKey);
|
|
797
|
-
const canAnswerInteraction = message.source === undefined
|
|
897
|
+
const canAnswerInteraction = !message.executionPermissionOverride && (message.source === undefined
|
|
798
898
|
|| message.source === 'user'
|
|
799
|
-
|| message.source === 'card-trigger';
|
|
899
|
+
|| message.source === 'card-trigger');
|
|
800
900
|
if (interceptor && canAnswerInteraction) {
|
|
801
901
|
this.interceptors.delete(sessionKey);
|
|
802
902
|
logger.debug(`[Queue] Message intercepted for ${sessionKey}`);
|
|
@@ -849,6 +949,7 @@ export class MessageQueue {
|
|
|
849
949
|
// Capture the physical enqueue instant once. It must not be regenerated
|
|
850
950
|
// on every persistence snapshot, otherwise old messages appear fresh.
|
|
851
951
|
const enqueuedAt = Date.now();
|
|
952
|
+
this.enqueueSequence += 1;
|
|
852
953
|
// Clone at the physical queue boundary. The same inbound object may be
|
|
853
954
|
// broadcast to multiple agents; mutating it would make all copies share
|
|
854
955
|
// the last queue's enqueue timestamp.
|
|
@@ -934,6 +1035,8 @@ export class MessageQueue {
|
|
|
934
1035
|
sessionId: sessionKey,
|
|
935
1036
|
reason: 'new_message',
|
|
936
1037
|
agentName: this.processingAgent.get(queueKey),
|
|
1038
|
+
taskId: this.activeStates.get(queueKey)?.taskId,
|
|
1039
|
+
generation: this.activeStates.get(queueKey)?.generation,
|
|
937
1040
|
causation: this.activeBatches.get(queueKey)?.message.causation,
|
|
938
1041
|
});
|
|
939
1042
|
if (this.interruptCallback) {
|
|
@@ -1154,7 +1257,10 @@ export class MessageQueue {
|
|
|
1154
1257
|
return result;
|
|
1155
1258
|
}
|
|
1156
1259
|
isMergeBarrier(message) {
|
|
1157
|
-
return !!message.
|
|
1260
|
+
return !!message.executionPermissionOverride
|
|
1261
|
+
|| !!message.handoffDelivery
|
|
1262
|
+
|| !!message.triggerMeta
|
|
1263
|
+
|| !!message.causation?.trigger?.path.length;
|
|
1158
1264
|
}
|
|
1159
1265
|
isOriginHandoff(message) {
|
|
1160
1266
|
return message.source === 'handoff' && message.handoffDelivery?.direction === 'origin';
|
|
@@ -1427,6 +1533,8 @@ export class MessageQueue {
|
|
|
1427
1533
|
sessionId: sessionKey,
|
|
1428
1534
|
reason: 'recalled',
|
|
1429
1535
|
agentName,
|
|
1536
|
+
taskId: activeState?.taskId,
|
|
1537
|
+
generation: activeState?.generation,
|
|
1430
1538
|
causation: active?.message.causation,
|
|
1431
1539
|
});
|
|
1432
1540
|
const barrier = this.interruptCallback
|
|
@@ -1437,23 +1545,122 @@ export class MessageQueue {
|
|
|
1437
1545
|
return { found: false, barrier: Promise.resolve() };
|
|
1438
1546
|
}
|
|
1439
1547
|
/**
|
|
1440
|
-
*
|
|
1441
|
-
*
|
|
1548
|
+
* Capture the session's queue state and install a dequeue gate in one
|
|
1549
|
+
* synchronous critical section. Messages arriving after this call remain
|
|
1550
|
+
* durable in the queue, but processNext will wait until the last holder
|
|
1551
|
+
* releases the gate.
|
|
1442
1552
|
*/
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1553
|
+
acquireDequeueGate(sessionKey) {
|
|
1554
|
+
const existing = this.externalLocks.get(sessionKey);
|
|
1555
|
+
if (existing) {
|
|
1556
|
+
existing.holders += 1;
|
|
1557
|
+
let released = false;
|
|
1558
|
+
return {
|
|
1559
|
+
snapshot: { ...existing.snapshot },
|
|
1560
|
+
release: () => {
|
|
1561
|
+
if (released)
|
|
1562
|
+
return;
|
|
1563
|
+
released = true;
|
|
1564
|
+
existing.holders -= 1;
|
|
1565
|
+
if (existing.holders > 0)
|
|
1566
|
+
return;
|
|
1567
|
+
if (this.externalLocks.get(sessionKey) === existing)
|
|
1568
|
+
this.externalLocks.delete(sessionKey);
|
|
1569
|
+
existing.releasePromise();
|
|
1570
|
+
},
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
let releasePromise;
|
|
1574
|
+
const promise = new Promise(resolve => { releasePromise = resolve; });
|
|
1575
|
+
let queued = 0;
|
|
1576
|
+
let active = false;
|
|
1577
|
+
let dispatching = false;
|
|
1578
|
+
for (const [queueKey, queue] of this.queues.entries()) {
|
|
1579
|
+
if (!this.matchesSession(queueKey, sessionKey))
|
|
1580
|
+
continue;
|
|
1581
|
+
queued += this.queueRawMessageCount(queue);
|
|
1582
|
+
if (this.processing.has(queueKey) || this.activeBatches.has(queueKey))
|
|
1583
|
+
active = true;
|
|
1584
|
+
if (this.processing.has(queueKey) && !this.activeBatches.has(queueKey))
|
|
1585
|
+
dispatching = true;
|
|
1586
|
+
}
|
|
1587
|
+
for (const queueKey of this.activeBatches.keys()) {
|
|
1588
|
+
if (this.matchesSession(queueKey, sessionKey))
|
|
1589
|
+
active = true;
|
|
1590
|
+
}
|
|
1591
|
+
const state = {
|
|
1592
|
+
promise,
|
|
1593
|
+
releasePromise,
|
|
1594
|
+
holders: 1,
|
|
1595
|
+
snapshot: {
|
|
1596
|
+
prequeue: this.prequeueReservations.get(sessionKey) ?? 0,
|
|
1597
|
+
active,
|
|
1598
|
+
queued,
|
|
1599
|
+
dispatching,
|
|
1600
|
+
cutoff: this.enqueueSequence,
|
|
1601
|
+
},
|
|
1602
|
+
};
|
|
1603
|
+
this.externalLocks.set(sessionKey, state);
|
|
1604
|
+
let released = false;
|
|
1605
|
+
return {
|
|
1606
|
+
snapshot: { ...state.snapshot },
|
|
1607
|
+
release: () => {
|
|
1608
|
+
if (released)
|
|
1609
|
+
return;
|
|
1610
|
+
released = true;
|
|
1611
|
+
state.holders -= 1;
|
|
1612
|
+
if (state.holders > 0)
|
|
1613
|
+
return;
|
|
1614
|
+
if (this.externalLocks.get(sessionKey) === state)
|
|
1615
|
+
this.externalLocks.delete(sessionKey);
|
|
1616
|
+
state.releasePromise();
|
|
1617
|
+
},
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Mark an admitted message before it enters debounce or other pre-queue
|
|
1622
|
+
* work. The returned release is idempotent so callers can use it both from
|
|
1623
|
+
* onPersisted and a surrounding finally block.
|
|
1624
|
+
*/
|
|
1625
|
+
reserveSessionIngress(sessionKey) {
|
|
1626
|
+
this.prequeueReservations.set(sessionKey, (this.prequeueReservations.get(sessionKey) ?? 0) + 1);
|
|
1627
|
+
let released = false;
|
|
1447
1628
|
return () => {
|
|
1448
|
-
|
|
1449
|
-
|
|
1629
|
+
if (released)
|
|
1630
|
+
return;
|
|
1631
|
+
released = true;
|
|
1632
|
+
const remaining = (this.prequeueReservations.get(sessionKey) ?? 1) - 1;
|
|
1633
|
+
if (remaining > 0)
|
|
1634
|
+
this.prequeueReservations.set(sessionKey, remaining);
|
|
1635
|
+
else
|
|
1636
|
+
this.prequeueReservations.delete(sessionKey);
|
|
1450
1637
|
};
|
|
1451
1638
|
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Run a fast session command while no queued work can start. The queue
|
|
1641
|
+
* snapshot and external gate are installed before the callback is invoked;
|
|
1642
|
+
* messages arriving afterwards remain durable but are outside this probe.
|
|
1643
|
+
*/
|
|
1644
|
+
async withSessionBarrier(sessionKey, callback) {
|
|
1645
|
+
return this.sessionBarrierMutex.forKey(sessionKey).runExclusive(async () => {
|
|
1646
|
+
const gate = this.acquireDequeueGate(sessionKey);
|
|
1647
|
+
try {
|
|
1648
|
+
return await callback(gate.snapshot);
|
|
1649
|
+
}
|
|
1650
|
+
finally {
|
|
1651
|
+
gate.release();
|
|
1652
|
+
}
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
/** Backwards-compatible lock API used by existing quick commands. */
|
|
1656
|
+
acquireLock(sessionKey) {
|
|
1657
|
+
return this.acquireDequeueGate(sessionKey).release;
|
|
1658
|
+
}
|
|
1452
1659
|
/** 检查是否有外部锁 */
|
|
1453
1660
|
getExternalLock(queueKey) {
|
|
1454
|
-
for (const [key,
|
|
1661
|
+
for (const [key, state] of this.externalLocks) {
|
|
1455
1662
|
if (this.matchesSession(queueKey, key))
|
|
1456
|
-
return promise;
|
|
1663
|
+
return state.promise;
|
|
1457
1664
|
}
|
|
1458
1665
|
return undefined;
|
|
1459
1666
|
}
|
|
@@ -1576,6 +1783,8 @@ export class MessageQueue {
|
|
|
1576
1783
|
sessionId: sessionKey,
|
|
1577
1784
|
reason: 'stop',
|
|
1578
1785
|
agentName: name,
|
|
1786
|
+
taskId: this.activeStates.get(queueKey)?.taskId,
|
|
1787
|
+
generation: this.activeStates.get(queueKey)?.generation,
|
|
1579
1788
|
causation: this.activeBatches.get(queueKey)?.message.causation,
|
|
1580
1789
|
});
|
|
1581
1790
|
if (this.interruptCallback) {
|
|
@@ -1848,6 +2057,8 @@ export class MessageQueue {
|
|
|
1848
2057
|
sessionId: sessionKey,
|
|
1849
2058
|
reason: 'new_message',
|
|
1850
2059
|
agentName: targetAgentName,
|
|
2060
|
+
taskId: this.activeStates.get(targetQueueKey)?.taskId,
|
|
2061
|
+
generation: this.activeStates.get(targetQueueKey)?.generation,
|
|
1851
2062
|
causation: this.activeBatches.get(targetQueueKey)?.message.causation,
|
|
1852
2063
|
});
|
|
1853
2064
|
if (this.interruptCallback) {
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
* 从 message-processor.ts 提取的独立工具函数,供 ResponseEngine 和其他模块使用。
|
|
5
5
|
*/
|
|
6
6
|
import { renderCommandCardAsText, renderActionAsText } from '../interaction-router.js';
|
|
7
|
+
/** A legacy adapter resolved its send call without providing a structured receipt. */
|
|
8
|
+
export const LEGACY_INTERACTION_ACCEPTED = 'legacy-accepted';
|
|
9
|
+
export function isInteractionSendAccepted(result) {
|
|
10
|
+
return result === LEGACY_INTERACTION_ACCEPTED
|
|
11
|
+
|| (result !== false && (result.status === 'sent' || result.status === 'queued'));
|
|
12
|
+
}
|
|
7
13
|
/**
|
|
8
14
|
* 构造出站消息信封
|
|
9
15
|
*/
|
|
@@ -129,7 +135,8 @@ export function defaultFallbackText(interaction) {
|
|
|
129
135
|
* 通过统一的 adapter.send 入口发送交互负载
|
|
130
136
|
*
|
|
131
137
|
* 通过 adapter.send(envelope, { kind: 'interaction', ... }) 发送交互。
|
|
132
|
-
*
|
|
138
|
+
* 保留 adapter 的结构化回执,让调用方区分 sent、queued、failed 和
|
|
139
|
+
* suppressed;legacy adapter 的 void 返回表示已接受但尚无可验证回执。
|
|
133
140
|
*/
|
|
134
141
|
export async function sendInteractionPayload(adapter, envelope, interaction, fallbackText, replyCtx) {
|
|
135
142
|
const text = fallbackText ?? defaultFallbackText(interaction);
|
|
@@ -141,11 +148,11 @@ export async function sendInteractionPayload(adapter, envelope, interaction, fal
|
|
|
141
148
|
try {
|
|
142
149
|
const enriched = replyCtx ? withEnvelopeReplyContext(envelope, replyCtx) : envelope;
|
|
143
150
|
const receipt = await adapter.send(enriched, payload);
|
|
144
|
-
if (receipt
|
|
151
|
+
if (!receipt)
|
|
152
|
+
return LEGACY_INTERACTION_ACCEPTED;
|
|
153
|
+
if (receipt.status === 'sent' && !(receipt.messageId || receipt.messages[0]?.messageId))
|
|
145
154
|
return false;
|
|
146
|
-
|
|
147
|
-
return receipt.messages[0]?.messageId || false;
|
|
148
|
-
return 'sent';
|
|
155
|
+
return receipt;
|
|
149
156
|
}
|
|
150
157
|
catch {
|
|
151
158
|
return false;
|