@llblab/pi-telegram 0.55.1 → 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/lib/pi.ts CHANGED
@@ -20,6 +20,8 @@ import {
20
20
  type SessionShutdownEvent,
21
21
  type SessionStartEvent,
22
22
  type SlashCommandInfo,
23
+ type TurnEndEvent,
24
+ type TurnEndEventResult,
23
25
  type UIPromptEndEvent,
24
26
  type UIPromptStartEvent,
25
27
  SettingsManager,
@@ -41,6 +43,8 @@ export type {
41
43
  SessionShutdownEvent,
42
44
  SessionStartEvent,
43
45
  SlashCommandInfo,
46
+ TurnEndEvent,
47
+ TurnEndEventResult,
44
48
  UIPromptEndEvent,
45
49
  UIPromptStartEvent,
46
50
  };
@@ -294,6 +298,10 @@ export function isExtensionContextIdle(ctx: ExtensionContext): boolean {
294
298
  return ctx.isIdle();
295
299
  }
296
300
 
301
+ export function isExtensionContextAbortRequested(ctx: ExtensionContext): boolean {
302
+ return ctx.signal?.aborted === true;
303
+ }
304
+
297
305
  export function hasExtensionContextPendingMessages(
298
306
  ctx: ExtensionContext,
299
307
  ): boolean {
package/lib/polling.ts CHANGED
@@ -1303,7 +1303,9 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
1303
1303
  if (threadModeEnabled) consecutiveDisabledProbes = 0;
1304
1304
  const botState = deps.topicTargetStore.getBotState();
1305
1305
  const current = botState.threadMode;
1306
- if (threadModeEnabled && current === "enabled") return;
1306
+ // Persisted capability is not proof that this owner started its local bus.
1307
+ if (threadModeEnabled && current === "enabled" &&
1308
+ (deps.getPollingStartedWithTelegramBus() || !deps.ownsLock(ctx))) return;
1307
1309
  if (!threadModeEnabled && current === "disabled") {
1308
1310
  if (
1309
1311
  !deps.ownsLock(ctx) ||
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, pure queue mutations, and dispatch planning
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: 1,
87
+ dispatchRank: 2,
81
88
  allowedKinds: ["prompt"],
82
89
  },
83
90
  {
84
91
  lane: "default",
85
92
  admissionMode: "default-queue",
86
- dispatchRank: 2,
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)).length;
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 = isPriority ? "priority" : "default";
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(deps.getQueuedItems(), queueOrder);
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
- const dispatchPlan = planNextTelegramQueueAction(
3623
- dispatchableItems,
3624
- canDispatch,
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.hasPendingInboundQueueMutationForItem?.(dispatchPlan.item) ===
3638
- true ||
3639
- deps.isQueueItemAdmissionReady?.(dispatchPlan.item) === false ||
3640
- deps.isQueueItemTransportActive?.(dispatchPlan.item) === false;
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;