@ganglion/xacpx 0.23.0-beta.5 → 0.23.0-beta.7
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/dist/cli.js
CHANGED
|
@@ -28499,7 +28499,7 @@ function buildXacpxMcpToolRegistry(input) {
|
|
|
28499
28499
|
})
|
|
28500
28500
|
}, {
|
|
28501
28501
|
name: "agent_send",
|
|
28502
|
-
description: "Send a high-value asynchronous peer message to another agent. ONLY use this when: (1) you have critical information that directly alters the target's current work (e.g. breaking schema change, interface takeover), or (2) you need an unblocking decision only the target can provide. DO NOT send acknowledgments ('Received', 'OK', 'Working on it'), status polling ('Are you done?'), or conversational chatter. One-way notifications require NO reply. Provide replyTo when answering an earlier message. Completion modes: none (one-way message; no completion signal is returned), notify (after the peer turn triggered by this message reaches terminal state, xacpx notifies the sender that the peer completed/failed/cancelled), result (after the peer turn reaches terminal state, xacpx returns the peer's bounded final assistant result to the sender). Note agent_send still returns delivery admission immediately.",
|
|
28502
|
+
description: "Send a high-value asynchronous peer message to another agent. ONLY use this when: (1) you have critical information that directly alters the target's current work (e.g. breaking schema change, interface takeover), or (2) you need an unblocking decision only the target can provide. DO NOT send acknowledgments ('Received', 'OK', 'Working on it'), status polling ('Are you done?'), or conversational chatter. One-way notifications require NO reply. Provide replyTo when answering an earlier message. Completion modes: none (one-way message; no completion signal is returned), notify (after the peer turn triggered by this message reaches terminal state, xacpx notifies the sender that the peer completed/failed/cancelled), result (after the peer turn reaches terminal state, xacpx returns the peer's bounded final assistant result to the sender). Note agent_send still returns delivery admission immediately. mode='interrupt' requests cancellation of the target Agent's current turn and reserves this message as its highest-priority next turn; xacpx waits for the current turn to fully settle before starting the message, and never runs the two turns in parallel. Some runtimes may finish the current turn instead of stopping immediately. Use interrupt only when waiting behind ordinary queued work would materially harm the task. The default/auto mode NEVER selects interrupt, and xacpx never escalates a message to interrupt based on its wording.",
|
|
28503
28503
|
inputSchema: exports_external.object({
|
|
28504
28504
|
to: exports_external.string().min(1).optional().describe("Target opaque handle from agent_list"),
|
|
28505
28505
|
selector: exports_external.object({
|
|
@@ -28508,7 +28508,7 @@ function buildXacpxMcpToolRegistry(input) {
|
|
|
28508
28508
|
agent: exports_external.string().min(1).optional().describe("Target agent type")
|
|
28509
28509
|
}).strict().refine((s) => Boolean(s.displayName || s.workspace || s.agent), "Target selector must specify at least one criterion (displayName, workspace, or agent)").optional().describe("Target selector criteria when handle is not known"),
|
|
28510
28510
|
message: exports_external.string().min(1).describe("Concise, actionable message content"),
|
|
28511
|
-
mode: exports_external.enum(["auto", "steer", "queue", "interrupt"]).optional().describe("Delivery mode (default: auto -> queue)"),
|
|
28511
|
+
mode: exports_external.enum(["auto", "steer", "queue", "interrupt"]).optional().describe("Delivery mode (default: auto -> queue). 'interrupt' REQUESTS cancellation of the target's current turn and reserves this message as its highest-priority next turn; xacpx waits for the current turn to fully settle first, and some runtimes may finish it instead of stopping immediately. 'auto' and 'queue' never cancel. 'steer' injects into the target's active turn when the runtime supports it."),
|
|
28512
28512
|
replyTo: exports_external.string().min(1).optional().describe("Message ID being replied to, if continuing a thread"),
|
|
28513
28513
|
completion: exports_external.enum(["none", "notify", "result"]).optional().describe("Completion notification mode: 'none' (one-way message; no completion signal is returned), 'notify' (notifies sender when peer turn reaches terminal state), or 'result' (returns peer's bounded final assistant result). Note agent_send still returns delivery admission immediately.")
|
|
28514
28514
|
}).strict().refine((data) => data.to !== undefined && data.selector === undefined || data.to === undefined && data.selector !== undefined, {
|
|
@@ -50667,7 +50667,7 @@ class AgentEndpointRegistry {
|
|
|
50667
50667
|
activity: {
|
|
50668
50668
|
status: isRunning ? "working" : "idle"
|
|
50669
50669
|
},
|
|
50670
|
-
capabilities: queueOnlyCapabilities({ completion: true }),
|
|
50670
|
+
capabilities: queueOnlyCapabilities({ completion: true, interrupt: true }),
|
|
50671
50671
|
endpointKind: "logical",
|
|
50672
50672
|
channelId: getChannelIdFromChatKey(session3.alias)
|
|
50673
50673
|
};
|
|
@@ -50718,7 +50718,7 @@ function queueOnlyCapabilities(options) {
|
|
|
50718
50718
|
receive: true,
|
|
50719
50719
|
steer: false,
|
|
50720
50720
|
queue: true,
|
|
50721
|
-
interrupt: false,
|
|
50721
|
+
interrupt: options?.interrupt ?? false,
|
|
50722
50722
|
conversation: true,
|
|
50723
50723
|
completion: options?.completion ?? false
|
|
50724
50724
|
};
|
|
@@ -50981,7 +50981,8 @@ var init_agent_message_router = __esm(() => {
|
|
|
50981
50981
|
"TARGET_UNAVAILABLE",
|
|
50982
50982
|
"MESSAGE_QUEUE_FULL",
|
|
50983
50983
|
"DELIVERY_DENIED",
|
|
50984
|
-
"COMPLETION_NOT_SUPPORTED"
|
|
50984
|
+
"COMPLETION_NOT_SUPPORTED",
|
|
50985
|
+
"TARGET_NOT_INTERRUPTIBLE"
|
|
50985
50986
|
]);
|
|
50986
50987
|
AgentMessageRouter = class AgentMessageRouter {
|
|
50987
50988
|
deps;
|
|
@@ -51636,6 +51637,9 @@ var init_agent_message_router = __esm(() => {
|
|
|
51636
51637
|
createdAt,
|
|
51637
51638
|
...input.replyTo ? { replyTo: input.replyTo } : {}
|
|
51638
51639
|
};
|
|
51640
|
+
if (message.requestedMode === "interrupt" && target.endpoint.capabilities.interrupt !== true) {
|
|
51641
|
+
throw new AgentMessagingError("TARGET_NOT_INTERRUPTIBLE", "The target does not support explicit interrupt delivery.");
|
|
51642
|
+
}
|
|
51639
51643
|
if (message.completion !== "none") {
|
|
51640
51644
|
if (target.endpoint.capabilities.completion !== true) {
|
|
51641
51645
|
throw new AgentMessagingError("COMPLETION_NOT_SUPPORTED", `Completion mode '${message.completion}' requires a target that advertises completion capability.`);
|
|
@@ -52454,10 +52458,11 @@ class LocalAgentMessageDeliveryAdapter {
|
|
|
52454
52458
|
source: message.from,
|
|
52455
52459
|
target: message.to
|
|
52456
52460
|
};
|
|
52457
|
-
const res = await this.deps.deliverLogicalTurn(target.runtime.alias, renderedText, message.id, peerOrigin);
|
|
52461
|
+
const res = await this.deps.deliverLogicalTurn(target.runtime.alias, renderedText, message.id, peerOrigin, message.requestedMode);
|
|
52458
52462
|
return {
|
|
52459
52463
|
status: res.status,
|
|
52460
|
-
modeUsed: "queue"
|
|
52464
|
+
modeUsed: res.modeUsed ?? "queue",
|
|
52465
|
+
...res.targetState ? { targetState: res.targetState } : {}
|
|
52461
52466
|
};
|
|
52462
52467
|
}
|
|
52463
52468
|
const session3 = target.runtime.kind === "logical" ? await this.deps.resolveLogicalSession(target.runtime.transportSession) : this.deps.resolveWorkerSession(target.runtime);
|
|
@@ -60156,6 +60161,7 @@ class TurnQueue {
|
|
|
60156
60161
|
settledRequestIds = new Map;
|
|
60157
60162
|
static SETTLED_REQUEST_IDS_MAX = 2000;
|
|
60158
60163
|
queues = new Map;
|
|
60164
|
+
pendingInterrupts = new Map;
|
|
60159
60165
|
draining = new Set;
|
|
60160
60166
|
removing = new Set;
|
|
60161
60167
|
resolveKey(chatKey, sessionAlias, concurrencyKey) {
|
|
@@ -60163,7 +60169,7 @@ class TurnQueue {
|
|
|
60163
60169
|
return concurrencyKey;
|
|
60164
60170
|
}
|
|
60165
60171
|
const directKey = turnKey(chatKey, sessionAlias);
|
|
60166
|
-
if (this.inFlight.has(directKey) || this.queues.has(directKey) || this.draining.has(directKey) || this.removing.has(directKey)) {
|
|
60172
|
+
if (this.inFlight.has(directKey) || this.queues.has(directKey) || this.draining.has(directKey) || this.removing.has(directKey) || this.pendingInterrupts.has(directKey)) {
|
|
60167
60173
|
return directKey;
|
|
60168
60174
|
}
|
|
60169
60175
|
const cleanAlias = sessionAlias.includes(":") ? sessionAlias.split(":").pop() : sessionAlias;
|
|
@@ -60183,6 +60189,10 @@ class TurnQueue {
|
|
|
60183
60189
|
if (k === directKey || k.endsWith(":" + cleanAlias) || k.endsWith(" " + cleanAlias) || k === cleanAlias)
|
|
60184
60190
|
return k;
|
|
60185
60191
|
}
|
|
60192
|
+
for (const k of this.pendingInterrupts.keys()) {
|
|
60193
|
+
if (k === directKey || k.endsWith(":" + cleanAlias) || k.endsWith(" " + cleanAlias) || k === cleanAlias)
|
|
60194
|
+
return k;
|
|
60195
|
+
}
|
|
60186
60196
|
return directKey;
|
|
60187
60197
|
}
|
|
60188
60198
|
queueLength(chatKey, sessionAlias, concurrencyKey) {
|
|
@@ -60254,6 +60264,99 @@ class TurnQueue {
|
|
|
60254
60264
|
this.recordSettledRequestId(params.promptRequestId);
|
|
60255
60265
|
return { status: "injected" };
|
|
60256
60266
|
}
|
|
60267
|
+
pendingInterruptCount(chatKey, sessionAlias, concurrencyKey) {
|
|
60268
|
+
return this.pendingInterrupts.has(this.resolveKey(chatKey, sessionAlias, concurrencyKey)) ? 1 : 0;
|
|
60269
|
+
}
|
|
60270
|
+
submitPeerInterrupt(params) {
|
|
60271
|
+
const key = this.resolveKey(params.chatKey, params.sessionAlias, params.concurrencyKey);
|
|
60272
|
+
if (this.removing.has(key)) {
|
|
60273
|
+
return { status: "rejected", reason: "target-unavailable" };
|
|
60274
|
+
}
|
|
60275
|
+
if (params.promptRequestId !== undefined && params.isPeerMessage) {
|
|
60276
|
+
const pendingDup = this.pendingInterrupts.get(key);
|
|
60277
|
+
if (pendingDup?.promptRequestId === params.promptRequestId) {
|
|
60278
|
+
return { status: "queued", modeUsed: "interrupt", queueItemId: pendingDup.id };
|
|
60279
|
+
}
|
|
60280
|
+
if (this.hasSettledRequestId(params.promptRequestId)) {
|
|
60281
|
+
return { status: "injected", modeUsed: "prompt" };
|
|
60282
|
+
}
|
|
60283
|
+
const queuedDup = (this.queues.get(key) ?? []).some((item2) => item2.promptRequestId === params.promptRequestId);
|
|
60284
|
+
const inFlightDup = this.inFlight.get(key)?.promptRequestId === params.promptRequestId;
|
|
60285
|
+
if (queuedDup || inFlightDup) {
|
|
60286
|
+
return { status: "injected", modeUsed: "prompt" };
|
|
60287
|
+
}
|
|
60288
|
+
}
|
|
60289
|
+
const existing = this.inFlight.get(key);
|
|
60290
|
+
const busy = this.draining.has(key) || existing !== undefined;
|
|
60291
|
+
if (!busy) {
|
|
60292
|
+
const idle = this.submitPeerTurn(params);
|
|
60293
|
+
if (idle.status === "rejected")
|
|
60294
|
+
return idle;
|
|
60295
|
+
if (idle.status === "queued") {
|
|
60296
|
+
return { status: "rejected", reason: "queue-full" };
|
|
60297
|
+
}
|
|
60298
|
+
return { status: "injected", modeUsed: "prompt" };
|
|
60299
|
+
}
|
|
60300
|
+
const pending = this.pendingInterrupts.get(key);
|
|
60301
|
+
if (pending) {
|
|
60302
|
+
this.emitPeerInterrupt("rejected_pending", params.chatKey, params.sessionAlias, pending, {
|
|
60303
|
+
requestMessageId: params.peerOrigin?.requestMessageId,
|
|
60304
|
+
promptRequestId: params.promptRequestId,
|
|
60305
|
+
pendingRequestMessageId: pending.peerOrigin?.requestMessageId
|
|
60306
|
+
});
|
|
60307
|
+
return { status: "rejected", reason: "queue-full" };
|
|
60308
|
+
}
|
|
60309
|
+
const id = randomUUID13();
|
|
60310
|
+
const item = {
|
|
60311
|
+
id,
|
|
60312
|
+
text: params.text,
|
|
60313
|
+
enqueuedAt: new Date().toISOString(),
|
|
60314
|
+
senderId: params.senderId,
|
|
60315
|
+
executionContext: {
|
|
60316
|
+
chatKey: params.chatKey,
|
|
60317
|
+
sessionAlias: params.sessionAlias,
|
|
60318
|
+
...params.boundSessionAlias ? { boundSessionAlias: params.boundSessionAlias } : {}
|
|
60319
|
+
},
|
|
60320
|
+
concurrencyKey: params.concurrencyKey,
|
|
60321
|
+
isPeerMessage: true,
|
|
60322
|
+
allowRestoreArchived: false,
|
|
60323
|
+
...params.isOwner !== undefined ? { isOwner: params.isOwner } : {},
|
|
60324
|
+
...params.preserveCoordinatorRoute !== undefined ? { preserveCoordinatorRoute: params.preserveCoordinatorRoute } : {},
|
|
60325
|
+
...params.accountId !== undefined ? { accountId: params.accountId } : {},
|
|
60326
|
+
...params.media !== undefined ? { media: params.media } : {},
|
|
60327
|
+
...params.agentMentions !== undefined ? { agentMentions: params.agentMentions } : {},
|
|
60328
|
+
...params.promptRequestId !== undefined ? { promptRequestId: params.promptRequestId } : {},
|
|
60329
|
+
...params.peerOrigin !== undefined ? { peerOrigin: params.peerOrigin } : {},
|
|
60330
|
+
...params.trustedPeerCompletion !== undefined ? { trustedPeerCompletion: params.trustedPeerCompletion } : {}
|
|
60331
|
+
};
|
|
60332
|
+
this.pendingInterrupts.set(key, item);
|
|
60333
|
+
this.recordSettledRequestId(params.promptRequestId);
|
|
60334
|
+
const predecessorWasAlreadyAborted = existing?.controller.signal.aborted ?? false;
|
|
60335
|
+
this.emitPeerInterrupt("reserved", params.chatKey, params.sessionAlias, item, {
|
|
60336
|
+
predecessorWasAlreadyAborted
|
|
60337
|
+
});
|
|
60338
|
+
if (existing && !predecessorWasAlreadyAborted) {
|
|
60339
|
+
existing.controller.abort();
|
|
60340
|
+
this.emitPeerInterrupt("abort_signalled", params.chatKey, params.sessionAlias, item);
|
|
60341
|
+
}
|
|
60342
|
+
this.emitQueueUpdated(params.chatKey, params.sessionAlias, key);
|
|
60343
|
+
return { status: "queued", modeUsed: "interrupt", queueItemId: id };
|
|
60344
|
+
}
|
|
60345
|
+
emitPeerInterrupt(kind, chatKey, sessionAlias, item, ids) {
|
|
60346
|
+
if (!this.deps.onPeerInterruptEvent)
|
|
60347
|
+
return;
|
|
60348
|
+
try {
|
|
60349
|
+
this.deps.onPeerInterruptEvent({
|
|
60350
|
+
kind,
|
|
60351
|
+
chatKey,
|
|
60352
|
+
sessionAlias,
|
|
60353
|
+
...ids?.requestMessageId !== undefined ? { requestMessageId: ids.requestMessageId } : item?.peerOrigin?.requestMessageId !== undefined ? { requestMessageId: item.peerOrigin.requestMessageId } : {},
|
|
60354
|
+
...ids?.promptRequestId !== undefined ? { promptRequestId: ids.promptRequestId } : item?.promptRequestId !== undefined ? { promptRequestId: item.promptRequestId } : {},
|
|
60355
|
+
...ids?.pendingRequestMessageId !== undefined ? { pendingRequestMessageId: ids.pendingRequestMessageId } : {},
|
|
60356
|
+
...ids?.predecessorWasAlreadyAborted !== undefined ? { predecessorWasAlreadyAborted: ids.predecessorWasAlreadyAborted } : {}
|
|
60357
|
+
});
|
|
60358
|
+
} catch {}
|
|
60359
|
+
}
|
|
60257
60360
|
hasSettledRequestId(id) {
|
|
60258
60361
|
const expiresAt = this.settledRequestIds.get(id);
|
|
60259
60362
|
if (expiresAt === undefined)
|
|
@@ -60293,11 +60396,20 @@ class TurnQueue {
|
|
|
60293
60396
|
}
|
|
60294
60397
|
emitQueueUpdated(chatKey, sessionAlias, queueKey) {
|
|
60295
60398
|
const key = queueKey ?? this.resolveKey(chatKey, sessionAlias);
|
|
60296
|
-
const
|
|
60297
|
-
|
|
60298
|
-
|
|
60299
|
-
|
|
60300
|
-
|
|
60399
|
+
const interrupt = this.pendingInterrupts.get(key);
|
|
60400
|
+
const items = interrupt ? [{
|
|
60401
|
+
id: interrupt.id,
|
|
60402
|
+
textPreview: interrupt.text.length > QUEUE_PREVIEW_MAX ? interrupt.text.slice(0, QUEUE_PREVIEW_MAX) : interrupt.text,
|
|
60403
|
+
enqueuedAt: interrupt.enqueuedAt,
|
|
60404
|
+
kind: "interrupt"
|
|
60405
|
+
}] : [];
|
|
60406
|
+
for (const q of this.queues.get(key) ?? []) {
|
|
60407
|
+
items.push({
|
|
60408
|
+
id: q.id,
|
|
60409
|
+
textPreview: q.text.length > QUEUE_PREVIEW_MAX ? q.text.slice(0, QUEUE_PREVIEW_MAX) : q.text,
|
|
60410
|
+
enqueuedAt: q.enqueuedAt
|
|
60411
|
+
});
|
|
60412
|
+
}
|
|
60301
60413
|
this.deps.emitQueueUpdated(chatKey, sessionAlias, items);
|
|
60302
60414
|
}
|
|
60303
60415
|
async submit(params) {
|
|
@@ -60397,6 +60509,7 @@ class TurnQueue {
|
|
|
60397
60509
|
};
|
|
60398
60510
|
let result;
|
|
60399
60511
|
try {
|
|
60512
|
+
const turnStarted = params.turnStarted ? params.promptRequestId !== undefined && params.turnStarted.promptRequestId === undefined ? { ...params.turnStarted, promptRequestId: params.promptRequestId } : params.turnStarted : params.promptRequestId !== undefined ? { promptRequestId: params.promptRequestId } : undefined;
|
|
60400
60513
|
result = await this.deps.runTurn({
|
|
60401
60514
|
chatKey: params.chatKey,
|
|
60402
60515
|
sessionAlias: params.sessionAlias,
|
|
@@ -60405,7 +60518,7 @@ class TurnQueue {
|
|
|
60405
60518
|
senderId: params.senderId,
|
|
60406
60519
|
isOwner: params.isOwner,
|
|
60407
60520
|
accountId: params.accountId,
|
|
60408
|
-
turnStarted
|
|
60521
|
+
turnStarted,
|
|
60409
60522
|
media: params.media,
|
|
60410
60523
|
agentMentions: params.agentMentions,
|
|
60411
60524
|
allowRestoreArchived: params.allowRestoreArchived,
|
|
@@ -60418,7 +60531,7 @@ class TurnQueue {
|
|
|
60418
60531
|
} finally {
|
|
60419
60532
|
if (idleTimer)
|
|
60420
60533
|
this.clearTimer(idleTimer);
|
|
60421
|
-
if ((this.queues.get(key)?.length ?? 0) > 0) {
|
|
60534
|
+
if ((this.queues.get(key)?.length ?? 0) > 0 || this.pendingInterrupts.has(key)) {
|
|
60422
60535
|
this.draining.add(key);
|
|
60423
60536
|
}
|
|
60424
60537
|
const detection = result?.postTurnDetection;
|
|
@@ -60435,40 +60548,50 @@ class TurnQueue {
|
|
|
60435
60548
|
};
|
|
60436
60549
|
}
|
|
60437
60550
|
advanceQueue(key) {
|
|
60551
|
+
const interrupt = this.pendingInterrupts.get(key);
|
|
60552
|
+
if (interrupt) {
|
|
60553
|
+
this.pendingInterrupts.delete(key);
|
|
60554
|
+
this.emitPeerInterrupt("started", interrupt.executionContext.chatKey, interrupt.executionContext.sessionAlias, interrupt);
|
|
60555
|
+
this.drainQueuedPrompt(key, interrupt);
|
|
60556
|
+
return;
|
|
60557
|
+
}
|
|
60438
60558
|
const q = this.queues.get(key);
|
|
60439
60559
|
const next = q?.shift();
|
|
60440
60560
|
if (q && q.length === 0)
|
|
60441
60561
|
this.queues.delete(key);
|
|
60442
60562
|
if (next) {
|
|
60443
|
-
this.
|
|
60444
|
-
const concurrencyKey = next.concurrencyKey ?? key;
|
|
60445
|
-
const turnStarted = next.isPeerMessage ? { queueItemId: next.id, ...next.promptRequestId !== undefined ? { promptRequestId: next.promptRequestId } : {} } : { prompt: next.text, queueItemId: next.id, ...next.promptRequestId !== undefined ? { promptRequestId: next.promptRequestId } : {} };
|
|
60446
|
-
this.submit({
|
|
60447
|
-
chatKey: next.executionContext.chatKey,
|
|
60448
|
-
sessionAlias: next.executionContext.sessionAlias,
|
|
60449
|
-
boundSessionAlias: next.executionContext.boundSessionAlias,
|
|
60450
|
-
concurrencyKey,
|
|
60451
|
-
text: next.text,
|
|
60452
|
-
senderId: next.senderId,
|
|
60453
|
-
queueable: true,
|
|
60454
|
-
drained: true,
|
|
60455
|
-
isPeerMessage: next.isPeerMessage,
|
|
60456
|
-
allowRestoreArchived: next.allowRestoreArchived,
|
|
60457
|
-
preserveCoordinatorRoute: next.preserveCoordinatorRoute,
|
|
60458
|
-
promptRequestId: next.promptRequestId,
|
|
60459
|
-
peerOrigin: next.peerOrigin,
|
|
60460
|
-
trustedPeerCompletion: next.trustedPeerCompletion,
|
|
60461
|
-
...next.isOwner !== undefined ? { isOwner: next.isOwner } : {},
|
|
60462
|
-
...next.accountId !== undefined ? { accountId: next.accountId } : {},
|
|
60463
|
-
...next.media !== undefined ? { media: next.media } : {},
|
|
60464
|
-
...next.agentMentions !== undefined ? { agentMentions: next.agentMentions } : {},
|
|
60465
|
-
turnStarted
|
|
60466
|
-
});
|
|
60563
|
+
this.drainQueuedPrompt(key, next);
|
|
60467
60564
|
} else {
|
|
60468
60565
|
this.draining.delete(key);
|
|
60469
60566
|
this.inFlight.delete(key);
|
|
60470
60567
|
}
|
|
60471
60568
|
}
|
|
60569
|
+
drainQueuedPrompt(key, next) {
|
|
60570
|
+
this.emitQueueUpdated(next.executionContext.chatKey, next.executionContext.sessionAlias, key);
|
|
60571
|
+
const concurrencyKey = next.concurrencyKey ?? key;
|
|
60572
|
+
const turnStarted = next.isPeerMessage ? { queueItemId: next.id, ...next.promptRequestId !== undefined ? { promptRequestId: next.promptRequestId } : {} } : { prompt: next.text, queueItemId: next.id, ...next.promptRequestId !== undefined ? { promptRequestId: next.promptRequestId } : {} };
|
|
60573
|
+
this.submit({
|
|
60574
|
+
chatKey: next.executionContext.chatKey,
|
|
60575
|
+
sessionAlias: next.executionContext.sessionAlias,
|
|
60576
|
+
boundSessionAlias: next.executionContext.boundSessionAlias,
|
|
60577
|
+
concurrencyKey,
|
|
60578
|
+
text: next.text,
|
|
60579
|
+
senderId: next.senderId,
|
|
60580
|
+
queueable: true,
|
|
60581
|
+
drained: true,
|
|
60582
|
+
isPeerMessage: next.isPeerMessage,
|
|
60583
|
+
allowRestoreArchived: next.allowRestoreArchived,
|
|
60584
|
+
preserveCoordinatorRoute: next.preserveCoordinatorRoute,
|
|
60585
|
+
promptRequestId: next.promptRequestId,
|
|
60586
|
+
peerOrigin: next.peerOrigin,
|
|
60587
|
+
trustedPeerCompletion: next.trustedPeerCompletion,
|
|
60588
|
+
...next.isOwner !== undefined ? { isOwner: next.isOwner } : {},
|
|
60589
|
+
...next.accountId !== undefined ? { accountId: next.accountId } : {},
|
|
60590
|
+
...next.media !== undefined ? { media: next.media } : {},
|
|
60591
|
+
...next.agentMentions !== undefined ? { agentMentions: next.agentMentions } : {},
|
|
60592
|
+
turnStarted
|
|
60593
|
+
});
|
|
60594
|
+
}
|
|
60472
60595
|
cancelTurn(chatKey, sessionAlias, concurrencyKey) {
|
|
60473
60596
|
const key = this.resolveKey(chatKey, sessionAlias, concurrencyKey);
|
|
60474
60597
|
const entry = this.inFlight.get(key);
|
|
@@ -60480,12 +60603,21 @@ class TurnQueue {
|
|
|
60480
60603
|
}
|
|
60481
60604
|
async clearSession(chatKey, sessionAlias, concurrencyKey) {
|
|
60482
60605
|
const key = this.resolveKey(chatKey, sessionAlias, concurrencyKey);
|
|
60606
|
+
const guardPreexisting = this.removing.has(key);
|
|
60607
|
+
this.removing.add(key);
|
|
60483
60608
|
const droppedFirst = this.queues.get(key);
|
|
60484
60609
|
if (droppedFirst) {
|
|
60485
60610
|
this.queues.delete(key);
|
|
60486
60611
|
this.emitQueueUpdated(chatKey, sessionAlias, key);
|
|
60487
60612
|
this.notifyQueuedPeerCancelled(droppedFirst);
|
|
60488
60613
|
}
|
|
60614
|
+
const droppedInterruptFirst = this.pendingInterrupts.get(key);
|
|
60615
|
+
if (droppedInterruptFirst) {
|
|
60616
|
+
this.pendingInterrupts.delete(key);
|
|
60617
|
+
this.emitPeerInterrupt("cancelled_before_start", droppedInterruptFirst.executionContext.chatKey, droppedInterruptFirst.executionContext.sessionAlias, droppedInterruptFirst);
|
|
60618
|
+
this.notifyQueuedPeerCancelled([droppedInterruptFirst]);
|
|
60619
|
+
this.emitQueueUpdated(chatKey, sessionAlias, key);
|
|
60620
|
+
}
|
|
60489
60621
|
const entry = this.inFlight.get(key);
|
|
60490
60622
|
if (entry) {
|
|
60491
60623
|
entry.controller.abort();
|
|
@@ -60496,10 +60628,17 @@ class TurnQueue {
|
|
|
60496
60628
|
this.emitQueueUpdated(chatKey, sessionAlias, key);
|
|
60497
60629
|
this.notifyQueuedPeerCancelled(droppedSecond);
|
|
60498
60630
|
}
|
|
60631
|
+
const droppedInterruptSecond = this.pendingInterrupts.get(key);
|
|
60632
|
+
if (droppedInterruptSecond) {
|
|
60633
|
+
this.pendingInterrupts.delete(key);
|
|
60634
|
+
this.emitPeerInterrupt("cancelled_before_start", droppedInterruptSecond.executionContext.chatKey, droppedInterruptSecond.executionContext.sessionAlias, droppedInterruptSecond);
|
|
60635
|
+
this.notifyQueuedPeerCancelled([droppedInterruptSecond]);
|
|
60636
|
+
this.emitQueueUpdated(chatKey, sessionAlias, key);
|
|
60637
|
+
}
|
|
60499
60638
|
}
|
|
60500
|
-
const cleared = !this.inFlight.has(key) && !this.draining.has(key);
|
|
60501
|
-
if (cleared) {
|
|
60502
|
-
this.removing.
|
|
60639
|
+
const cleared = !this.inFlight.has(key) && !this.draining.has(key) && !this.pendingInterrupts.has(key);
|
|
60640
|
+
if (cleared) {} else if (!guardPreexisting) {
|
|
60641
|
+
this.removing.delete(key);
|
|
60503
60642
|
}
|
|
60504
60643
|
return { cleared };
|
|
60505
60644
|
}
|
|
@@ -60508,6 +60647,14 @@ class TurnQueue {
|
|
|
60508
60647
|
}
|
|
60509
60648
|
cancelQueuedItem(chatKey, sessionAlias, itemId, concurrencyKey) {
|
|
60510
60649
|
const key = this.resolveKey(chatKey, sessionAlias, concurrencyKey);
|
|
60650
|
+
const pendingInterrupt = this.pendingInterrupts.get(key);
|
|
60651
|
+
if (pendingInterrupt?.id === itemId) {
|
|
60652
|
+
this.pendingInterrupts.delete(key);
|
|
60653
|
+
this.emitQueueUpdated(chatKey, sessionAlias, key);
|
|
60654
|
+
this.emitPeerInterrupt("cancelled_before_start", pendingInterrupt.executionContext.chatKey, pendingInterrupt.executionContext.sessionAlias, pendingInterrupt);
|
|
60655
|
+
this.notifyQueuedPeerCancelled([pendingInterrupt]);
|
|
60656
|
+
return { cancelled: true };
|
|
60657
|
+
}
|
|
60511
60658
|
const q = this.queues.get(key);
|
|
60512
60659
|
if (!q)
|
|
60513
60660
|
return { cancelled: false };
|
|
@@ -60578,6 +60725,7 @@ class ControlService {
|
|
|
60578
60725
|
...this.deps.onTurnIdleTimeout ? { onIdleTimeout: this.deps.onTurnIdleTimeout } : {},
|
|
60579
60726
|
...this.deps.cancelDrainTimeoutMs !== undefined ? { cancelDrainTimeoutMs: this.deps.cancelDrainTimeoutMs } : {},
|
|
60580
60727
|
...this.deps.onQueuedPeerCancelled ? { onQueuedPeerCancelled: this.deps.onQueuedPeerCancelled } : {},
|
|
60728
|
+
...this.deps.onPeerInterruptEvent ? { onPeerInterruptEvent: this.deps.onPeerInterruptEvent } : {},
|
|
60581
60729
|
emitQueueUpdated: (chatKey, sessionAlias, items) => this.deps.events.emit({
|
|
60582
60730
|
type: "queue-updated",
|
|
60583
60731
|
chatKey,
|
|
@@ -60865,6 +61013,7 @@ class ControlService {
|
|
|
60865
61013
|
return {
|
|
60866
61014
|
alias: toDisplaySessionAlias(session3.alias),
|
|
60867
61015
|
agent: session3.agent,
|
|
61016
|
+
driver: session3.driver,
|
|
60868
61017
|
workspace: session3.workspace,
|
|
60869
61018
|
transportSession: session3.transportSession,
|
|
60870
61019
|
running,
|
|
@@ -61101,7 +61250,7 @@ class ControlService {
|
|
|
61101
61250
|
if (!session3 || session3.archived === true) {
|
|
61102
61251
|
throw new AgentMessagingError("TARGET_UNAVAILABLE", "The target Agent session is not currently available or is archived.");
|
|
61103
61252
|
}
|
|
61104
|
-
const
|
|
61253
|
+
const peerParams = {
|
|
61105
61254
|
chatKey: input.chatKey,
|
|
61106
61255
|
sessionAlias: input.sessionAlias,
|
|
61107
61256
|
boundSessionAlias: internalAlias,
|
|
@@ -61113,17 +61262,28 @@ class ControlService {
|
|
|
61113
61262
|
allowRestoreArchived: false,
|
|
61114
61263
|
preserveCoordinatorRoute: true,
|
|
61115
61264
|
peerOrigin: input.peerOrigin
|
|
61116
|
-
}
|
|
61117
|
-
if (
|
|
61118
|
-
|
|
61119
|
-
|
|
61120
|
-
|
|
61121
|
-
if (admission.reason === "target-unavailable") {
|
|
61122
|
-
throw new AgentMessagingError("TARGET_UNAVAILABLE", "The target Agent session is currently being removed or archived.");
|
|
61265
|
+
};
|
|
61266
|
+
if (input.requestedMode === "interrupt") {
|
|
61267
|
+
const admission2 = this.turnQueue.submitPeerInterrupt(peerParams);
|
|
61268
|
+
if (admission2.status === "rejected") {
|
|
61269
|
+
this.throwPeerAdmissionError(admission2.reason, "interrupt");
|
|
61123
61270
|
}
|
|
61124
|
-
|
|
61271
|
+
return admission2.status === "injected" ? { status: "injected", modeUsed: "prompt", targetState: "idle" } : { status: "queued", modeUsed: "interrupt", targetState: "running" };
|
|
61272
|
+
}
|
|
61273
|
+
const admission = this.turnQueue.submitPeerTurn(peerParams);
|
|
61274
|
+
if (admission.status === "rejected") {
|
|
61275
|
+
this.throwPeerAdmissionError(admission.reason, "peer");
|
|
61125
61276
|
}
|
|
61126
|
-
return { status: admission.status };
|
|
61277
|
+
return { status: admission.status, modeUsed: "queue" };
|
|
61278
|
+
}
|
|
61279
|
+
throwPeerAdmissionError(reason, mode) {
|
|
61280
|
+
if (reason === "queue-full") {
|
|
61281
|
+
throw new AgentMessagingError("MESSAGE_QUEUE_FULL", mode === "interrupt" ? "The target Agent already has a pending peer interrupt; only one interrupt may be reserved at a time." : "The target Agent's message queue is full.");
|
|
61282
|
+
}
|
|
61283
|
+
if (reason === "target-unavailable") {
|
|
61284
|
+
throw new AgentMessagingError("TARGET_UNAVAILABLE", "The target Agent session is currently being removed or archived.");
|
|
61285
|
+
}
|
|
61286
|
+
throw new AgentMessagingError("DELIVERY_FAILED", `Peer turn admission rejected: ${reason}`);
|
|
61127
61287
|
}
|
|
61128
61288
|
async submitCompletionTurn(input) {
|
|
61129
61289
|
const session3 = await this.deps.sessions.getSession(input.sourceAlias);
|
|
@@ -61234,9 +61394,9 @@ var init_control_service = __esm(() => {
|
|
|
61234
61394
|
init_native_session_history();
|
|
61235
61395
|
init_workspace_fs();
|
|
61236
61396
|
init_workspace_git();
|
|
61397
|
+
init_turn_support();
|
|
61237
61398
|
init_session_turn_runner();
|
|
61238
61399
|
init_turn_queue();
|
|
61239
|
-
init_turn_support();
|
|
61240
61400
|
init_command_timeouts();
|
|
61241
61401
|
MODEL_SET_SETTLE_BUDGET_MS = 2 * (DEFAULT_MANAGEMENT_COMMAND_TIMEOUT_MS + BRIDGE_REQUEST_TIMEOUT_GRACE_MS);
|
|
61242
61402
|
});
|
|
@@ -62892,7 +63052,7 @@ async function buildApp(paths, deps = {}) {
|
|
|
62892
63052
|
return null;
|
|
62893
63053
|
}
|
|
62894
63054
|
},
|
|
62895
|
-
deliverLogicalTurn: async (alias, renderedText, messageId, peerOrigin) => {
|
|
63055
|
+
deliverLogicalTurn: async (alias, renderedText, messageId, peerOrigin, requestedMode) => {
|
|
62896
63056
|
if (controlRef) {
|
|
62897
63057
|
const chatKey = `relay:agent-message:${alias}`;
|
|
62898
63058
|
return await controlRef.submitPeerTurn({
|
|
@@ -62902,10 +63062,11 @@ async function buildApp(paths, deps = {}) {
|
|
|
62902
63062
|
text: renderedText,
|
|
62903
63063
|
senderId: "agent-messaging",
|
|
62904
63064
|
messageId,
|
|
63065
|
+
requestedMode,
|
|
62905
63066
|
peerOrigin
|
|
62906
63067
|
});
|
|
62907
63068
|
}
|
|
62908
|
-
return { status: "queued" };
|
|
63069
|
+
return { status: "queued", modeUsed: "queue" };
|
|
62909
63070
|
},
|
|
62910
63071
|
deliverCompletionTurn: async (alias, completion, requestMessageId) => {
|
|
62911
63072
|
if (controlRef) {
|
|
@@ -63090,6 +63251,16 @@ async function buildApp(paths, deps = {}) {
|
|
|
63090
63251
|
idleMs
|
|
63091
63252
|
});
|
|
63092
63253
|
},
|
|
63254
|
+
onPeerInterruptEvent: (event) => {
|
|
63255
|
+
logger.info(`agent_messaging.peer_interrupt_${event.kind}`, "peer interrupt lane event", {
|
|
63256
|
+
chatKey: event.chatKey,
|
|
63257
|
+
sessionAlias: event.sessionAlias,
|
|
63258
|
+
...event.requestMessageId !== undefined ? { requestMessageId: event.requestMessageId } : {},
|
|
63259
|
+
...event.promptRequestId !== undefined ? { promptRequestId: event.promptRequestId } : {},
|
|
63260
|
+
...event.pendingRequestMessageId !== undefined ? { pendingRequestMessageId: event.pendingRequestMessageId } : {},
|
|
63261
|
+
...event.predecessorWasAlreadyAborted !== undefined ? { predecessorWasAlreadyAborted: event.predecessorWasAlreadyAborted } : {}
|
|
63262
|
+
});
|
|
63263
|
+
},
|
|
63093
63264
|
agentMessaging: {
|
|
63094
63265
|
deliverInbound: async (input) => await agentMessaging.deliverInbound(input),
|
|
63095
63266
|
deliverInboundCompletion: async (input) => await agentMessaging.deliverInboundCompletion(input),
|
|
@@ -14,6 +14,9 @@ export interface QueuedItemInfo {
|
|
|
14
14
|
id: string;
|
|
15
15
|
textPreview: string;
|
|
16
16
|
enqueuedAt: string;
|
|
17
|
+
/** v0.4: present ONLY for a reserved-but-not-started peer interrupt
|
|
18
|
+
* (snapshot-first item). Additive and backward compatible. */
|
|
19
|
+
kind?: "interrupt";
|
|
17
20
|
}
|
|
18
21
|
export type ControlEvent = {
|
|
19
22
|
type: "turn-output";
|
|
@@ -6,15 +6,16 @@ import type { CreateScheduledTaskInput, ScheduledTaskService } from "../schedule
|
|
|
6
6
|
import type { ScheduledTaskRecord } from "../scheduled/scheduled-types";
|
|
7
7
|
import type { CancelTaskInput, OrchestrationService, OrchestrationTaskFilter } from "../orchestration/orchestration-service";
|
|
8
8
|
import type { OrchestrationTaskRecord } from "../orchestration/orchestration-types";
|
|
9
|
-
import type { AgentMessageCompletion } from "../orchestration/agent-messaging-types";
|
|
9
|
+
import type { AgentMessageCompletion, AgentMessageMode } from "../orchestration/agent-messaging-types";
|
|
10
10
|
import type { ControlEventBus } from "./control-event-bus";
|
|
11
11
|
import type { AgentCatalogEntry } from "../config/agent-catalog";
|
|
12
12
|
import { type BrowseDirsResult, type DirListing, type FileContent, type SearchOptions, type SearchResult, type WorkspaceDiff } from "./workspace-fs";
|
|
13
13
|
import { type GitCheckoutOptions, type GitCommitResult, type GitPushOptions, type GitStatus, type GitWorktreeCreateOptions, type GitWorktreeCreateResult } from "./workspace-git";
|
|
14
14
|
import type { AgentEndpointView, AgentMessageTraceRecord } from "../orchestration/agent-messaging-types";
|
|
15
15
|
import type { PromptAttachmentRef } from "@ganglion/xacpx-relay-protocol";
|
|
16
|
-
import type { UploadStore } from "./upload-store.js";
|
|
17
16
|
import { type TurnIdleTimeoutDetail, type PeerTurnOrigin } from "./turn-support";
|
|
17
|
+
import type { PeerInterruptEvent } from "./turn-queue";
|
|
18
|
+
import type { UploadStore } from "./upload-store.js";
|
|
18
19
|
import type { AppLogger } from "../logging/app-logger";
|
|
19
20
|
export interface ModelSetRequestOptions {
|
|
20
21
|
/** Connector-side deadline derived from the Hub request lifetime. */
|
|
@@ -23,6 +24,12 @@ export interface ModelSetRequestOptions {
|
|
|
23
24
|
export interface ControlSessionInfo {
|
|
24
25
|
alias: string;
|
|
25
26
|
agent: string;
|
|
27
|
+
/** The acpx driver backing `agent`, resolved from the agent's config at listing
|
|
28
|
+
* time. Lets the web render the brand icon without an agents-map lookup — which
|
|
29
|
+
* fails for sleeping sessions whose rows live outside the active session list.
|
|
30
|
+
* Optional: mirrors ResolvedSession.driver (custom agents always resolve one,
|
|
31
|
+
* but the type stays tolerant). */
|
|
32
|
+
driver?: string;
|
|
26
33
|
workspace: string;
|
|
27
34
|
transportSession: string;
|
|
28
35
|
running: boolean;
|
|
@@ -111,6 +118,7 @@ export interface ControlServiceDeps {
|
|
|
111
118
|
peerOrigin: PeerTurnOrigin;
|
|
112
119
|
promptRequestId?: string;
|
|
113
120
|
}) => void;
|
|
121
|
+
onPeerInterruptEvent?: (event: PeerInterruptEvent) => void;
|
|
114
122
|
agentMessaging?: {
|
|
115
123
|
deliverInbound(input: {
|
|
116
124
|
sourceNodeId: string;
|
|
@@ -394,10 +402,17 @@ export declare class ControlService {
|
|
|
394
402
|
text: string;
|
|
395
403
|
senderId: string;
|
|
396
404
|
messageId: string;
|
|
405
|
+
/** v0.4: "interrupt" routes through TurnQueue.submitPeerInterrupt (reserve →
|
|
406
|
+
* abort → true-settle → priority drain); every other mode keeps the
|
|
407
|
+
* non-cancelling peer path. */
|
|
408
|
+
requestedMode?: AgentMessageMode;
|
|
397
409
|
peerOrigin?: PeerTurnOrigin;
|
|
398
410
|
}): Promise<{
|
|
399
411
|
status: "injected" | "queued";
|
|
412
|
+
modeUsed: "prompt" | "queue" | "interrupt";
|
|
413
|
+
targetState?: "idle" | "running";
|
|
400
414
|
}>;
|
|
415
|
+
private throwPeerAdmissionError;
|
|
401
416
|
submitCompletionTurn(input: {
|
|
402
417
|
sourceAlias: string;
|
|
403
418
|
completion: AgentMessageCompletion;
|
|
@@ -5,6 +5,10 @@ export interface QueuedItemSnapshot {
|
|
|
5
5
|
id: string;
|
|
6
6
|
textPreview: string;
|
|
7
7
|
enqueuedAt: string;
|
|
8
|
+
/** v0.4: present ONLY for a reserved-but-not-started peer interrupt
|
|
9
|
+
* (snapshot-first item). Lets the existing per-item cancel surface reach
|
|
10
|
+
* the interrupt slot by id (spec §11.1) without a new RPC. */
|
|
11
|
+
kind?: "interrupt";
|
|
8
12
|
}
|
|
9
13
|
export interface TurnQueueDeps {
|
|
10
14
|
runTurn(req: TurnRequest, signal: AbortSignal, onActivity: () => void): Promise<TurnResult>;
|
|
@@ -21,6 +25,20 @@ export interface TurnQueueDeps {
|
|
|
21
25
|
peerOrigin: PeerTurnOrigin;
|
|
22
26
|
promptRequestId?: string;
|
|
23
27
|
}) => void;
|
|
28
|
+
onPeerInterruptEvent?: (event: PeerInterruptEvent) => void;
|
|
29
|
+
}
|
|
30
|
+
/** v0.4: structured peer-interrupt lifecycle event (spec §17). Ids and lane
|
|
31
|
+
* keys only — never message content. */
|
|
32
|
+
export interface PeerInterruptEvent {
|
|
33
|
+
kind: "reserved" | "abort_signalled" | "started" | "cancelled_before_start" | "rejected_pending";
|
|
34
|
+
chatKey: string;
|
|
35
|
+
sessionAlias: string;
|
|
36
|
+
requestMessageId?: string;
|
|
37
|
+
promptRequestId?: string;
|
|
38
|
+
/** rejected_pending only: requestMessageId of the reservation that occupied
|
|
39
|
+
* the slot (the event's own ids describe the REJECTED request). */
|
|
40
|
+
pendingRequestMessageId?: string;
|
|
41
|
+
predecessorWasAlreadyAborted?: boolean;
|
|
24
42
|
}
|
|
25
43
|
export interface SubmitParams {
|
|
26
44
|
chatKey: string;
|
|
@@ -42,6 +60,7 @@ export interface SubmitParams {
|
|
|
42
60
|
allowRestoreArchived?: boolean;
|
|
43
61
|
preserveCoordinatorRoute?: boolean;
|
|
44
62
|
/** Hub-issued pre-write correlation; stored on the queue item and carried onto the
|
|
63
|
+
|
|
45
64
|
* drained turn-started so the hub can correlate a queue item back to its
|
|
46
65
|
* pre-written inbound row (see PromptPayload.promptRequestId). */
|
|
47
66
|
promptRequestId?: string;
|
|
@@ -56,6 +75,23 @@ export type SubmitResult = TurnResult | {
|
|
|
56
75
|
queued: true;
|
|
57
76
|
queueItemId: string;
|
|
58
77
|
};
|
|
78
|
+
/** v0.4: result of `submitPeerInterrupt`. `queued`+`interrupt` = busy target:
|
|
79
|
+
* the reservation is held and the predecessor cancel has been signalled.
|
|
80
|
+
* `injected`+`prompt` = idle target: ordinary synchronous peer admission with
|
|
81
|
+
* zero cancellations. Rejections reuse the existing reasons ("target-unavailable",
|
|
82
|
+
* "queue-full" — the latter also enforces the one-pending-interrupt slot rule,
|
|
83
|
+
* spec §8/§16). */
|
|
84
|
+
export type PeerInterruptAdmission = {
|
|
85
|
+
status: "injected";
|
|
86
|
+
modeUsed: "prompt";
|
|
87
|
+
} | {
|
|
88
|
+
status: "queued";
|
|
89
|
+
modeUsed: "interrupt";
|
|
90
|
+
queueItemId: string;
|
|
91
|
+
} | {
|
|
92
|
+
status: "rejected";
|
|
93
|
+
reason: string;
|
|
94
|
+
};
|
|
59
95
|
export declare class TurnQueue {
|
|
60
96
|
private readonly deps;
|
|
61
97
|
private readonly setTimer;
|
|
@@ -72,6 +108,7 @@ export declare class TurnQueue {
|
|
|
72
108
|
private readonly settledRequestIds;
|
|
73
109
|
private static readonly SETTLED_REQUEST_IDS_MAX;
|
|
74
110
|
private readonly queues;
|
|
111
|
+
private readonly pendingInterrupts;
|
|
75
112
|
private readonly draining;
|
|
76
113
|
private readonly removing;
|
|
77
114
|
private resolveKey;
|
|
@@ -83,6 +120,20 @@ export declare class TurnQueue {
|
|
|
83
120
|
status: "rejected";
|
|
84
121
|
reason: string;
|
|
85
122
|
};
|
|
123
|
+
/** v0.4 test/observability seam: 1 when a reserved-but-not-started peer
|
|
124
|
+
* interrupt occupies the lane's slot, else 0 (spec §7: at most one). */
|
|
125
|
+
pendingInterruptCount(chatKey: string, sessionAlias: string, concurrencyKey?: string): number;
|
|
126
|
+
/**
|
|
127
|
+
* v0.4 Peer Interrupt Delivery (spec §6.4/§9): atomic reserve → abort →
|
|
128
|
+
* true-settle → priority-drain, owned entirely by this lane. Synchronous:
|
|
129
|
+
* by the time the accepted result returns, the reservation exists in the
|
|
130
|
+
* lane and a live predecessor's AbortController has been signalled. The
|
|
131
|
+
* interrupting turn itself starts only after the predecessor's `settled`
|
|
132
|
+
* resolves and its inFlight entry is gone (drain hand-off in advanceQueue).
|
|
133
|
+
* Router-level cancel();submit() races are impossible by construction here.
|
|
134
|
+
*/
|
|
135
|
+
submitPeerInterrupt(params: SubmitParams): PeerInterruptAdmission;
|
|
136
|
+
private emitPeerInterrupt;
|
|
86
137
|
private hasSettledRequestId;
|
|
87
138
|
private recordSettledRequestId;
|
|
88
139
|
/** Terminal cancellation for queued peer items dropped before execution. */
|
|
@@ -90,6 +141,7 @@ export declare class TurnQueue {
|
|
|
90
141
|
private emitQueueUpdated;
|
|
91
142
|
submit(params: SubmitParams): Promise<SubmitResult>;
|
|
92
143
|
private advanceQueue;
|
|
144
|
+
private drainQueuedPrompt;
|
|
93
145
|
cancelTurn(chatKey: string, sessionAlias: string, concurrencyKey?: string): boolean;
|
|
94
146
|
/** Tear down all turn state for a session that is being removed or archived: drop every
|
|
95
147
|
* queued prompt, abort a running turn, and wait (bounded) for it to unwind. The queue is
|
|
@@ -104,9 +156,14 @@ export declare class TurnQueue {
|
|
|
104
156
|
* in-flight turn and dropped every queued prompt (emitting `queue-updated([])`). The caller
|
|
105
157
|
* should surface a retry, not present the failure as a no-op.
|
|
106
158
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
159
|
+
* The teardown guard (`removing`) is armed SYNCHRONOUSLY on ENTRY — before
|
|
160
|
+
* any await — so peer admissions (`submitPeerTurn` / `submitPeerInterrupt`)
|
|
161
|
+
* fail closed `target-unavailable` for the ENTIRE clear lifecycle; a peer
|
|
162
|
+
* interrupt arriving mid-unwind can never be reserved and drained onto the
|
|
163
|
+
* dying session. On `cleared: true` the guard stays armed across the
|
|
164
|
+
* caller's transport teardown; on `cleared: false` it is released so a
|
|
165
|
+
* retry sees a usable lane. The caller MUST call `finishClear` once
|
|
166
|
+
* teardown settles (success or failure) to release the guard. */
|
|
110
167
|
clearSession(chatKey: string, sessionAlias: string, concurrencyKey?: string): Promise<{
|
|
111
168
|
cleared: boolean;
|
|
112
169
|
}>;
|