@llblab/pi-telegram 0.55.0 → 0.56.0
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/AGENTS.md +3 -2
- package/BACKLOG.md +4 -1
- package/CHANGELOG.md +14 -0
- package/README.md +27 -18
- package/dist/lib/bindings.d.ts +8 -4
- package/dist/lib/bindings.js +41 -1
- package/dist/lib/bus-follower.d.ts +1 -0
- package/dist/lib/bus-follower.js +5 -1
- package/dist/lib/bus-leader.js +9 -1
- package/dist/lib/bus.d.ts +5 -0
- package/dist/lib/bus.js +17 -6
- package/dist/lib/extension.js +14 -2
- package/dist/lib/lifecycle.d.ts +2 -1
- package/dist/lib/lifecycle.js +28 -20
- package/dist/lib/menu-queue.d.ts +1 -0
- package/dist/lib/menu-queue.js +102 -99
- package/dist/lib/pi.d.ts +3 -2
- package/dist/lib/pi.js +3 -0
- package/dist/lib/polling.js +3 -1
- package/dist/lib/queue.d.ts +57 -5
- package/dist/lib/queue.js +313 -25
- package/dist/lib/routing.d.ts +1 -1
- package/dist/lib/routing.js +261 -29
- package/dist/lib/status.d.ts +1 -2
- package/dist/lib/status.js +6 -7
- package/dist/lib/turns.d.ts +4 -0
- package/dist/lib/turns.js +6 -0
- package/dist/lib/updates.d.ts +10 -6
- package/dist/lib/updates.js +20 -4
- package/dist/package.json +1 -1
- package/docs/README.md +1 -1
- package/docs/architecture.md +17 -14
- package/docs/callback-namespaces.md +4 -0
- package/docs/multi-instance-bus.md +4 -2
- package/docs/ui-style.md +14 -10
- package/lib/bindings.ts +52 -3
- package/lib/bus-follower.ts +5 -0
- package/lib/bus-leader.ts +9 -0
- package/lib/bus.ts +20 -3
- package/lib/extension.ts +14 -1
- package/lib/lifecycle.ts +34 -28
- package/lib/menu-queue.ts +116 -162
- package/lib/pi.ts +8 -0
- package/lib/polling.ts +3 -1
- package/lib/queue.ts +328 -33
- package/lib/routing.ts +299 -29
- package/lib/status.ts +7 -7
- package/lib/turns.ts +7 -0
- package/lib/updates.ts +24 -10
- package/package.json +1 -1
package/lib/queue.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram queue core contracts and pure planning helpers
|
|
3
3
|
* Zones: telegram queue, pi agent lifecycle, scheduling
|
|
4
|
-
* Owns queue item contracts, lane admission,
|
|
4
|
+
* Owns queue item contracts, lane admission, mutations, dispatch planning, and protected Steer publication attempts
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
|
|
9
|
-
import { isPiStaleContextError } from "./pi.ts";
|
|
9
|
+
import { isPiStaleContextError, type TurnEndEvent, type TurnEndEventResult } from "./pi.ts";
|
|
10
10
|
import { isTelegramApiCommitUnknownError } from "./telegram-api.ts";
|
|
11
11
|
import { isVoiceTurn } from "./voice.ts";
|
|
12
12
|
import { isWireRecord as isQueuePayloadRecord } from "./wire.ts";
|
|
@@ -31,7 +31,7 @@ export type TelegramPromptContent =
|
|
|
31
31
|
TelegramPromptTextContent | TelegramPromptImageContent;
|
|
32
32
|
|
|
33
33
|
export type TelegramQueueItemKind = "prompt" | "control";
|
|
34
|
-
export type TelegramQueueLane = "control" | "priority" | "default";
|
|
34
|
+
export type TelegramQueueLane = "control" | "steer" | "priority" | "default";
|
|
35
35
|
export type TelegramQueueReactionDisposition =
|
|
36
36
|
| { kind: "default" }
|
|
37
37
|
| { kind: "priority"; emoji: string }
|
|
@@ -43,6 +43,7 @@ export type TelegramQueueReactionDisposition =
|
|
|
43
43
|
}
|
|
44
44
|
| {
|
|
45
45
|
kind: "reaction-transition";
|
|
46
|
+
promptLane?: Exclude<TelegramQueueLane, "control">;
|
|
46
47
|
priorityEmoji?: string | null;
|
|
47
48
|
suppressionEmoji?: string | null;
|
|
48
49
|
};
|
|
@@ -54,7 +55,7 @@ export interface TelegramQueueAdmissionReceipt {
|
|
|
54
55
|
journalBindingKey?: string;
|
|
55
56
|
}
|
|
56
57
|
export type TelegramQueueAdmissionMode =
|
|
57
|
-
"control-queue" | "priority-queue" | "default-queue";
|
|
58
|
+
"control-queue" | "steer-queue" | "priority-queue" | "default-queue";
|
|
58
59
|
|
|
59
60
|
export interface TelegramQueueLaneContract {
|
|
60
61
|
lane: TelegramQueueLane;
|
|
@@ -74,16 +75,22 @@ export const TELEGRAM_QUEUE_LANE_CONTRACTS: readonly TelegramQueueLaneContract[]
|
|
|
74
75
|
dispatchRank: 0,
|
|
75
76
|
allowedKinds: ["control", "prompt"],
|
|
76
77
|
},
|
|
78
|
+
{
|
|
79
|
+
lane: "steer",
|
|
80
|
+
admissionMode: "steer-queue",
|
|
81
|
+
dispatchRank: 1,
|
|
82
|
+
allowedKinds: ["prompt"],
|
|
83
|
+
},
|
|
77
84
|
{
|
|
78
85
|
lane: "priority",
|
|
79
86
|
admissionMode: "priority-queue",
|
|
80
|
-
dispatchRank:
|
|
87
|
+
dispatchRank: 2,
|
|
81
88
|
allowedKinds: ["prompt"],
|
|
82
89
|
},
|
|
83
90
|
{
|
|
84
91
|
lane: "default",
|
|
85
92
|
admissionMode: "default-queue",
|
|
86
|
-
dispatchRank:
|
|
93
|
+
dispatchRank: 3,
|
|
87
94
|
allowedKinds: ["prompt"],
|
|
88
95
|
},
|
|
89
96
|
] as const;
|
|
@@ -126,6 +133,11 @@ export interface PendingTelegramTurn extends TelegramQueueItemBase {
|
|
|
126
133
|
historyText: string;
|
|
127
134
|
priorityEmoji?: string;
|
|
128
135
|
reactionSuppressionEmoji?: string;
|
|
136
|
+
/** Frozen before receipt mutation; failed/partial attempts must never become executable again. */
|
|
137
|
+
steerAttempted?: true;
|
|
138
|
+
/** Original tail objects of an issued idle packet; metadata only, not another waiting queue. */
|
|
139
|
+
steerPacketTail?: readonly PendingTelegramTurn[];
|
|
140
|
+
steerPacketSourceOrders?: readonly number[];
|
|
129
141
|
/** Emit the explicit aborted-turn notice when /next settles this active turn. */
|
|
130
142
|
announceNextAbortOnEnd?: boolean;
|
|
131
143
|
|
|
@@ -226,7 +238,8 @@ export function parseTelegramQueueHandoffPayload(
|
|
|
226
238
|
): TelegramQueueHandoffPayload | undefined {
|
|
227
239
|
if (
|
|
228
240
|
!isQueuePayloadRecord(value) ||
|
|
229
|
-
(value.kind !== "prompt" && value.kind !== "control")
|
|
241
|
+
(value.kind !== "prompt" && value.kind !== "control") ||
|
|
242
|
+
value.steerAttempted !== undefined || value.steerPacketTail !== undefined || value.steerPacketSourceOrders !== undefined
|
|
230
243
|
)
|
|
231
244
|
return undefined;
|
|
232
245
|
const target =
|
|
@@ -259,6 +272,7 @@ export function parseTelegramQueueHandoffPayload(
|
|
|
259
272
|
typeof value.guestInlineMessageId !== "string") ||
|
|
260
273
|
!Number.isSafeInteger(value.queueOrder) ||
|
|
261
274
|
(value.queueLane !== "control" &&
|
|
275
|
+
value.queueLane !== "steer" &&
|
|
262
276
|
value.queueLane !== "priority" &&
|
|
263
277
|
value.queueLane !== "default") ||
|
|
264
278
|
!Number.isSafeInteger(value.laneOrder) ||
|
|
@@ -300,6 +314,7 @@ export function parseTelegramQueueHandoffPayload(
|
|
|
300
314
|
}
|
|
301
315
|
if (
|
|
302
316
|
value.queueLane === "control" ||
|
|
317
|
+
(value.queueLane === "steer" && (value.guestQueryId !== undefined || value.guestInlineMessageId !== undefined)) ||
|
|
303
318
|
!Array.isArray(value.sourceMessageIds) ||
|
|
304
319
|
value.sourceMessageIds.some((id) => !Number.isSafeInteger(id)) ||
|
|
305
320
|
!Array.isArray(value.queuedAttachments) ||
|
|
@@ -589,7 +604,8 @@ function isTelegramQueueItemSkipped<TContext = unknown>(
|
|
|
589
604
|
export function countExecutableTelegramQueueItems<TContext = unknown>(
|
|
590
605
|
items: readonly TelegramQueueItem<TContext>[],
|
|
591
606
|
): number {
|
|
592
|
-
return items.filter((item) => !isTelegramQueueItemSkipped(item)
|
|
607
|
+
return items.filter((item) => !isTelegramQueueItemSkipped(item) &&
|
|
608
|
+
!(item.kind === "prompt" && item.steerAttempted)).length;
|
|
593
609
|
}
|
|
594
610
|
|
|
595
611
|
export function createTelegramQueueItemCountGetter<TContext = unknown>(
|
|
@@ -677,7 +693,7 @@ export function partitionTelegramQueueItemsForHistory<TContext = unknown>(
|
|
|
677
693
|
const historyTurns: PendingTelegramTurn[] = [];
|
|
678
694
|
const remainingItems: TelegramQueueItem<TContext>[] = [];
|
|
679
695
|
for (const item of items) {
|
|
680
|
-
if (isPendingTelegramTurn(item)) {
|
|
696
|
+
if (isPendingTelegramTurn(item) && item.queueLane !== "steer" && !item.steerAttempted) {
|
|
681
697
|
historyTurns.push(item);
|
|
682
698
|
continue;
|
|
683
699
|
}
|
|
@@ -774,13 +790,14 @@ export function appendTelegramQueueItem<
|
|
|
774
790
|
export function createTelegramQueueHandoff<TContext>(input: {
|
|
775
791
|
handoffToken: string;
|
|
776
792
|
item: TelegramQueueItem<TContext>;
|
|
793
|
+
steerSupported?: boolean;
|
|
777
794
|
}): TelegramQueueHandoff {
|
|
778
795
|
if (!input.handoffToken) {
|
|
779
796
|
throw new Error("Telegram queue handoff token is required.");
|
|
780
797
|
}
|
|
781
798
|
const handoff = {
|
|
782
799
|
handoffToken: input.handoffToken,
|
|
783
|
-
payload: createTelegramQueueHandoffPayload(input.item),
|
|
800
|
+
payload: createTelegramQueueHandoffPayload(input.item, { steerSupported: input.steerSupported }),
|
|
784
801
|
};
|
|
785
802
|
if (
|
|
786
803
|
Buffer.byteLength(JSON.stringify(handoff)) >
|
|
@@ -793,8 +810,18 @@ export function createTelegramQueueHandoff<TContext>(input: {
|
|
|
793
810
|
|
|
794
811
|
export function createTelegramQueueHandoffPayload<TContext>(
|
|
795
812
|
item: TelegramQueueItem<TContext>,
|
|
813
|
+
options: { steerSupported?: boolean } = {},
|
|
796
814
|
): TelegramQueueHandoffPayload {
|
|
797
815
|
assertTelegramQueueItemAdmissionValid(item);
|
|
816
|
+
if (item.kind === "prompt" && (item.steerAttempted || item.steerPacketTail !== undefined || item.steerPacketSourceOrders !== undefined)) {
|
|
817
|
+
throw new Error("Attempted Steer work cannot be transferred.");
|
|
818
|
+
}
|
|
819
|
+
if (item.queueLane === "steer" && (item.guestQueryId !== undefined || item.guestInlineMessageId !== undefined)) {
|
|
820
|
+
throw new Error("Guest prompts cannot use Steer queue transfer.");
|
|
821
|
+
}
|
|
822
|
+
if (item.queueLane === "steer" && options.steerSupported !== true) {
|
|
823
|
+
throw new Error("Steer queue transfer requires compatible lane support.");
|
|
824
|
+
}
|
|
798
825
|
if (!item.admissionReceipts?.length) {
|
|
799
826
|
throw new Error(
|
|
800
827
|
"Telegram queue handoff requires durable admission receipts.",
|
|
@@ -1098,6 +1125,7 @@ export function removeTelegramQueueItemsByMessageIds<TContext = unknown>(
|
|
|
1098
1125
|
for (const item of items) {
|
|
1099
1126
|
const shouldRemove =
|
|
1100
1127
|
isPendingTelegramTurn(item) &&
|
|
1128
|
+
!item.steerAttempted &&
|
|
1101
1129
|
isTelegramQueueItemInMessageScope(item, scope) &&
|
|
1102
1130
|
item.sourceMessageIds.some((messageId) =>
|
|
1103
1131
|
deletedMessageIds.has(messageId),
|
|
@@ -1142,10 +1170,18 @@ export function applyTelegramQueuePromptReactionDisposition<TContext = unknown>(
|
|
|
1142
1170
|
destinationLaneOrder?: number,
|
|
1143
1171
|
scope?: TelegramQueueMessageScope,
|
|
1144
1172
|
): { items: TelegramQueueItem<TContext>[]; changed: boolean } {
|
|
1173
|
+
if (
|
|
1174
|
+
disposition.kind === "reaction-transition" &&
|
|
1175
|
+
disposition.promptLane !== undefined &&
|
|
1176
|
+
!["steer", "priority", "default"].includes(disposition.promptLane)
|
|
1177
|
+
) {
|
|
1178
|
+
throw new Error("Telegram prompt lane selection is invalid.");
|
|
1179
|
+
}
|
|
1145
1180
|
let nextItems = items;
|
|
1146
1181
|
for (const [index, item] of items.entries()) {
|
|
1147
1182
|
if (
|
|
1148
1183
|
!isPendingTelegramTurn(item) ||
|
|
1184
|
+
item.steerAttempted ||
|
|
1149
1185
|
item.queueLane === "control" ||
|
|
1150
1186
|
!isTelegramQueueItemInMessageScope(item, scope) ||
|
|
1151
1187
|
!item.sourceMessageIds.includes(messageId)
|
|
@@ -1159,7 +1195,13 @@ export function applyTelegramQueuePromptReactionDisposition<TContext = unknown>(
|
|
|
1159
1195
|
: disposition.priorityEmoji !== null
|
|
1160
1196
|
: disposition.kind === "priority" ||
|
|
1161
1197
|
disposition.kind === "priority-suppressed";
|
|
1162
|
-
const queueLane: TelegramQueueLane =
|
|
1198
|
+
const queueLane: TelegramQueueLane =
|
|
1199
|
+
disposition.kind === "reaction-transition"
|
|
1200
|
+
? (disposition.promptLane ??
|
|
1201
|
+
(disposition.priorityEmoji === undefined
|
|
1202
|
+
? item.queueLane
|
|
1203
|
+
: isPriority ? "priority" : "default"))
|
|
1204
|
+
: isPriority ? "priority" : "default";
|
|
1163
1205
|
const laneOrder =
|
|
1164
1206
|
item.queueLane === queueLane ? item.laneOrder : destinationLaneOrder;
|
|
1165
1207
|
if (laneOrder === undefined) {
|
|
@@ -1219,16 +1261,14 @@ export function consumeDispatchedTelegramPrompt<TContext = unknown>(
|
|
|
1219
1261
|
if (!nextItem || !isPendingTelegramTurn(nextItem)) {
|
|
1220
1262
|
return { activeTurn: undefined, remainingItems: items };
|
|
1221
1263
|
}
|
|
1264
|
+
if (nextItem.steerPacketTail !== undefined) {
|
|
1265
|
+
const packet = [nextItem, ...nextItem.steerPacketTail];
|
|
1266
|
+
const originals = new Set<TelegramQueueItem<TContext>>(packet);
|
|
1267
|
+
return { activeTurn: projectTelegramSteerPacketTurn(packet), remainingItems: items.filter(item => !originals.has(item)) };
|
|
1268
|
+
}
|
|
1222
1269
|
return { activeTurn: nextItem, remainingItems: items.slice(1) };
|
|
1223
1270
|
}
|
|
1224
1271
|
|
|
1225
|
-
export function formatQueuedTelegramItemsStatus<TContext = unknown>(
|
|
1226
|
-
items: TelegramQueueItem<TContext>[],
|
|
1227
|
-
): string {
|
|
1228
|
-
const count = countExecutableTelegramQueueItems(items);
|
|
1229
|
-
return count === 0 ? "" : ` +${count}`;
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
1272
|
export function truncateTelegramQueueSummary(
|
|
1233
1273
|
text: string,
|
|
1234
1274
|
maxWords = 5,
|
|
@@ -1336,6 +1376,7 @@ export function createTelegramControlItemBuilder<TContext = unknown>(
|
|
|
1336
1376
|
|
|
1337
1377
|
export type TelegramQueueDispatchAction<TContext = unknown> =
|
|
1338
1378
|
| { kind: "none"; remainingItems: TelegramQueueItem<TContext>[] }
|
|
1379
|
+
| { kind: "steer"; item: PendingTelegramTurn; packet: readonly PendingTelegramTurn[]; remainingItems: TelegramQueueItem<TContext>[] }
|
|
1339
1380
|
| {
|
|
1340
1381
|
kind: "control";
|
|
1341
1382
|
item: PendingTelegramControlItem<TContext>;
|
|
@@ -1347,6 +1388,177 @@ export type TelegramQueueDispatchAction<TContext = unknown> =
|
|
|
1347
1388
|
remainingItems: TelegramQueueItem<TContext>[];
|
|
1348
1389
|
};
|
|
1349
1390
|
|
|
1391
|
+
export type TelegramSteerPacketPlan =
|
|
1392
|
+
| { kind: "none" }
|
|
1393
|
+
| { kind: "skip"; item: PendingTelegramTurn }
|
|
1394
|
+
| { kind: "packet"; items: readonly PendingTelegramTurn[] };
|
|
1395
|
+
|
|
1396
|
+
/** Plan a fixed ready prefix; publication/settlement remain with the Queue effect shell. */
|
|
1397
|
+
export function planTelegramSteerPacket<TContext>(
|
|
1398
|
+
items: readonly TelegramQueueItem<TContext>[],
|
|
1399
|
+
options: {
|
|
1400
|
+
blocked: boolean;
|
|
1401
|
+
isItemReady: (item: TelegramQueueItem<TContext>) => boolean;
|
|
1402
|
+
canAppend?: (packet: readonly PendingTelegramTurn[], item: PendingTelegramTurn) => boolean;
|
|
1403
|
+
},
|
|
1404
|
+
): TelegramSteerPacketPlan {
|
|
1405
|
+
if (options.blocked) return { kind: "none" };
|
|
1406
|
+
const ordered = [...items].sort(compareTelegramQueueItems);
|
|
1407
|
+
if (ordered.some((item) => item.queueLane === "control" && options.isItemReady(item))) {
|
|
1408
|
+
return { kind: "none" };
|
|
1409
|
+
}
|
|
1410
|
+
const packet: PendingTelegramTurn[] = [];
|
|
1411
|
+
for (const item of ordered) {
|
|
1412
|
+
if (item.queueLane !== "steer" || !isPendingTelegramTurn(item)) continue;
|
|
1413
|
+
if (item.steerAttempted || !options.isItemReady(item) || item.guestQueryId !== undefined || item.guestInlineMessageId !== undefined) break;
|
|
1414
|
+
if (item.reactionSuppressionEmoji) {
|
|
1415
|
+
if (packet.length === 0) return { kind: "skip", item };
|
|
1416
|
+
break;
|
|
1417
|
+
}
|
|
1418
|
+
const first = packet[0];
|
|
1419
|
+
if (first && !isSameTelegramPromptNamespace(first, item)) break;
|
|
1420
|
+
if (options.canAppend && !options.canAppend(packet, item)) break;
|
|
1421
|
+
packet.push(item);
|
|
1422
|
+
}
|
|
1423
|
+
return packet.length > 0 ? { kind: "packet", items: packet } : { kind: "none" };
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function buildTelegramSteerPacketContent(packet: readonly PendingTelegramTurn[], idleTask = false): TelegramPromptContent[] {
|
|
1427
|
+
const content = packet.flatMap((item, index) => [
|
|
1428
|
+
...(index > 0 ? [{ type: "text" as const, text: "\n\n" }] : []), ...item.content,
|
|
1429
|
+
]);
|
|
1430
|
+
// Ordinary idle prompts must retain origin detection by before_agent_start.
|
|
1431
|
+
return idleTask ? [{ type: "text", text: "[telegram]\n" }, ...content] : content;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function createTelegramSteerPacketBudget(idleTask = false) {
|
|
1435
|
+
let packetBytes = idleTask ? 64 : 0;
|
|
1436
|
+
return (packet: readonly PendingTelegramTurn[], item: PendingTelegramTurn): boolean => {
|
|
1437
|
+
const bytes = Buffer.byteLength(JSON.stringify(item.content)) +
|
|
1438
|
+
(packet.length > 0 ? Buffer.byteLength("\n\n") : 0) + 64;
|
|
1439
|
+
if (packetBytes + bytes > TELEGRAM_QUEUE_HANDOFF_PAYLOAD_MAX_BYTES) return false;
|
|
1440
|
+
packetBytes += bytes;
|
|
1441
|
+
return true;
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
export function isTelegramSteerItemWithinIdleCapacity(item: PendingTelegramTurn): boolean {
|
|
1446
|
+
return createTelegramSteerPacketBudget(true)([], item);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
function projectTelegramSteerPacketTurn(packet: readonly PendingTelegramTurn[]): PendingTelegramTurn {
|
|
1450
|
+
const first = packet[0];
|
|
1451
|
+
if (!first) throw new Error("Steer packet cannot be empty.");
|
|
1452
|
+
const projected: PendingTelegramTurn = {
|
|
1453
|
+
...first,
|
|
1454
|
+
content: buildTelegramSteerPacketContent(packet, true),
|
|
1455
|
+
historyText: packet.map(item => item.historyText).join("\n\n"),
|
|
1456
|
+
sourceMessageIds: [...new Set(packet.flatMap(item => item.sourceMessageIds))],
|
|
1457
|
+
queuedAttachments: packet.flatMap(item => item.queuedAttachments),
|
|
1458
|
+
admissionReceipts: packet.flatMap(item => item.admissionReceipts ?? []),
|
|
1459
|
+
voiceReplyPreferred: packet.some(item => item.voiceReplyPreferred === true),
|
|
1460
|
+
voiceReplyRequired: packet.some(item => item.voiceReplyRequired === true),
|
|
1461
|
+
steerPacketSourceOrders: packet.map(item => item.queueOrder),
|
|
1462
|
+
};
|
|
1463
|
+
delete projected.steerPacketTail;
|
|
1464
|
+
return projected;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function isSameTelegramPromptNamespace(left: PendingTelegramTurn, right: PendingTelegramTurn): boolean {
|
|
1468
|
+
return left.chatId === right.chatId &&
|
|
1469
|
+
left.target?.chatId === right.target?.chatId &&
|
|
1470
|
+
left.target?.threadId === right.target?.threadId &&
|
|
1471
|
+
left.transportStamp?.profile === right.transportStamp?.profile &&
|
|
1472
|
+
left.transportStamp?.generation === right.transportStamp?.generation;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
export interface TelegramSteerBoundaryRuntimeDeps<TContext> extends TelegramQueueStore<TContext> {
|
|
1476
|
+
isContextCurrent: (ctx: TContext) => boolean;
|
|
1477
|
+
isBlocked: (ctx: TContext) => boolean;
|
|
1478
|
+
getGeneration: () => number;
|
|
1479
|
+
isItemReady: (item: TelegramQueueItem<TContext>, ctx: TContext) => boolean;
|
|
1480
|
+
isItemCurrent: (item: PendingTelegramTurn, ctx: TContext) => boolean;
|
|
1481
|
+
getActiveTurn: () => PendingTelegramTurn | undefined;
|
|
1482
|
+
commitPacket: (items: readonly PendingTelegramTurn[], ctx: TContext) => boolean;
|
|
1483
|
+
discardItem: (item: PendingTelegramTurn, ctx: TContext) => boolean;
|
|
1484
|
+
updateStatus: (ctx: TContext) => void;
|
|
1485
|
+
recordRuntimeEvent?: TelegramRuntimeEventRecorderPort["recordRuntimeEvent"];
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/** One publication attempt; journal ACK is source disposition, never SDK/model-use confirmation. */
|
|
1489
|
+
export function createTelegramSteerBoundaryRuntime<TContext>(deps: TelegramSteerBoundaryRuntimeDeps<TContext>) {
|
|
1490
|
+
const updateStatus = (ctx: TContext): void => {
|
|
1491
|
+
try { if (deps.isContextCurrent(ctx)) deps.updateStatus(ctx); } catch (error) {
|
|
1492
|
+
if (!isPiStaleContextError(error)) {
|
|
1493
|
+
try { deps.recordRuntimeEvent?.("queue", error, { phase: "steer-status" }); } catch { /* Diagnostic failure cannot replay input. */ }
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
};
|
|
1497
|
+
const report = (error: unknown, phase: string, ctx: TContext): void => {
|
|
1498
|
+
try { deps.recordRuntimeEvent?.("queue", error, { phase }); } catch { /* Diagnostics cannot replay input. */ }
|
|
1499
|
+
updateStatus(ctx);
|
|
1500
|
+
};
|
|
1501
|
+
return {
|
|
1502
|
+
onTurnEnd(event: Pick<TurnEndEvent, "turnIndex" | "outcome">, ctx: TContext): TurnEndEventResult | undefined {
|
|
1503
|
+
const generation = deps.getGeneration();
|
|
1504
|
+
const current = () => deps.isContextCurrent(ctx) && generation === deps.getGeneration();
|
|
1505
|
+
const blocked = () => !current() || event.outcome !== "completed" || deps.isBlocked(ctx);
|
|
1506
|
+
try {
|
|
1507
|
+
if (blocked()) return;
|
|
1508
|
+
const matchesActive = (item: TelegramQueueItem<TContext>) => {
|
|
1509
|
+
const active = deps.getActiveTurn();
|
|
1510
|
+
return item.kind !== "prompt" || !active || isSameTelegramPromptNamespace(active, item);
|
|
1511
|
+
};
|
|
1512
|
+
const ready = (item: TelegramQueueItem<TContext>) => deps.isItemReady(item, ctx) && matchesActive(item);
|
|
1513
|
+
// Skipped heads settle under the same exact owner before recapturing a fresh packet.
|
|
1514
|
+
const budget = deps.getQueuedItems().length;
|
|
1515
|
+
for (let index = 0; index <= budget; index++) {
|
|
1516
|
+
const plan = planTelegramSteerPacket(deps.getQueuedItems(), {
|
|
1517
|
+
blocked: blocked(), isItemReady: ready, canAppend: createTelegramSteerPacketBudget(),
|
|
1518
|
+
});
|
|
1519
|
+
if (plan.kind === "none") return;
|
|
1520
|
+
if (plan.kind === "skip") {
|
|
1521
|
+
if (blocked() || !deps.discardItem(plan.item, ctx)) return;
|
|
1522
|
+
if (!current()) return;
|
|
1523
|
+
deps.setQueuedItems(deps.getQueuedItems().filter(item => item !== plan.item));
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
const packet = plan.items;
|
|
1527
|
+
const content = buildTelegramSteerPacketContent(packet);
|
|
1528
|
+
const draft: NonNullable<TurnEndEventResult["entries"]>[number] = {
|
|
1529
|
+
type: "custom_message", customType: "telegram-steer", content, display: true,
|
|
1530
|
+
details: { boundaryTurnIndex: event.turnIndex, queueOrders: packet.map(item => item.queueOrder) },
|
|
1531
|
+
};
|
|
1532
|
+
if (packet.some(item => !ready(item)) ||
|
|
1533
|
+
packet.some(item => !deps.getQueuedItems().includes(item)) || blocked()) return;
|
|
1534
|
+
// Keep the same Queue objects/receipts: a refused prefix cannot become a new attempt.
|
|
1535
|
+
for (const item of packet) item.steerAttempted = true;
|
|
1536
|
+
if (!deps.commitPacket(packet, ctx)) {
|
|
1537
|
+
report(new Error("Steer packet receipt settlement is unconfirmed."), "steer-settlement-unconfirmed", ctx);
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
if (blocked() || packet.some(item => !matchesActive(item) ||
|
|
1541
|
+
!deps.isItemCurrent(item, ctx) || !deps.getQueuedItems().includes(item))) {
|
|
1542
|
+
report(new Error("Steer packet authority changed before publication."), "steer-publication-refused", ctx);
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
const selected = new Set<TelegramQueueItem<TContext>>(packet);
|
|
1546
|
+
deps.setQueuedItems(deps.getQueuedItems().filter(item => !selected.has(item)));
|
|
1547
|
+
updateStatus(ctx);
|
|
1548
|
+
if (!current() || deps.isBlocked(ctx)) {
|
|
1549
|
+
report(new Error("Steer publication was refused after source disposition."), "steer-publication-refused", ctx);
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
return { entries: [draft], continue: true };
|
|
1553
|
+
}
|
|
1554
|
+
} catch (error) {
|
|
1555
|
+
report(error, "steer-attempt-failed", ctx);
|
|
1556
|
+
}
|
|
1557
|
+
return undefined;
|
|
1558
|
+
},
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1350
1562
|
export function planNextTelegramQueueAction<TContext = unknown>(
|
|
1351
1563
|
items: TelegramQueueItem<TContext>[],
|
|
1352
1564
|
canDispatch: boolean,
|
|
@@ -1360,6 +1572,9 @@ export function planNextTelegramQueueAction<TContext = unknown>(
|
|
|
1360
1572
|
}
|
|
1361
1573
|
assertTelegramQueueItemAdmissionValid(firstItem);
|
|
1362
1574
|
if (isPendingTelegramTurn(firstItem)) {
|
|
1575
|
+
if (firstItem.steerAttempted) return { kind: "none", remainingItems: items };
|
|
1576
|
+
// Steer needs whole-packet idle/boundary dispatch, never this single-prompt fallback.
|
|
1577
|
+
if (firstItem.queueLane === "steer") return { kind: "none", remainingItems: items };
|
|
1363
1578
|
return { kind: "prompt", item: firstItem, remainingItems: items };
|
|
1364
1579
|
}
|
|
1365
1580
|
return { kind: "control", item: firstItem, remainingItems };
|
|
@@ -2966,8 +3181,12 @@ function removeTelegramQueuedGuestPromptByOrderRuntime<TContext>(
|
|
|
2966
3181
|
queueOrder: number,
|
|
2967
3182
|
deps: TelegramQueueMutationRuntimeDeps<TContext>,
|
|
2968
3183
|
): boolean {
|
|
3184
|
+
const queuedItems = deps.getQueuedItems();
|
|
3185
|
+
const head = queuedItems[0];
|
|
3186
|
+
// agent_start must consume the issued guest, not adopt the next waiting reply owner.
|
|
3187
|
+
if (deps.hasPendingDispatch?.() && head?.queueOrder === queueOrder) return false;
|
|
2969
3188
|
const { items, removedItems, removedCount } =
|
|
2970
|
-
removeTelegramQueuedGuestPromptByOrder(
|
|
3189
|
+
removeTelegramQueuedGuestPromptByOrder(queuedItems, queueOrder);
|
|
2971
3190
|
if (removedCount === 0) return false;
|
|
2972
3191
|
// A menu Skip means permanent discard, so settle durable admission before
|
|
2973
3192
|
// removing the live item. A failed settlement keeps the prompt retryable.
|
|
@@ -2984,6 +3203,9 @@ export function applyTelegramQueuePromptReactionDispositionRuntime<TContext>(
|
|
|
2984
3203
|
scope?: TelegramQueueMessageScope,
|
|
2985
3204
|
): boolean {
|
|
2986
3205
|
const queuedItems = deps.getQueuedItems();
|
|
3206
|
+
const head = queuedItems[0];
|
|
3207
|
+
if (deps.hasPendingDispatch?.() && head?.kind === "prompt" &&
|
|
3208
|
+
isTelegramQueueItemInMessageScope(head, scope) && head.sourceMessageIds.includes(messageId)) return false;
|
|
2987
3209
|
const suppression =
|
|
2988
3210
|
disposition.kind === "reaction-transition"
|
|
2989
3211
|
? disposition.suppressionEmoji
|
|
@@ -3021,6 +3243,7 @@ export function applyTelegramQueuePromptReactionDispositionRuntime<TContext>(
|
|
|
3021
3243
|
const changesLane = queuedItems.some((item) => {
|
|
3022
3244
|
if (
|
|
3023
3245
|
!isPendingTelegramTurn(item) ||
|
|
3246
|
+
item.steerAttempted ||
|
|
3024
3247
|
item.queueLane === "control" ||
|
|
3025
3248
|
!isTelegramQueueItemInMessageScope(item, scope) ||
|
|
3026
3249
|
!item.sourceMessageIds.includes(messageId)
|
|
@@ -3029,11 +3252,9 @@ export function applyTelegramQueuePromptReactionDispositionRuntime<TContext>(
|
|
|
3029
3252
|
}
|
|
3030
3253
|
const queueLane: TelegramQueueLane =
|
|
3031
3254
|
disposition.kind === "reaction-transition"
|
|
3032
|
-
? disposition.priorityEmoji === undefined
|
|
3255
|
+
? (disposition.promptLane ?? (disposition.priorityEmoji === undefined
|
|
3033
3256
|
? item.queueLane
|
|
3034
|
-
: disposition.priorityEmoji === null
|
|
3035
|
-
? "default"
|
|
3036
|
-
: "priority"
|
|
3257
|
+
: disposition.priorityEmoji === null ? "default" : "priority"))
|
|
3037
3258
|
: disposition.kind === "priority" ||
|
|
3038
3259
|
disposition.kind === "priority-suppressed"
|
|
3039
3260
|
? "priority"
|
|
@@ -3082,6 +3303,8 @@ export async function enqueueTelegramPromptTurnRuntime<
|
|
|
3082
3303
|
if (
|
|
3083
3304
|
(pendingDispatch && index === 0) ||
|
|
3084
3305
|
!isPendingTelegramTurn(item) ||
|
|
3306
|
+
item.steerAttempted ||
|
|
3307
|
+
item.queueLane === "steer" ||
|
|
3085
3308
|
!historyOrders.has(item.queueOrder)
|
|
3086
3309
|
)
|
|
3087
3310
|
return true;
|
|
@@ -3396,6 +3619,12 @@ export interface TelegramQueueDispatchControllerDeps<
|
|
|
3396
3619
|
reconcileNextDispatchAnnouncementReplyOwnership?: (
|
|
3397
3620
|
item: PendingTelegramTurn,
|
|
3398
3621
|
) => void;
|
|
3622
|
+
steerDispatch?: {
|
|
3623
|
+
commitPacket: (items: readonly PendingTelegramTurn[], ctx: TContext) => boolean;
|
|
3624
|
+
isContextCurrent?: (ctx: TContext) => boolean;
|
|
3625
|
+
/** Rechecks idle/native/abort guards without rejecting this packet's own pending flag. */
|
|
3626
|
+
canSubmit?: (ctx: TContext) => boolean;
|
|
3627
|
+
};
|
|
3399
3628
|
isQueueItemTransportActive?: (item: TelegramQueueItem<TContext>) => boolean;
|
|
3400
3629
|
hasPendingInboundQueueMutationForItem?: (
|
|
3401
3630
|
item: TelegramQueueItem<TContext>,
|
|
@@ -3426,6 +3655,7 @@ export function executeTelegramQueueDispatchPlan<TContext = unknown>(
|
|
|
3426
3655
|
deps.executeControlItem(plan.item);
|
|
3427
3656
|
return;
|
|
3428
3657
|
}
|
|
3658
|
+
if (plan.kind === "steer") throw new Error("Steer packet requires dedicated Queue dispatch.");
|
|
3429
3659
|
deps.onPromptDispatchStart(plan.item.chatId, plan.item.target);
|
|
3430
3660
|
try {
|
|
3431
3661
|
if (deps.commitPromptDispatch && !deps.commitPromptDispatch(plan.item)) {
|
|
@@ -3470,6 +3700,7 @@ export function createTelegramQueueDispatchRuntime<TContext = unknown>(
|
|
|
3470
3700
|
onPromptDispatchFailure: deps.onPromptDispatchFailure,
|
|
3471
3701
|
reconcileNextDispatchAnnouncementReplyOwnership:
|
|
3472
3702
|
deps.reconcileNextDispatchAnnouncementReplyOwnership,
|
|
3703
|
+
steerDispatch: deps.steerDispatch,
|
|
3473
3704
|
isQueueItemTransportActive: deps.isQueueItemTransportActive,
|
|
3474
3705
|
hasPendingInboundQueueMutationForItem:
|
|
3475
3706
|
deps.hasPendingInboundQueueMutationForItem,
|
|
@@ -3510,6 +3741,11 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
3510
3741
|
const retainedItems: TelegramQueueItem<TContext>[] = [];
|
|
3511
3742
|
let droppedInactiveItemCount = 0;
|
|
3512
3743
|
for (const item of queuedItems) {
|
|
3744
|
+
if (item.kind === "prompt" && item.steerAttempted) {
|
|
3745
|
+
protectedInactiveItems.push(item);
|
|
3746
|
+
retainedItems.push(item);
|
|
3747
|
+
continue;
|
|
3748
|
+
}
|
|
3513
3749
|
if (
|
|
3514
3750
|
!deps.isQueueItemTransportActive ||
|
|
3515
3751
|
deps.isQueueItemTransportActive(item)
|
|
@@ -3619,14 +3855,26 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
3619
3855
|
return;
|
|
3620
3856
|
}
|
|
3621
3857
|
const dispatchBasisItems = deps.getQueuedItems();
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3858
|
+
let dispatchPlan = planNextTelegramQueueAction(dispatchableItems, canDispatch);
|
|
3859
|
+
if (canDispatch && nextItem?.kind === "prompt" && nextItem.queueLane === "steer" &&
|
|
3860
|
+
deps.steerDispatch && deps.steerDispatch.isContextCurrent?.(ctx) !== false &&
|
|
3861
|
+
deps.steerDispatch.canSubmit?.(ctx) !== false) {
|
|
3862
|
+
const packetPlan = planTelegramSteerPacket(deps.getQueuedItems(), {
|
|
3863
|
+
blocked: false,
|
|
3864
|
+
isItemReady: item => deps.isQueueItemTransportActive?.(item) !== false &&
|
|
3865
|
+
deps.hasPendingInboundQueueMutationForItem?.(item) !== true &&
|
|
3866
|
+
deps.isQueueItemAdmissionReady?.(item) !== false,
|
|
3867
|
+
canAppend: createTelegramSteerPacketBudget(true),
|
|
3868
|
+
});
|
|
3869
|
+
if (packetPlan.kind === "packet" && packetPlan.items[0] === nextItem) {
|
|
3870
|
+
dispatchPlan = { kind: "steer", item: nextItem, packet: packetPlan.items, remainingItems: dispatchableItems };
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3626
3873
|
const commitDispatchPlan = (): boolean => {
|
|
3627
3874
|
if (dispatchPlan.kind === "none") return true;
|
|
3628
3875
|
const currentItems = deps.getQueuedItems();
|
|
3629
|
-
if (dispatchPlan.kind === "prompt") {
|
|
3876
|
+
if (dispatchPlan.kind === "prompt" || dispatchPlan.kind === "steer") {
|
|
3877
|
+
const inputs = dispatchPlan.kind === "steer" ? dispatchPlan.packet : [dispatchPlan.item];
|
|
3630
3878
|
const queueDrifted =
|
|
3631
3879
|
currentItems.length !== dispatchBasisItems.length ||
|
|
3632
3880
|
currentItems.some(
|
|
@@ -3634,10 +3882,10 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
3634
3882
|
);
|
|
3635
3883
|
const dispatchEligibilityDrifted =
|
|
3636
3884
|
!deps.canDispatch(ctx) ||
|
|
3637
|
-
deps.
|
|
3638
|
-
|
|
3639
|
-
deps.
|
|
3640
|
-
|
|
3885
|
+
(dispatchPlan.kind === "steer" && (deps.steerDispatch?.isContextCurrent?.(ctx) === false ||
|
|
3886
|
+
deps.steerDispatch?.canSubmit?.(ctx) === false)) ||
|
|
3887
|
+
inputs.some(item => deps.hasPendingInboundQueueMutationForItem?.(item) === true ||
|
|
3888
|
+
deps.isQueueItemAdmissionReady?.(item) === false || deps.isQueueItemTransportActive?.(item) === false);
|
|
3641
3889
|
if (queueDrifted || dispatchEligibilityDrifted) {
|
|
3642
3890
|
const selectedItemRetained = currentItems.includes(
|
|
3643
3891
|
dispatchPlan.item,
|
|
@@ -3669,6 +3917,53 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
3669
3917
|
};
|
|
3670
3918
|
const executePlan = (): void => {
|
|
3671
3919
|
if (!commitDispatchPlan()) return;
|
|
3920
|
+
if (dispatchPlan.kind === "steer") {
|
|
3921
|
+
const { packet, item: first } = dispatchPlan;
|
|
3922
|
+
const content = buildTelegramSteerPacketContent(packet, true);
|
|
3923
|
+
const generation = deps.getDispatchGeneration?.();
|
|
3924
|
+
const current = (): boolean => {
|
|
3925
|
+
try {
|
|
3926
|
+
return (!deps.hasDispatchContext || deps.hasDispatchContext()) &&
|
|
3927
|
+
(generation === undefined || deps.isDispatchGenerationActive?.(generation) !== false) &&
|
|
3928
|
+
deps.steerDispatch?.isContextCurrent?.(ctx) !== false;
|
|
3929
|
+
} catch { return false; }
|
|
3930
|
+
};
|
|
3931
|
+
const refuse = (message: string): void => {
|
|
3932
|
+
deps.recordRuntimeEvent?.("dispatch", new Error(message), { phase: "steer-idle-attempt" });
|
|
3933
|
+
if (current()) deps.onPromptDispatchFailure(ctx, message);
|
|
3934
|
+
};
|
|
3935
|
+
controlDispatchPending = true;
|
|
3936
|
+
try {
|
|
3937
|
+
for (const item of packet) item.steerAttempted = true;
|
|
3938
|
+
if (deps.steerDispatch?.commitPacket(packet, ctx) !== true) {
|
|
3939
|
+
refuse("Steer packet receipt settlement is unconfirmed.");
|
|
3940
|
+
return;
|
|
3941
|
+
}
|
|
3942
|
+
const sourceChanged = packet.some(item => !deps.getQueuedItems().includes(item) ||
|
|
3943
|
+
deps.isQueueItemTransportActive?.(item) === false || deps.hasPendingInboundQueueMutationForItem?.(item) === true);
|
|
3944
|
+
const readyControl = deps.getQueuedItems().some(item => item.queueLane === "control" &&
|
|
3945
|
+
deps.isQueueItemTransportActive?.(item) !== false && deps.isQueueItemAdmissionReady?.(item) !== false);
|
|
3946
|
+
if (!current() || deps.steerDispatch?.canSubmit?.(ctx) === false || !deps.canDispatch(ctx) || sourceChanged || readyControl) {
|
|
3947
|
+
refuse("Steer packet lost idle readiness before submission.");
|
|
3948
|
+
return;
|
|
3949
|
+
}
|
|
3950
|
+
first.steerPacketTail = packet.slice(1);
|
|
3951
|
+
// Pin the original head, retaining every original until agent_start consumes the packet.
|
|
3952
|
+
deps.setQueuedItems([first, ...deps.getQueuedItems().filter(item => item !== first)]);
|
|
3953
|
+
deps.onPromptDispatchStart(ctx, first.chatId, first.target);
|
|
3954
|
+
if (!current() || deps.steerDispatch?.canSubmit?.(ctx) === false || packet.some(item => !deps.getQueuedItems().includes(item) ||
|
|
3955
|
+
deps.isQueueItemTransportActive?.(item) === false || deps.hasPendingInboundQueueMutationForItem?.(item) === true)) {
|
|
3956
|
+
refuse("Steer packet authority changed before submission.");
|
|
3957
|
+
return;
|
|
3958
|
+
}
|
|
3959
|
+
deps.sendUserMessage(content);
|
|
3960
|
+
} catch (error) {
|
|
3961
|
+
refuse(getTelegramQueueErrorMessage(error));
|
|
3962
|
+
} finally {
|
|
3963
|
+
controlDispatchPending = false;
|
|
3964
|
+
}
|
|
3965
|
+
return;
|
|
3966
|
+
}
|
|
3672
3967
|
executeTelegramQueueDispatchPlan(dispatchPlan, {
|
|
3673
3968
|
executeControlItem: (item) => {
|
|
3674
3969
|
controlDispatchPending = true;
|
|
@@ -3717,7 +4012,7 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
3717
4012
|
},
|
|
3718
4013
|
});
|
|
3719
4014
|
};
|
|
3720
|
-
if (dispatchPlan.kind === "prompt" && nextDispatchAnnouncementRequested) {
|
|
4015
|
+
if ((dispatchPlan.kind === "prompt" || dispatchPlan.kind === "steer") && nextDispatchAnnouncementRequested) {
|
|
3721
4016
|
nextDispatchAnnouncementRequested = false;
|
|
3722
4017
|
controlDispatchPending = true;
|
|
3723
4018
|
const announcementGeneration = nextDispatchAnnouncementGeneration;
|