@ganglion/xacpx 0.23.0-beta.4 → 0.23.0-beta.6
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.
|
@@ -3529,7 +3529,7 @@ function buildToolUseEvent(update, driver) {
|
|
|
3529
3529
|
const content = update.content;
|
|
3530
3530
|
const rawOutput = update.rawOutput ?? claudeToolResponse;
|
|
3531
3531
|
const locations = update.locations;
|
|
3532
|
-
const machineToolName = (isClaudeDriver ? update._meta?.claudeCode?.toolName?.trim() : "") || (driver === "qoder" ? update._meta?.qoder?.toolName?.trim() : "") || (driver === "codex" ? codexMcpMachineToolName(update.rawInput) : "") || (driver === "cursor" ? machineToolNameFromCursorInput(update.rawInput) : "") || undefined;
|
|
3532
|
+
const machineToolName = (isClaudeDriver ? update._meta?.claudeCode?.toolName?.trim() : "") || (driver === "qoder" ? update._meta?.qoder?.toolName?.trim() : "") || (driver === "codex" ? codexMcpMachineToolName(update.rawInput) : "") || (driver === "cursor" ? machineToolNameFromCursorInput(update.rawInput) : "") || (driver === "omp" ? ompXdevMachineToolName(update.rawInput, update.rawOutput) : "") || undefined;
|
|
3533
3533
|
const parentToolCallId = claudeMeta?.parentToolUseId?.trim() || update.parentToolCallId?.trim();
|
|
3534
3534
|
const isSubagent = claudeMeta?.toolName === "Agent" || driver === "qoder" && update._meta?.qoder?.toolName === "Agent" || driver === "kimi" && isKimiSubagentInput(rawInput) || driver === "codex" && isCodexSubagentMeta(update._meta?.codex?.subagent) || driver === "cursor" && isCursorSubagentInput(rawInput, title);
|
|
3535
3535
|
return {
|
|
@@ -3558,6 +3558,23 @@ function codexMcpMachineToolName(rawInput) {
|
|
|
3558
3558
|
return "";
|
|
3559
3559
|
return tool.trim();
|
|
3560
3560
|
}
|
|
3561
|
+
function ompXdevMachineToolName(rawInput, rawOutput) {
|
|
3562
|
+
const details = isRecord2(rawOutput) ? rec2(rawOutput.details) : undefined;
|
|
3563
|
+
const xdev = details ? rec2(details.xdev) : undefined;
|
|
3564
|
+
if (typeof xdev?.tool === "string" && xdev.tool.trim().length > 0) {
|
|
3565
|
+
return xdev.tool.trim();
|
|
3566
|
+
}
|
|
3567
|
+
const path2 = isRecord2(rawInput) && typeof rawInput.path === "string" ? rawInput.path.trim() : "";
|
|
3568
|
+
if (path2.toLowerCase().startsWith("xd://")) {
|
|
3569
|
+
const name = path2.slice(5).split(/[/?#]/)[0] ?? "";
|
|
3570
|
+
if (name.length > 0)
|
|
3571
|
+
return name;
|
|
3572
|
+
}
|
|
3573
|
+
return "";
|
|
3574
|
+
}
|
|
3575
|
+
function rec2(v) {
|
|
3576
|
+
return isRecord2(v) ? v : undefined;
|
|
3577
|
+
}
|
|
3561
3578
|
function machineToolNameFromCursorInput(rawInput) {
|
|
3562
3579
|
if (!isRecord2(rawInput))
|
|
3563
3580
|
return;
|
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);
|
|
@@ -56318,7 +56323,7 @@ function buildToolUseEvent(update, driver) {
|
|
|
56318
56323
|
const content = update.content;
|
|
56319
56324
|
const rawOutput = update.rawOutput ?? claudeToolResponse;
|
|
56320
56325
|
const locations = update.locations;
|
|
56321
|
-
const machineToolName = (isClaudeDriver ? update._meta?.claudeCode?.toolName?.trim() : "") || (driver === "qoder" ? update._meta?.qoder?.toolName?.trim() : "") || (driver === "codex" ? codexMcpMachineToolName(update.rawInput) : "") || (driver === "cursor" ? machineToolNameFromCursorInput(update.rawInput) : "") || undefined;
|
|
56326
|
+
const machineToolName = (isClaudeDriver ? update._meta?.claudeCode?.toolName?.trim() : "") || (driver === "qoder" ? update._meta?.qoder?.toolName?.trim() : "") || (driver === "codex" ? codexMcpMachineToolName(update.rawInput) : "") || (driver === "cursor" ? machineToolNameFromCursorInput(update.rawInput) : "") || (driver === "omp" ? ompXdevMachineToolName(update.rawInput, update.rawOutput) : "") || undefined;
|
|
56322
56327
|
const parentToolCallId = claudeMeta?.parentToolUseId?.trim() || update.parentToolCallId?.trim();
|
|
56323
56328
|
const isSubagent = claudeMeta?.toolName === "Agent" || driver === "qoder" && update._meta?.qoder?.toolName === "Agent" || driver === "kimi" && isKimiSubagentInput(rawInput) || driver === "codex" && isCodexSubagentMeta(update._meta?.codex?.subagent) || driver === "cursor" && isCursorSubagentInput(rawInput, title);
|
|
56324
56329
|
return {
|
|
@@ -56347,6 +56352,23 @@ function codexMcpMachineToolName(rawInput) {
|
|
|
56347
56352
|
return "";
|
|
56348
56353
|
return tool.trim();
|
|
56349
56354
|
}
|
|
56355
|
+
function ompXdevMachineToolName(rawInput, rawOutput) {
|
|
56356
|
+
const details = isRecord5(rawOutput) ? rec2(rawOutput.details) : undefined;
|
|
56357
|
+
const xdev = details ? rec2(details.xdev) : undefined;
|
|
56358
|
+
if (typeof xdev?.tool === "string" && xdev.tool.trim().length > 0) {
|
|
56359
|
+
return xdev.tool.trim();
|
|
56360
|
+
}
|
|
56361
|
+
const path14 = isRecord5(rawInput) && typeof rawInput.path === "string" ? rawInput.path.trim() : "";
|
|
56362
|
+
if (path14.toLowerCase().startsWith("xd://")) {
|
|
56363
|
+
const name = path14.slice(5).split(/[/?#]/)[0] ?? "";
|
|
56364
|
+
if (name.length > 0)
|
|
56365
|
+
return name;
|
|
56366
|
+
}
|
|
56367
|
+
return "";
|
|
56368
|
+
}
|
|
56369
|
+
function rec2(v) {
|
|
56370
|
+
return isRecord5(v) ? v : undefined;
|
|
56371
|
+
}
|
|
56350
56372
|
function machineToolNameFromCursorInput(rawInput) {
|
|
56351
56373
|
if (!isRecord5(rawInput))
|
|
56352
56374
|
return;
|
|
@@ -60139,6 +60161,7 @@ class TurnQueue {
|
|
|
60139
60161
|
settledRequestIds = new Map;
|
|
60140
60162
|
static SETTLED_REQUEST_IDS_MAX = 2000;
|
|
60141
60163
|
queues = new Map;
|
|
60164
|
+
pendingInterrupts = new Map;
|
|
60142
60165
|
draining = new Set;
|
|
60143
60166
|
removing = new Set;
|
|
60144
60167
|
resolveKey(chatKey, sessionAlias, concurrencyKey) {
|
|
@@ -60146,7 +60169,7 @@ class TurnQueue {
|
|
|
60146
60169
|
return concurrencyKey;
|
|
60147
60170
|
}
|
|
60148
60171
|
const directKey = turnKey(chatKey, sessionAlias);
|
|
60149
|
-
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)) {
|
|
60150
60173
|
return directKey;
|
|
60151
60174
|
}
|
|
60152
60175
|
const cleanAlias = sessionAlias.includes(":") ? sessionAlias.split(":").pop() : sessionAlias;
|
|
@@ -60166,6 +60189,10 @@ class TurnQueue {
|
|
|
60166
60189
|
if (k === directKey || k.endsWith(":" + cleanAlias) || k.endsWith(" " + cleanAlias) || k === cleanAlias)
|
|
60167
60190
|
return k;
|
|
60168
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
|
+
}
|
|
60169
60196
|
return directKey;
|
|
60170
60197
|
}
|
|
60171
60198
|
queueLength(chatKey, sessionAlias, concurrencyKey) {
|
|
@@ -60237,6 +60264,99 @@ class TurnQueue {
|
|
|
60237
60264
|
this.recordSettledRequestId(params.promptRequestId);
|
|
60238
60265
|
return { status: "injected" };
|
|
60239
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
|
+
}
|
|
60240
60360
|
hasSettledRequestId(id) {
|
|
60241
60361
|
const expiresAt = this.settledRequestIds.get(id);
|
|
60242
60362
|
if (expiresAt === undefined)
|
|
@@ -60276,11 +60396,20 @@ class TurnQueue {
|
|
|
60276
60396
|
}
|
|
60277
60397
|
emitQueueUpdated(chatKey, sessionAlias, queueKey) {
|
|
60278
60398
|
const key = queueKey ?? this.resolveKey(chatKey, sessionAlias);
|
|
60279
|
-
const
|
|
60280
|
-
|
|
60281
|
-
|
|
60282
|
-
|
|
60283
|
-
|
|
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
|
+
}
|
|
60284
60413
|
this.deps.emitQueueUpdated(chatKey, sessionAlias, items);
|
|
60285
60414
|
}
|
|
60286
60415
|
async submit(params) {
|
|
@@ -60401,7 +60530,7 @@ class TurnQueue {
|
|
|
60401
60530
|
} finally {
|
|
60402
60531
|
if (idleTimer)
|
|
60403
60532
|
this.clearTimer(idleTimer);
|
|
60404
|
-
if ((this.queues.get(key)?.length ?? 0) > 0) {
|
|
60533
|
+
if ((this.queues.get(key)?.length ?? 0) > 0 || this.pendingInterrupts.has(key)) {
|
|
60405
60534
|
this.draining.add(key);
|
|
60406
60535
|
}
|
|
60407
60536
|
const detection = result?.postTurnDetection;
|
|
@@ -60418,40 +60547,50 @@ class TurnQueue {
|
|
|
60418
60547
|
};
|
|
60419
60548
|
}
|
|
60420
60549
|
advanceQueue(key) {
|
|
60550
|
+
const interrupt = this.pendingInterrupts.get(key);
|
|
60551
|
+
if (interrupt) {
|
|
60552
|
+
this.pendingInterrupts.delete(key);
|
|
60553
|
+
this.emitPeerInterrupt("started", interrupt.executionContext.chatKey, interrupt.executionContext.sessionAlias, interrupt);
|
|
60554
|
+
this.drainQueuedPrompt(key, interrupt);
|
|
60555
|
+
return;
|
|
60556
|
+
}
|
|
60421
60557
|
const q = this.queues.get(key);
|
|
60422
60558
|
const next = q?.shift();
|
|
60423
60559
|
if (q && q.length === 0)
|
|
60424
60560
|
this.queues.delete(key);
|
|
60425
60561
|
if (next) {
|
|
60426
|
-
this.
|
|
60427
|
-
const concurrencyKey = next.concurrencyKey ?? key;
|
|
60428
|
-
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 } : {} };
|
|
60429
|
-
this.submit({
|
|
60430
|
-
chatKey: next.executionContext.chatKey,
|
|
60431
|
-
sessionAlias: next.executionContext.sessionAlias,
|
|
60432
|
-
boundSessionAlias: next.executionContext.boundSessionAlias,
|
|
60433
|
-
concurrencyKey,
|
|
60434
|
-
text: next.text,
|
|
60435
|
-
senderId: next.senderId,
|
|
60436
|
-
queueable: true,
|
|
60437
|
-
drained: true,
|
|
60438
|
-
isPeerMessage: next.isPeerMessage,
|
|
60439
|
-
allowRestoreArchived: next.allowRestoreArchived,
|
|
60440
|
-
preserveCoordinatorRoute: next.preserveCoordinatorRoute,
|
|
60441
|
-
promptRequestId: next.promptRequestId,
|
|
60442
|
-
peerOrigin: next.peerOrigin,
|
|
60443
|
-
trustedPeerCompletion: next.trustedPeerCompletion,
|
|
60444
|
-
...next.isOwner !== undefined ? { isOwner: next.isOwner } : {},
|
|
60445
|
-
...next.accountId !== undefined ? { accountId: next.accountId } : {},
|
|
60446
|
-
...next.media !== undefined ? { media: next.media } : {},
|
|
60447
|
-
...next.agentMentions !== undefined ? { agentMentions: next.agentMentions } : {},
|
|
60448
|
-
turnStarted
|
|
60449
|
-
});
|
|
60562
|
+
this.drainQueuedPrompt(key, next);
|
|
60450
60563
|
} else {
|
|
60451
60564
|
this.draining.delete(key);
|
|
60452
60565
|
this.inFlight.delete(key);
|
|
60453
60566
|
}
|
|
60454
60567
|
}
|
|
60568
|
+
drainQueuedPrompt(key, next) {
|
|
60569
|
+
this.emitQueueUpdated(next.executionContext.chatKey, next.executionContext.sessionAlias, key);
|
|
60570
|
+
const concurrencyKey = next.concurrencyKey ?? key;
|
|
60571
|
+
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 } : {} };
|
|
60572
|
+
this.submit({
|
|
60573
|
+
chatKey: next.executionContext.chatKey,
|
|
60574
|
+
sessionAlias: next.executionContext.sessionAlias,
|
|
60575
|
+
boundSessionAlias: next.executionContext.boundSessionAlias,
|
|
60576
|
+
concurrencyKey,
|
|
60577
|
+
text: next.text,
|
|
60578
|
+
senderId: next.senderId,
|
|
60579
|
+
queueable: true,
|
|
60580
|
+
drained: true,
|
|
60581
|
+
isPeerMessage: next.isPeerMessage,
|
|
60582
|
+
allowRestoreArchived: next.allowRestoreArchived,
|
|
60583
|
+
preserveCoordinatorRoute: next.preserveCoordinatorRoute,
|
|
60584
|
+
promptRequestId: next.promptRequestId,
|
|
60585
|
+
peerOrigin: next.peerOrigin,
|
|
60586
|
+
trustedPeerCompletion: next.trustedPeerCompletion,
|
|
60587
|
+
...next.isOwner !== undefined ? { isOwner: next.isOwner } : {},
|
|
60588
|
+
...next.accountId !== undefined ? { accountId: next.accountId } : {},
|
|
60589
|
+
...next.media !== undefined ? { media: next.media } : {},
|
|
60590
|
+
...next.agentMentions !== undefined ? { agentMentions: next.agentMentions } : {},
|
|
60591
|
+
turnStarted
|
|
60592
|
+
});
|
|
60593
|
+
}
|
|
60455
60594
|
cancelTurn(chatKey, sessionAlias, concurrencyKey) {
|
|
60456
60595
|
const key = this.resolveKey(chatKey, sessionAlias, concurrencyKey);
|
|
60457
60596
|
const entry = this.inFlight.get(key);
|
|
@@ -60463,12 +60602,21 @@ class TurnQueue {
|
|
|
60463
60602
|
}
|
|
60464
60603
|
async clearSession(chatKey, sessionAlias, concurrencyKey) {
|
|
60465
60604
|
const key = this.resolveKey(chatKey, sessionAlias, concurrencyKey);
|
|
60605
|
+
const guardPreexisting = this.removing.has(key);
|
|
60606
|
+
this.removing.add(key);
|
|
60466
60607
|
const droppedFirst = this.queues.get(key);
|
|
60467
60608
|
if (droppedFirst) {
|
|
60468
60609
|
this.queues.delete(key);
|
|
60469
60610
|
this.emitQueueUpdated(chatKey, sessionAlias, key);
|
|
60470
60611
|
this.notifyQueuedPeerCancelled(droppedFirst);
|
|
60471
60612
|
}
|
|
60613
|
+
const droppedInterruptFirst = this.pendingInterrupts.get(key);
|
|
60614
|
+
if (droppedInterruptFirst) {
|
|
60615
|
+
this.pendingInterrupts.delete(key);
|
|
60616
|
+
this.emitPeerInterrupt("cancelled_before_start", droppedInterruptFirst.executionContext.chatKey, droppedInterruptFirst.executionContext.sessionAlias, droppedInterruptFirst);
|
|
60617
|
+
this.notifyQueuedPeerCancelled([droppedInterruptFirst]);
|
|
60618
|
+
this.emitQueueUpdated(chatKey, sessionAlias, key);
|
|
60619
|
+
}
|
|
60472
60620
|
const entry = this.inFlight.get(key);
|
|
60473
60621
|
if (entry) {
|
|
60474
60622
|
entry.controller.abort();
|
|
@@ -60479,10 +60627,17 @@ class TurnQueue {
|
|
|
60479
60627
|
this.emitQueueUpdated(chatKey, sessionAlias, key);
|
|
60480
60628
|
this.notifyQueuedPeerCancelled(droppedSecond);
|
|
60481
60629
|
}
|
|
60630
|
+
const droppedInterruptSecond = this.pendingInterrupts.get(key);
|
|
60631
|
+
if (droppedInterruptSecond) {
|
|
60632
|
+
this.pendingInterrupts.delete(key);
|
|
60633
|
+
this.emitPeerInterrupt("cancelled_before_start", droppedInterruptSecond.executionContext.chatKey, droppedInterruptSecond.executionContext.sessionAlias, droppedInterruptSecond);
|
|
60634
|
+
this.notifyQueuedPeerCancelled([droppedInterruptSecond]);
|
|
60635
|
+
this.emitQueueUpdated(chatKey, sessionAlias, key);
|
|
60636
|
+
}
|
|
60482
60637
|
}
|
|
60483
|
-
const cleared = !this.inFlight.has(key) && !this.draining.has(key);
|
|
60484
|
-
if (cleared) {
|
|
60485
|
-
this.removing.
|
|
60638
|
+
const cleared = !this.inFlight.has(key) && !this.draining.has(key) && !this.pendingInterrupts.has(key);
|
|
60639
|
+
if (cleared) {} else if (!guardPreexisting) {
|
|
60640
|
+
this.removing.delete(key);
|
|
60486
60641
|
}
|
|
60487
60642
|
return { cleared };
|
|
60488
60643
|
}
|
|
@@ -60491,6 +60646,14 @@ class TurnQueue {
|
|
|
60491
60646
|
}
|
|
60492
60647
|
cancelQueuedItem(chatKey, sessionAlias, itemId, concurrencyKey) {
|
|
60493
60648
|
const key = this.resolveKey(chatKey, sessionAlias, concurrencyKey);
|
|
60649
|
+
const pendingInterrupt = this.pendingInterrupts.get(key);
|
|
60650
|
+
if (pendingInterrupt?.id === itemId) {
|
|
60651
|
+
this.pendingInterrupts.delete(key);
|
|
60652
|
+
this.emitQueueUpdated(chatKey, sessionAlias, key);
|
|
60653
|
+
this.emitPeerInterrupt("cancelled_before_start", pendingInterrupt.executionContext.chatKey, pendingInterrupt.executionContext.sessionAlias, pendingInterrupt);
|
|
60654
|
+
this.notifyQueuedPeerCancelled([pendingInterrupt]);
|
|
60655
|
+
return { cancelled: true };
|
|
60656
|
+
}
|
|
60494
60657
|
const q = this.queues.get(key);
|
|
60495
60658
|
if (!q)
|
|
60496
60659
|
return { cancelled: false };
|
|
@@ -60561,6 +60724,7 @@ class ControlService {
|
|
|
60561
60724
|
...this.deps.onTurnIdleTimeout ? { onIdleTimeout: this.deps.onTurnIdleTimeout } : {},
|
|
60562
60725
|
...this.deps.cancelDrainTimeoutMs !== undefined ? { cancelDrainTimeoutMs: this.deps.cancelDrainTimeoutMs } : {},
|
|
60563
60726
|
...this.deps.onQueuedPeerCancelled ? { onQueuedPeerCancelled: this.deps.onQueuedPeerCancelled } : {},
|
|
60727
|
+
...this.deps.onPeerInterruptEvent ? { onPeerInterruptEvent: this.deps.onPeerInterruptEvent } : {},
|
|
60564
60728
|
emitQueueUpdated: (chatKey, sessionAlias, items) => this.deps.events.emit({
|
|
60565
60729
|
type: "queue-updated",
|
|
60566
60730
|
chatKey,
|
|
@@ -60848,6 +61012,7 @@ class ControlService {
|
|
|
60848
61012
|
return {
|
|
60849
61013
|
alias: toDisplaySessionAlias(session3.alias),
|
|
60850
61014
|
agent: session3.agent,
|
|
61015
|
+
driver: session3.driver,
|
|
60851
61016
|
workspace: session3.workspace,
|
|
60852
61017
|
transportSession: session3.transportSession,
|
|
60853
61018
|
running,
|
|
@@ -61084,7 +61249,7 @@ class ControlService {
|
|
|
61084
61249
|
if (!session3 || session3.archived === true) {
|
|
61085
61250
|
throw new AgentMessagingError("TARGET_UNAVAILABLE", "The target Agent session is not currently available or is archived.");
|
|
61086
61251
|
}
|
|
61087
|
-
const
|
|
61252
|
+
const peerParams = {
|
|
61088
61253
|
chatKey: input.chatKey,
|
|
61089
61254
|
sessionAlias: input.sessionAlias,
|
|
61090
61255
|
boundSessionAlias: internalAlias,
|
|
@@ -61096,17 +61261,28 @@ class ControlService {
|
|
|
61096
61261
|
allowRestoreArchived: false,
|
|
61097
61262
|
preserveCoordinatorRoute: true,
|
|
61098
61263
|
peerOrigin: input.peerOrigin
|
|
61099
|
-
}
|
|
61100
|
-
if (
|
|
61101
|
-
|
|
61102
|
-
|
|
61103
|
-
|
|
61104
|
-
if (admission.reason === "target-unavailable") {
|
|
61105
|
-
throw new AgentMessagingError("TARGET_UNAVAILABLE", "The target Agent session is currently being removed or archived.");
|
|
61264
|
+
};
|
|
61265
|
+
if (input.requestedMode === "interrupt") {
|
|
61266
|
+
const admission2 = this.turnQueue.submitPeerInterrupt(peerParams);
|
|
61267
|
+
if (admission2.status === "rejected") {
|
|
61268
|
+
this.throwPeerAdmissionError(admission2.reason, "interrupt");
|
|
61106
61269
|
}
|
|
61107
|
-
|
|
61270
|
+
return admission2.status === "injected" ? { status: "injected", modeUsed: "prompt", targetState: "idle" } : { status: "queued", modeUsed: "interrupt", targetState: "running" };
|
|
61271
|
+
}
|
|
61272
|
+
const admission = this.turnQueue.submitPeerTurn(peerParams);
|
|
61273
|
+
if (admission.status === "rejected") {
|
|
61274
|
+
this.throwPeerAdmissionError(admission.reason, "peer");
|
|
61108
61275
|
}
|
|
61109
|
-
return { status: admission.status };
|
|
61276
|
+
return { status: admission.status, modeUsed: "queue" };
|
|
61277
|
+
}
|
|
61278
|
+
throwPeerAdmissionError(reason, mode) {
|
|
61279
|
+
if (reason === "queue-full") {
|
|
61280
|
+
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.");
|
|
61281
|
+
}
|
|
61282
|
+
if (reason === "target-unavailable") {
|
|
61283
|
+
throw new AgentMessagingError("TARGET_UNAVAILABLE", "The target Agent session is currently being removed or archived.");
|
|
61284
|
+
}
|
|
61285
|
+
throw new AgentMessagingError("DELIVERY_FAILED", `Peer turn admission rejected: ${reason}`);
|
|
61110
61286
|
}
|
|
61111
61287
|
async submitCompletionTurn(input) {
|
|
61112
61288
|
const session3 = await this.deps.sessions.getSession(input.sourceAlias);
|
|
@@ -61217,9 +61393,9 @@ var init_control_service = __esm(() => {
|
|
|
61217
61393
|
init_native_session_history();
|
|
61218
61394
|
init_workspace_fs();
|
|
61219
61395
|
init_workspace_git();
|
|
61396
|
+
init_turn_support();
|
|
61220
61397
|
init_session_turn_runner();
|
|
61221
61398
|
init_turn_queue();
|
|
61222
|
-
init_turn_support();
|
|
61223
61399
|
init_command_timeouts();
|
|
61224
61400
|
MODEL_SET_SETTLE_BUDGET_MS = 2 * (DEFAULT_MANAGEMENT_COMMAND_TIMEOUT_MS + BRIDGE_REQUEST_TIMEOUT_GRACE_MS);
|
|
61225
61401
|
});
|
|
@@ -62875,7 +63051,7 @@ async function buildApp(paths, deps = {}) {
|
|
|
62875
63051
|
return null;
|
|
62876
63052
|
}
|
|
62877
63053
|
},
|
|
62878
|
-
deliverLogicalTurn: async (alias, renderedText, messageId, peerOrigin) => {
|
|
63054
|
+
deliverLogicalTurn: async (alias, renderedText, messageId, peerOrigin, requestedMode) => {
|
|
62879
63055
|
if (controlRef) {
|
|
62880
63056
|
const chatKey = `relay:agent-message:${alias}`;
|
|
62881
63057
|
return await controlRef.submitPeerTurn({
|
|
@@ -62885,10 +63061,11 @@ async function buildApp(paths, deps = {}) {
|
|
|
62885
63061
|
text: renderedText,
|
|
62886
63062
|
senderId: "agent-messaging",
|
|
62887
63063
|
messageId,
|
|
63064
|
+
requestedMode,
|
|
62888
63065
|
peerOrigin
|
|
62889
63066
|
});
|
|
62890
63067
|
}
|
|
62891
|
-
return { status: "queued" };
|
|
63068
|
+
return { status: "queued", modeUsed: "queue" };
|
|
62892
63069
|
},
|
|
62893
63070
|
deliverCompletionTurn: async (alias, completion, requestMessageId) => {
|
|
62894
63071
|
if (controlRef) {
|
|
@@ -63073,6 +63250,16 @@ async function buildApp(paths, deps = {}) {
|
|
|
63073
63250
|
idleMs
|
|
63074
63251
|
});
|
|
63075
63252
|
},
|
|
63253
|
+
onPeerInterruptEvent: (event) => {
|
|
63254
|
+
logger.info(`agent_messaging.peer_interrupt_${event.kind}`, "peer interrupt lane event", {
|
|
63255
|
+
chatKey: event.chatKey,
|
|
63256
|
+
sessionAlias: event.sessionAlias,
|
|
63257
|
+
...event.requestMessageId !== undefined ? { requestMessageId: event.requestMessageId } : {},
|
|
63258
|
+
...event.promptRequestId !== undefined ? { promptRequestId: event.promptRequestId } : {},
|
|
63259
|
+
...event.pendingRequestMessageId !== undefined ? { pendingRequestMessageId: event.pendingRequestMessageId } : {},
|
|
63260
|
+
...event.predecessorWasAlreadyAborted !== undefined ? { predecessorWasAlreadyAborted: event.predecessorWasAlreadyAborted } : {}
|
|
63261
|
+
});
|
|
63262
|
+
},
|
|
63076
63263
|
agentMessaging: {
|
|
63077
63264
|
deliverInbound: async (input) => await agentMessaging.deliverInbound(input),
|
|
63078
63265
|
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
|
}>;
|