@llblab/pi-kit 0.33.1 → 0.34.1

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.
Files changed (64) hide show
  1. package/BACKLOG.md +2 -1
  2. package/CHANGELOG.md +9 -0
  3. package/README.md +3 -3
  4. package/node_modules/@llblab/pi-telegram/AGENTS.md +6 -5
  5. package/node_modules/@llblab/pi-telegram/BACKLOG.md +6 -1
  6. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +20 -0
  7. package/node_modules/@llblab/pi-telegram/README.md +28 -17
  8. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +8 -4
  9. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +41 -1
  10. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +1 -0
  11. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +10 -5
  12. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +2 -0
  13. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +34 -20
  14. package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +8 -0
  15. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +21 -7
  16. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +1 -1
  17. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +33 -28
  18. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +18 -3
  19. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +2 -1
  20. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +28 -20
  21. package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +6 -2
  22. package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +3 -3
  23. package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.d.ts +1 -0
  24. package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.js +100 -99
  25. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +3 -2
  26. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +3 -0
  27. package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +6 -2
  28. package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -3
  29. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +57 -5
  30. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +313 -25
  31. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +4 -0
  32. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +62 -1
  33. package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +1 -2
  34. package/node_modules/@llblab/pi-telegram/dist/lib/status.js +6 -7
  35. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +11 -1
  36. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +11 -0
  37. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +57 -6
  38. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +13 -32
  39. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +44 -15
  40. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  41. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
  42. package/node_modules/@llblab/pi-telegram/docs/architecture.md +25 -14
  43. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +4 -0
  44. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +8 -6
  45. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +17 -17
  46. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +52 -3
  47. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +13 -5
  48. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +37 -23
  49. package/node_modules/@llblab/pi-telegram/lib/bus.ts +23 -7
  50. package/node_modules/@llblab/pi-telegram/lib/commands.ts +52 -47
  51. package/node_modules/@llblab/pi-telegram/lib/extension.ts +18 -2
  52. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +34 -28
  53. package/node_modules/@llblab/pi-telegram/lib/locks.ts +5 -2
  54. package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +114 -162
  55. package/node_modules/@llblab/pi-telegram/lib/pi.ts +8 -0
  56. package/node_modules/@llblab/pi-telegram/lib/polling.ts +7 -2
  57. package/node_modules/@llblab/pi-telegram/lib/queue.ts +328 -33
  58. package/node_modules/@llblab/pi-telegram/lib/routing.ts +51 -1
  59. package/node_modules/@llblab/pi-telegram/lib/status.ts +7 -7
  60. package/node_modules/@llblab/pi-telegram/lib/sync.ts +11 -0
  61. package/node_modules/@llblab/pi-telegram/lib/threads.ts +57 -5
  62. package/node_modules/@llblab/pi-telegram/lib/updates.ts +44 -21
  63. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  64. package/package.json +2 -2
@@ -37,7 +37,12 @@ interface TelegramQueueMenuItem {
37
37
  queueOrder: number;
38
38
  isGuest: boolean;
39
39
  queuePosition: number;
40
- isPriority: boolean;
40
+ queueLane: Queue.TelegramQueueLane;
41
+ canSelectLane: boolean;
42
+ canSelectDisposition: boolean;
43
+ dispatchProtected: boolean;
44
+ attemptFrozen: boolean;
45
+ exceedsIdleCapacity: boolean;
41
46
  priorityEmoji?: string;
42
47
  reactionSuppressionEmoji?: string;
43
48
  hasAttachments: boolean;
@@ -60,15 +65,26 @@ function getTelegramQueueItemPromptText<Context>(
60
65
 
61
66
  function toTelegramQueueMenuItems<Context>(
62
67
  items: readonly Queue.TelegramQueueItem<Context>[],
68
+ pendingDispatch = false,
63
69
  ): TelegramQueueMenuItem[] {
70
+ const head = items[0];
64
71
  return items.map((item, index) => {
72
+ const dispatchProtected = pendingDispatch && (item === head ||
73
+ (head?.kind === "prompt" && head.steerPacketTail?.includes(item as Queue.PendingTelegramTurn) === true));
74
+ const attemptFrozen = item.kind === "prompt" && item.steerAttempted === true && !dispatchProtected;
75
+ const editable = item.kind === "prompt" && item.guestQueryId === undefined && item.guestInlineMessageId === undefined && !dispatchProtected && !attemptFrozen;
65
76
  return {
66
77
  chatId: item.chatId,
67
78
  replyToMessageId: item.replyToMessageId,
68
79
  queueOrder: item.queueOrder,
69
80
  isGuest: item.kind === "prompt" && item.guestQueryId !== undefined,
70
81
  queuePosition: index + 1,
71
- isPriority: item.queueLane === "priority",
82
+ queueLane: item.queueLane,
83
+ canSelectLane: editable && item.queueLane !== "control",
84
+ canSelectDisposition: editable && (item.queueLane !== "control" || item.sourceMessageIds.includes(item.replyToMessageId)),
85
+ dispatchProtected, attemptFrozen,
86
+ exceedsIdleCapacity: item.kind === "prompt" && item.queueLane === "steer" && !item.steerAttempted &&
87
+ !Queue.isTelegramSteerItemWithinIdleCapacity(item),
72
88
  priorityEmoji: item.kind === "prompt" ? item.priorityEmoji : undefined,
73
89
  reactionSuppressionEmoji:
74
90
  item.kind === "prompt" ? item.reactionSuppressionEmoji : undefined,
@@ -98,18 +114,16 @@ function buildTelegramQueueMenuReplyMarkup(
98
114
  const refreshRow = [{ text: "🔄 Refresh", callback_data: refreshData }];
99
115
  if (items.length === 0) return { inline_keyboard: [backRow, refreshRow] };
100
116
  const rows = items.map((item) => {
101
- const prefix = item.reactionSuppressionEmoji
102
- ? `${item.reactionSuppressionEmoji} `
103
- : item.isPriority
104
- ? `${item.priorityEmoji ?? "⚡"} `
105
- : item.hasAttachments
106
- ? "📎 "
107
- : "";
117
+ const badge = getTelegramQueueMenuLaneBadge(item);
118
+ const prefix = item.reactionSuppressionEmoji ? "👎 " :
119
+ badge ? `${badge} ` : item.hasAttachments ? "📎 " : "";
108
120
  const position = item.reactionSuppressionEmoji
109
121
  ? formatSkippedTelegramQueuePosition(item.queuePosition)
110
122
  : String(item.queuePosition);
111
123
  const ordinalSeparator = item.reactionSuppressionEmoji ? "\u200A" : "";
112
- const label = `${position}${ordinalSeparator}. ${prefix}${item.statusSummary}`;
124
+ const state = item.attemptFrozen ? " · frozen" : item.dispatchProtected ? " · dispatch protected" :
125
+ item.exceedsIdleCapacity ? " · idle capacity" : "";
126
+ const label = `${position}${ordinalSeparator}. ${prefix}${item.statusSummary}${state}`;
113
127
  return [
114
128
  {
115
129
  text: label,
@@ -165,18 +179,26 @@ function escapeTelegramQueueMenuHtmlPreview(text: string): string {
165
179
  return truncated ? escaped + suffix : escaped;
166
180
  }
167
181
 
182
+ function getTelegramQueueMenuLaneBadge(item: Pick<TelegramQueueMenuItem, "queueLane">): string {
183
+ if (item.queueLane === "control") return "⚡";
184
+ if (item.queueLane === "steer") return "🔥";
185
+ if (item.queueLane !== "priority") return "";
186
+ return "👍";
187
+ }
188
+
168
189
  function getTelegramQueueMenuItemText(item: TelegramQueueMenuItem): string {
169
- const badge = item.reactionSuppressionEmoji
170
- ? ` ${item.reactionSuppressionEmoji}`
171
- : item.isPriority
172
- ? ` ${item.priorityEmoji ?? "⚡"}`
173
- : "";
190
+ const marker = item.reactionSuppressionEmoji ? "👎" : getTelegramQueueMenuLaneBadge(item);
191
+ const badge = marker ? ` ${marker}` : "";
174
192
  const position = item.reactionSuppressionEmoji
175
193
  ? `<s>${item.queuePosition}</s>.`
176
194
  : `<b>${item.queuePosition}.</b>`;
177
195
  const heading = `${position}${badge}`;
178
196
  const preview = `<pre>${escapeTelegramQueueMenuHtmlPreview(item.promptText)}</pre>`;
179
- return `${heading}\n${preview}`;
197
+ const note = item.attemptFrozen ? "\n<b>⚠️ Attempt frozen.</b>\nSDK/model use is unconfirmed; not editable or automatically retried." :
198
+ item.dispatchProtected ? "\n<b>⏳ Dispatch protected.</b>\nWaiting for agent_start; SDK/model use is unconfirmed." :
199
+ item.exceedsIdleCapacity ? "\n<b>⚠️ Idle Steer capacity exceeded.</b>\nEdit the source or choose Priority/Normal; source groups are never split." :
200
+ item.queueLane === "control" ? "\nControl lane is producer-owned." : "";
201
+ return `${heading}\n${preview}${note}`;
180
202
  }
181
203
 
182
204
  function buildTelegramGuestQueueItemSubmenuReplyMarkup(
@@ -195,37 +217,18 @@ function buildTelegramGuestQueueItemSubmenuReplyMarkup(
195
217
  };
196
218
  }
197
219
 
198
- function buildTelegramQueueItemSubmenuReplyMarkup(
199
- chatId: number,
200
- replyToMessageId: number,
201
- isPriority: boolean,
202
- isSkipped: boolean,
203
- ): TelegramQueueMenuReplyMarkup {
204
- return {
205
- inline_keyboard: [
206
- [{ text: "⬆️ Back", callback_data: "queue:list" }],
207
- [
208
- {
209
- text: isPriority ? "🟡 Priority" : "⚫️ Priority",
210
- callback_data: `queue:prio-set:${chatId}:${replyToMessageId}:priority`,
211
- },
212
- {
213
- text: isPriority ? "⚫️ Normal" : "🔵 Normal",
214
- callback_data: `queue:prio-set:${chatId}:${replyToMessageId}:normal`,
215
- },
216
- ],
217
- [
218
- {
219
- text: isSkipped ? "⚫️ Keep" : "🟢 Keep",
220
- callback_data: `queue:skip-set:${chatId}:${replyToMessageId}:keep`,
221
- },
222
- {
223
- text: isSkipped ? "🔴 Skip" : "⚫️ Skip",
224
- callback_data: `queue:skip-set:${chatId}:${replyToMessageId}:skip`,
225
- },
226
- ],
227
- ],
228
- };
220
+ function buildTelegramQueueItemSubmenuReplyMarkup(item: TelegramQueueMenuItem): TelegramQueueMenuReplyMarkup {
221
+ const rows: TelegramQueueMenuReplyMarkup["inline_keyboard"] = [[{ text: "⬆️ Back", callback_data: "queue:list" }]];
222
+ if (item.canSelectLane) rows.push([
223
+ { text: item.queueLane === "default" ? "🔵 Normal" : "⚫️ Normal", callback_data: `queue:lane-set:${item.chatId}:${item.replyToMessageId}:normal` },
224
+ { text: item.queueLane === "priority" ? "🟣 Priority" : "⚫️ Priority", callback_data: `queue:lane-set:${item.chatId}:${item.replyToMessageId}:priority` },
225
+ { text: item.queueLane === "steer" ? "🟡 Steer" : "⚫️ Steer", callback_data: `queue:lane-set:${item.chatId}:${item.replyToMessageId}:steer` },
226
+ ]);
227
+ if (item.canSelectDisposition) rows.push([
228
+ { text: item.reactionSuppressionEmoji ? "⚫️ Keep" : "🟢 Keep", callback_data: `queue:skip-set:${item.chatId}:${item.replyToMessageId}:keep` },
229
+ { text: item.reactionSuppressionEmoji ? "🔴 Skip" : "⚫️ Skip", callback_data: `queue:skip-set:${item.chatId}:${item.replyToMessageId}:skip` },
230
+ ]);
231
+ return { inline_keyboard: rows };
229
232
  }
230
233
 
231
234
  interface TelegramQueueMenuCallbackDeps<Context = unknown> {
@@ -236,12 +239,7 @@ interface TelegramQueueMenuCallbackDeps<Context = unknown> {
236
239
  ) => TelegramQueueMenuItem | undefined;
237
240
  findGuestItem: (queueOrder: number) => TelegramQueueMenuItem | undefined;
238
241
  skipGuest: (queueOrder: number, ctx: Context) => Promise<boolean>;
239
- togglePriority: (chatId: number, replyToMessageId: number) => boolean;
240
- setPriority: (
241
- chatId: number,
242
- replyToMessageId: number,
243
- enabled: boolean,
244
- ) => boolean;
242
+ setLane: (chatId: number, replyToMessageId: number, lane: Exclude<Queue.TelegramQueueLane, "control">) => boolean;
245
243
  setSkipped: (
246
244
  chatId: number,
247
245
  replyToMessageId: number,
@@ -307,7 +305,13 @@ async function handleTelegramQueueMenuCallback<Context>(
307
305
  }
308
306
  const guestSkipMatch = data.match(/^queue:guest-skip:(\d+)$/);
309
307
  if (guestSkipMatch) {
310
- const skipped = await deps.skipGuest(Number(guestSkipMatch[1]), ctx);
308
+ const queueOrder = Number(guestSkipMatch[1]);
309
+ const item = deps.findGuestItem(queueOrder);
310
+ if (item?.dispatchProtected || item?.attemptFrozen) {
311
+ await deps.answerCallbackQuery(callbackQueryId, "Queue item is protected");
312
+ return true;
313
+ }
314
+ const skipped = await deps.skipGuest(queueOrder, ctx);
311
315
  if (!skipped) {
312
316
  await refreshStaleTelegramQueueMenuItem(
313
317
  callbackQueryId,
@@ -339,16 +343,16 @@ async function handleTelegramQueueMenuCallback<Context>(
339
343
  return true;
340
344
  }
341
345
  const prioSetMatch = data.match(
342
- /^queue:prio-set:(\d+):(\d+):(priority|normal)$/,
346
+ /^queue:(?:lane-set|prio-set):(\d+):(\d+):(steer|priority|normal)$/,
343
347
  );
344
348
  if (prioSetMatch) {
345
- await handleTelegramQueueMenuPrioritySet(
349
+ await handleTelegramQueueMenuLaneSet(
346
350
  callbackQueryId,
347
351
  replyChatId,
348
352
  replyMessageId,
349
353
  Number(prioSetMatch[1]),
350
354
  Number(prioSetMatch[2]),
351
- prioSetMatch[3] === "priority",
355
+ prioSetMatch[3] === "normal" ? "default" : prioSetMatch[3] as "steer" | "priority",
352
356
  ctx,
353
357
  deps,
354
358
  );
@@ -448,7 +452,9 @@ async function handleTelegramGuestQueueMenuPick<Context>(
448
452
  replyChatId,
449
453
  replyMessageId,
450
454
  getTelegramQueueMenuItemText(item),
451
- buildTelegramGuestQueueItemSubmenuReplyMarkup(queueOrder),
455
+ item.dispatchProtected || item.attemptFrozen
456
+ ? buildTelegramQueueItemSubmenuReplyMarkup(item)
457
+ : buildTelegramGuestQueueItemSubmenuReplyMarkup(queueOrder),
452
458
  );
453
459
  await deps.answerCallbackQuery(callbackQueryId);
454
460
  }
@@ -474,12 +480,7 @@ async function handleTelegramQueueMenuPick<Context>(
474
480
  replyChatId,
475
481
  replyMessageId,
476
482
  getTelegramQueueMenuItemText(item),
477
- buildTelegramQueueItemSubmenuReplyMarkup(
478
- chatId,
479
- msgId,
480
- item.isPriority,
481
- item.reactionSuppressionEmoji !== undefined,
482
- ),
483
+ buildTelegramQueueItemSubmenuReplyMarkup(item),
483
484
  );
484
485
  await deps.answerCallbackQuery(callbackQueryId);
485
486
  }
@@ -502,25 +503,25 @@ async function handleTelegramQueueMenuPriority<Context>(
502
503
  deps,
503
504
  );
504
505
  }
505
- await updateTelegramQueueMenuPriority(
506
+ await updateTelegramQueueMenuLane(
506
507
  callbackQueryId,
507
508
  replyChatId,
508
509
  replyMessageId,
509
510
  chatId,
510
511
  msgId,
511
- !item.isPriority,
512
+ item.queueLane === "priority" ? "default" : "priority",
512
513
  ctx,
513
514
  deps,
514
515
  );
515
516
  }
516
517
 
517
- async function handleTelegramQueueMenuPrioritySet<Context>(
518
+ async function handleTelegramQueueMenuLaneSet<Context>(
518
519
  callbackQueryId: string,
519
520
  replyChatId: number,
520
521
  replyMessageId: number,
521
522
  chatId: number,
522
523
  msgId: number,
523
- enabled: boolean,
524
+ lane: Exclude<Queue.TelegramQueueLane, "control">,
524
525
  ctx: Context,
525
526
  deps: TelegramQueueMenuCallbackDeps<Context>,
526
527
  ): Promise<void> {
@@ -533,29 +534,32 @@ async function handleTelegramQueueMenuPrioritySet<Context>(
533
534
  deps,
534
535
  );
535
536
  }
536
- await updateTelegramQueueMenuPriority(
537
+ await updateTelegramQueueMenuLane(
537
538
  callbackQueryId,
538
539
  replyChatId,
539
540
  replyMessageId,
540
541
  chatId,
541
542
  msgId,
542
- enabled,
543
+ lane,
543
544
  ctx,
544
545
  deps,
545
546
  );
546
547
  }
547
548
 
548
- async function updateTelegramQueueMenuPriority<Context>(
549
+ async function updateTelegramQueueMenuLane<Context>(
549
550
  callbackQueryId: string,
550
551
  replyChatId: number,
551
552
  replyMessageId: number,
552
553
  chatId: number,
553
554
  msgId: number,
554
- enabled: boolean,
555
+ lane: Exclude<Queue.TelegramQueueLane, "control">,
555
556
  ctx: Context,
556
557
  deps: TelegramQueueMenuCallbackDeps<Context>,
557
558
  ): Promise<void> {
558
- deps.setPriority(chatId, msgId, enabled);
559
+ if (!deps.setLane(chatId, msgId, lane)) {
560
+ await deps.answerCallbackQuery(callbackQueryId, "Queue item is protected");
561
+ return;
562
+ }
559
563
  deps.updateStatus(ctx);
560
564
  const updated = deps.findItem(chatId, msgId);
561
565
  if (!updated) {
@@ -570,16 +574,11 @@ async function updateTelegramQueueMenuPriority<Context>(
570
574
  replyChatId,
571
575
  replyMessageId,
572
576
  getTelegramQueueMenuItemText(updated),
573
- buildTelegramQueueItemSubmenuReplyMarkup(
574
- chatId,
575
- msgId,
576
- updated.isPriority,
577
- updated.reactionSuppressionEmoji !== undefined,
578
- ),
577
+ buildTelegramQueueItemSubmenuReplyMarkup(updated),
579
578
  );
580
579
  await deps.answerCallbackQuery(
581
580
  callbackQueryId,
582
- updated.isPriority ? "Prioritized" : "Normal priority",
581
+ updated.queueLane === "steer" ? "Steer selected" : updated.queueLane === "priority" ? "Prioritized" : "Normal priority",
583
582
  );
584
583
  }
585
584
 
@@ -602,7 +601,10 @@ async function handleTelegramQueueMenuSkipSet<Context>(
602
601
  deps,
603
602
  );
604
603
  }
605
- deps.setSkipped(chatId, msgId, skipped);
604
+ if (!deps.setSkipped(chatId, msgId, skipped)) {
605
+ await deps.answerCallbackQuery(callbackQueryId, "Queue item is protected");
606
+ return;
607
+ }
606
608
  deps.updateStatus(ctx);
607
609
  const updated = deps.findItem(chatId, msgId);
608
610
  if (!updated) {
@@ -617,12 +619,7 @@ async function handleTelegramQueueMenuSkipSet<Context>(
617
619
  replyChatId,
618
620
  replyMessageId,
619
621
  getTelegramQueueMenuItemText(updated),
620
- buildTelegramQueueItemSubmenuReplyMarkup(
621
- chatId,
622
- msgId,
623
- updated.isPriority,
624
- updated.reactionSuppressionEmoji !== undefined,
625
- ),
622
+ buildTelegramQueueItemSubmenuReplyMarkup(updated),
626
623
  );
627
624
  await deps.answerCallbackQuery(callbackQueryId);
628
625
  }
@@ -649,6 +646,7 @@ export function createTelegramQueueMenuRuntime<
649
646
  Context,
650
647
  TModel extends MenuModel = MenuModel,
651
648
  >(deps: {
649
+ hasPendingDispatch?: () => boolean;
652
650
  telegramQueueStore: Queue.TelegramQueueStateStore<Context>;
653
651
  queueMutationRuntime: Queue.TelegramQueueMutationController<Context>;
654
652
  sendInteractiveMessage: TelegramMenuMessageRuntimeDeps["sendInteractiveMessage"];
@@ -695,6 +693,7 @@ export function createTelegramQueueMenuRuntime<
695
693
  );
696
694
  return createOpenQueueMenu<Context, TModel>({
697
695
  getQueuedItems: deps.telegramQueueStore.getQueuedItems,
696
+ hasPendingDispatch: deps.hasPendingDispatch,
698
697
  getModelMenuState: (chat, context) =>
699
698
  deps.getModelMenuState(chat, context, threadId),
700
699
  storeModelMenuState: delivery.storeModelMenuState,
@@ -704,6 +703,7 @@ export function createTelegramQueueMenuRuntime<
704
703
  })(chatId, replyToMessageId, ctx);
705
704
  },
706
705
  handleCallbackQuery: createQueueMenuCallbackHandler<Context, TModel>({
706
+ hasPendingDispatch: deps.hasPendingDispatch,
707
707
  telegramQueueStore: deps.telegramQueueStore,
708
708
  queueMutationRuntime: deps.queueMutationRuntime,
709
709
  editInteractiveMessage: editQueueMenuMessage,
@@ -721,6 +721,7 @@ function createOpenQueueMenu<
721
721
  TModel extends MenuModel = MenuModel,
722
722
  >(deps: {
723
723
  getQueuedItems: () => Queue.TelegramQueueItem<Context>[];
724
+ hasPendingDispatch?: () => boolean;
724
725
  getModelMenuState: (
725
726
  chatId: number,
726
727
  ctx: Context,
@@ -739,7 +740,7 @@ function createOpenQueueMenu<
739
740
  ctx: Context,
740
741
  ): Promise<void> => {
741
742
  const state = await deps.getModelMenuState(chatId, ctx);
742
- const menuItems = toTelegramQueueMenuItems(deps.getQueuedItems());
743
+ const menuItems = toTelegramQueueMenuItems(deps.getQueuedItems(), deps.hasPendingDispatch?.());
743
744
  const text = getTelegramQueueMenuListText(menuItems);
744
745
  const messageId = await deps.sendInteractiveMessage(
745
746
  chatId,
@@ -758,6 +759,7 @@ function createQueueMenuCallbackHandler<
758
759
  Context,
759
760
  TModel extends MenuModel = MenuModel,
760
761
  >(deps: {
762
+ hasPendingDispatch?: () => boolean;
761
763
  telegramQueueStore: Queue.TelegramQueueStateStore<Context>;
762
764
  queueMutationRuntime: Queue.TelegramQueueMutationController<Context>;
763
765
  editInteractiveMessage: (
@@ -797,7 +799,7 @@ function createQueueMenuCallbackHandler<
797
799
  return true;
798
800
  }
799
801
  const menuItems = toTelegramQueueMenuItems(
800
- deps.telegramQueueStore.getQueuedItems(),
802
+ deps.telegramQueueStore.getQueuedItems(), deps.hasPendingDispatch?.(),
801
803
  );
802
804
  await deps.editInteractiveMessage(
803
805
  chatId,
@@ -814,7 +816,7 @@ function createQueueMenuCallbackHandler<
814
816
  return deps.telegramQueueStore.getQueuedItems();
815
817
  };
816
818
  const toMenuItems = () => {
817
- return toTelegramQueueMenuItems(getQueueSnapshot());
819
+ return toTelegramQueueMenuItems(getQueueSnapshot(), deps.hasPendingDispatch?.());
818
820
  };
819
821
  const findItem = (cId: number, rId: number) => {
820
822
  return findTelegramQueueItem(toMenuItems(), cId, rId);
@@ -827,6 +829,7 @@ function createQueueMenuCallbackHandler<
827
829
  const flagDeps = {
828
830
  getQueueSnapshot,
829
831
  queueMutationRuntime: deps.queueMutationRuntime,
832
+ hasPendingDispatch: deps.hasPendingDispatch,
830
833
  };
831
834
  return handleTelegramQueueMenuCallback(
832
835
  query.id,
@@ -845,34 +848,10 @@ function createQueueMenuCallbackHandler<
845
848
  dismissGuestPlaceholder: deps.dismissGuestPlaceholder,
846
849
  });
847
850
  },
848
- togglePriority: (cId, rId) =>
849
- applyQueuedTelegramPromptMenuFlags(
850
- cId,
851
- rId,
852
- ctx,
853
- flagDeps,
854
- (item) => ({
855
- priority: item.queueLane !== "priority",
856
- skipped: isQueuedTelegramPromptSkipped(item),
857
- }),
858
- ),
859
- setPriority: (cId, rId, enabled) =>
860
- applyQueuedTelegramPromptMenuFlags(
861
- cId,
862
- rId,
863
- ctx,
864
- flagDeps,
865
- (item) => ({
866
- priority: enabled,
867
- skipped: isQueuedTelegramPromptSkipped(item),
868
- }),
869
- ),
851
+ setLane: (cId, rId, lane) =>
852
+ applyQueuedTelegramPromptMenuFlags(cId, rId, ctx, flagDeps, () => ({ lane })),
870
853
  setSkipped: (cId, rId, skipped) =>
871
- applyQueuedTelegramPromptMenuFlags(cId, rId, ctx, flagDeps, (item) =>
872
- item.kind === "prompt"
873
- ? { priority: item.queueLane === "priority", skipped }
874
- : undefined,
875
- ),
854
+ applyQueuedTelegramPromptMenuFlags(cId, rId, ctx, flagDeps, () => ({ skipped })),
876
855
  updateQueueMessage: deps.editInteractiveMessage,
877
856
  answerCallbackQuery: deps.answerCallbackQuery,
878
857
  updateStatus: deps.updateStatus,
@@ -911,43 +890,12 @@ async function skipQueuedTelegramGuestPrompt<Context>(
911
890
  return true;
912
891
  }
913
892
 
914
- function getQueueMenuReactionDisposition<Context>(
915
- item: Queue.TelegramQueueItem<Context>,
916
- priority: boolean,
917
- skipped: boolean,
918
- ): Queue.TelegramQueueReactionDisposition {
919
- if (priority && skipped) {
920
- return {
921
- kind: "priority-suppressed",
922
- priorityEmoji:
923
- item.kind === "prompt" ? (item.priorityEmoji ?? "⚡") : "⚡",
924
- suppressionEmoji:
925
- item.kind === "prompt" ? (item.reactionSuppressionEmoji ?? "👎") : "👎",
926
- };
927
- }
928
- if (priority) {
929
- return {
930
- kind: "priority",
931
- emoji: item.kind === "prompt" ? (item.priorityEmoji ?? "⚡") : "⚡",
932
- };
933
- }
934
- if (skipped) {
935
- return {
936
- kind: "suppressed",
937
- emoji:
938
- item.kind === "prompt" ? (item.reactionSuppressionEmoji ?? "👎") : "👎",
939
- };
940
- }
941
- return { kind: "default" };
942
- }
943
-
944
- function isQueuedTelegramPromptSkipped<Context>(
945
- item: Queue.TelegramQueueItem<Context>,
946
- ): boolean {
947
- return item.kind === "prompt" && item.reactionSuppressionEmoji !== undefined;
893
+ interface TelegramQueueMenuFlags {
894
+ lane?: Exclude<Queue.TelegramQueueLane, "control">;
895
+ skipped?: boolean;
948
896
  }
949
897
 
950
- /** Applies a menu Priority/Skip choice to one queued item through the reaction disposition path. */
898
+ /** Each menu selection changes one dimension through the existing Queue owner. */
951
899
  function applyQueuedTelegramPromptMenuFlags<Context>(
952
900
  chatId: number,
953
901
  replyToMessageId: number,
@@ -955,10 +903,9 @@ function applyQueuedTelegramPromptMenuFlags<Context>(
955
903
  deps: {
956
904
  getQueueSnapshot: () => Queue.TelegramQueueItem<Context>[];
957
905
  queueMutationRuntime: Queue.TelegramQueueMutationController<Context>;
906
+ hasPendingDispatch?: () => boolean;
958
907
  },
959
- chooseFlags: (
960
- item: Queue.TelegramQueueItem<Context>,
961
- ) => { priority: boolean; skipped: boolean } | undefined,
908
+ chooseFlags: (item: Queue.TelegramQueueItem<Context>) => TelegramQueueMenuFlags | undefined,
962
909
  ): boolean {
963
910
  const item = findTelegramQueueItem(
964
911
  deps.getQueueSnapshot(),
@@ -966,13 +913,18 @@ function applyQueuedTelegramPromptMenuFlags<Context>(
966
913
  replyToMessageId,
967
914
  );
968
915
  const flags = item ? chooseFlags(item) : undefined;
969
- if (!item || !flags) return false;
970
- deps.queueMutationRuntime.applyReactionByMessageId(
971
- replyToMessageId,
972
- getQueueMenuReactionDisposition(item, flags.priority, flags.skipped),
973
- ctx,
974
- );
975
- return true;
916
+ if (!item || item.kind !== "prompt" || !flags) return false;
917
+ const view = findTelegramQueueItem(toTelegramQueueMenuItems(deps.getQueueSnapshot(), deps.hasPendingDispatch?.()), chatId, replyToMessageId);
918
+ if (!view || (flags.lane !== undefined && !view.canSelectLane) || (flags.skipped !== undefined && !view.canSelectDisposition)) return false;
919
+ const disposition: Extract<Queue.TelegramQueueReactionDisposition, { kind: "reaction-transition" }> = { kind: "reaction-transition" };
920
+ if (flags.lane !== undefined) {
921
+ disposition.promptLane = flags.lane;
922
+ disposition.priorityEmoji = flags.lane === "steer" ? "🔥" : flags.lane === "priority" ? "👍" : null;
923
+ }
924
+ if (flags.skipped !== undefined) disposition.suppressionEmoji = flags.skipped ? "👎" : null;
925
+ return deps.queueMutationRuntime.applyReactionByMessageId(replyToMessageId, disposition, ctx) ||
926
+ ((flags.lane === undefined || flags.lane === item.queueLane) &&
927
+ (flags.skipped === undefined || flags.skipped === (item.reactionSuppressionEmoji !== undefined)));
976
928
  }
977
929
 
978
930
  function createQueueMenuSendMessageAdapter(
@@ -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 {
@@ -757,6 +757,7 @@ export interface TelegramThreadAwarePollingPorts<TContext, TOwner> {
757
757
  registerFollowerWithOwner: (
758
758
  ctx: TContext,
759
759
  owner: TOwner,
760
+ options?: { recoverConnection?: boolean },
760
761
  ) => Promise<boolean | undefined>;
761
762
  restoreFollowerWithOwner: (
762
763
  ctx: TContext,
@@ -776,6 +777,7 @@ export interface TelegramThreadAwarePollingControls<TContext, TOwner> {
776
777
  registerFollowerWithLeader: (
777
778
  ctx: TContext,
778
779
  owner: TOwner,
780
+ options?: { recoverConnection?: boolean },
779
781
  ) => Promise<boolean | undefined>;
780
782
  restoreFollowerWithLeader?: (
781
783
  ctx: TContext,
@@ -1180,9 +1182,10 @@ export function createTelegramThreadAwarePollingPorts<TContext, TOwner>(
1180
1182
  const registerFollowerWithOwner = async (
1181
1183
  ctx: TContext,
1182
1184
  owner: TOwner,
1185
+ options?: { recoverConnection?: boolean },
1183
1186
  ): Promise<boolean | undefined> => {
1184
1187
  if (!(await refreshFollowerState())) return undefined;
1185
- return deps.registerFollowerWithLeader(ctx, owner);
1188
+ return deps.registerFollowerWithLeader(ctx, owner, options);
1186
1189
  };
1187
1190
  const restoreFollowerWithOwner = async (
1188
1191
  ctx: TContext,
@@ -1303,7 +1306,9 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
1303
1306
  if (threadModeEnabled) consecutiveDisabledProbes = 0;
1304
1307
  const botState = deps.topicTargetStore.getBotState();
1305
1308
  const current = botState.threadMode;
1306
- if (threadModeEnabled && current === "enabled") return;
1309
+ // Persisted capability is not proof that this owner started its local bus.
1310
+ if (threadModeEnabled && current === "enabled" &&
1311
+ (deps.getPollingStartedWithTelegramBus() || !deps.ownsLock(ctx))) return;
1307
1312
  if (!threadModeEnabled && current === "disabled") {
1308
1313
  if (
1309
1314
  !deps.ownsLock(ctx) ||