@llblab/pi-telegram 0.55.1 → 0.56.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 (60) hide show
  1. package/AGENTS.md +5 -4
  2. package/BACKLOG.md +6 -1
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +28 -17
  5. package/dist/lib/bindings.d.ts +8 -4
  6. package/dist/lib/bindings.js +41 -1
  7. package/dist/lib/bus-follower.d.ts +1 -0
  8. package/dist/lib/bus-follower.js +10 -5
  9. package/dist/lib/bus-leader.d.ts +2 -0
  10. package/dist/lib/bus-leader.js +34 -20
  11. package/dist/lib/bus.d.ts +8 -0
  12. package/dist/lib/bus.js +21 -7
  13. package/dist/lib/commands.d.ts +1 -1
  14. package/dist/lib/commands.js +33 -28
  15. package/dist/lib/extension.js +18 -3
  16. package/dist/lib/lifecycle.d.ts +2 -1
  17. package/dist/lib/lifecycle.js +28 -20
  18. package/dist/lib/locks.d.ts +6 -2
  19. package/dist/lib/locks.js +3 -3
  20. package/dist/lib/menu-queue.d.ts +1 -0
  21. package/dist/lib/menu-queue.js +102 -99
  22. package/dist/lib/pi.d.ts +3 -2
  23. package/dist/lib/pi.js +3 -0
  24. package/dist/lib/polling.d.ts +6 -2
  25. package/dist/lib/polling.js +5 -3
  26. package/dist/lib/queue.d.ts +57 -5
  27. package/dist/lib/queue.js +313 -25
  28. package/dist/lib/routing.d.ts +4 -0
  29. package/dist/lib/routing.js +62 -1
  30. package/dist/lib/status.d.ts +1 -2
  31. package/dist/lib/status.js +6 -7
  32. package/dist/lib/sync.js +11 -1
  33. package/dist/lib/threads.d.ts +11 -0
  34. package/dist/lib/threads.js +57 -6
  35. package/dist/lib/updates.d.ts +12 -7
  36. package/dist/lib/updates.js +44 -9
  37. package/dist/package.json +1 -1
  38. package/docs/README.md +1 -1
  39. package/docs/architecture.md +24 -13
  40. package/docs/callback-namespaces.md +4 -0
  41. package/docs/multi-instance-bus.md +6 -4
  42. package/docs/ui-style.md +14 -10
  43. package/lib/bindings.ts +52 -3
  44. package/lib/bus-follower.ts +13 -5
  45. package/lib/bus-leader.ts +37 -23
  46. package/lib/bus.ts +23 -7
  47. package/lib/commands.ts +52 -47
  48. package/lib/extension.ts +18 -2
  49. package/lib/lifecycle.ts +34 -28
  50. package/lib/locks.ts +5 -2
  51. package/lib/menu-queue.ts +116 -162
  52. package/lib/pi.ts +8 -0
  53. package/lib/polling.ts +7 -2
  54. package/lib/queue.ts +328 -33
  55. package/lib/routing.ts +51 -1
  56. package/lib/status.ts +7 -7
  57. package/lib/sync.ts +11 -0
  58. package/lib/threads.ts +57 -5
  59. package/lib/updates.ts +44 -15
  60. package/package.json +1 -1
@@ -4,6 +4,7 @@
4
4
  * Owns queue-menu rendering, queue item callbacks, and queue-menu runtime adapters while core queue mechanics stay in queue
5
5
  */
6
6
  import { createTelegramMenuDelivery, } from "./menu-model.js";
7
+ import * as Queue from "./queue.js";
7
8
  const QUEUE_ITEM_PROMPT_HTML_LIMIT = 3600;
8
9
  const QUEUE_ITEM_PROMPT_TRUNCATION_SUFFIX = "\n… [truncated]";
9
10
  const EMPTY_QUEUE_REFRESH_TITLES = [
@@ -29,15 +30,25 @@ function getTelegramQueueItemPromptText(item) {
29
30
  .join("\n")
30
31
  .trim() || item.statusSummary);
31
32
  }
32
- function toTelegramQueueMenuItems(items) {
33
+ function toTelegramQueueMenuItems(items, pendingDispatch = false) {
34
+ const head = items[0];
33
35
  return items.map((item, index) => {
36
+ const dispatchProtected = pendingDispatch && (item === head ||
37
+ (head?.kind === "prompt" && head.steerPacketTail?.includes(item) === true));
38
+ const attemptFrozen = item.kind === "prompt" && item.steerAttempted === true && !dispatchProtected;
39
+ const editable = item.kind === "prompt" && item.guestQueryId === undefined && item.guestInlineMessageId === undefined && !dispatchProtected && !attemptFrozen;
34
40
  return {
35
41
  chatId: item.chatId,
36
42
  replyToMessageId: item.replyToMessageId,
37
43
  queueOrder: item.queueOrder,
38
44
  isGuest: item.kind === "prompt" && item.guestQueryId !== undefined,
39
45
  queuePosition: index + 1,
40
- isPriority: item.queueLane === "priority",
46
+ queueLane: item.queueLane,
47
+ canSelectLane: editable && item.queueLane !== "control",
48
+ canSelectDisposition: editable && (item.queueLane !== "control" || item.sourceMessageIds.includes(item.replyToMessageId)),
49
+ dispatchProtected, attemptFrozen,
50
+ exceedsIdleCapacity: item.kind === "prompt" && item.queueLane === "steer" && !item.steerAttempted &&
51
+ !Queue.isTelegramSteerItemWithinIdleCapacity(item),
41
52
  priorityEmoji: item.kind === "prompt" ? item.priorityEmoji : undefined,
42
53
  reactionSuppressionEmoji: item.kind === "prompt" ? item.reactionSuppressionEmoji : undefined,
43
54
  hasAttachments: item.kind === "prompt" && item.queuedAttachments.length > 0,
@@ -59,18 +70,16 @@ function buildTelegramQueueMenuReplyMarkup(items, emptyRefreshIndex = 0) {
59
70
  if (items.length === 0)
60
71
  return { inline_keyboard: [backRow, refreshRow] };
61
72
  const rows = items.map((item) => {
62
- const prefix = item.reactionSuppressionEmoji
63
- ? `${item.reactionSuppressionEmoji} `
64
- : item.isPriority
65
- ? `${item.priorityEmoji ?? "⚡"} `
66
- : item.hasAttachments
67
- ? "📎 "
68
- : "";
73
+ const badge = getTelegramQueueMenuLaneBadge(item);
74
+ const prefix = item.reactionSuppressionEmoji ? `${item.reactionSuppressionEmoji} ` :
75
+ badge ? `${badge} ` : item.hasAttachments ? "📎 " : "";
69
76
  const position = item.reactionSuppressionEmoji
70
77
  ? formatSkippedTelegramQueuePosition(item.queuePosition)
71
78
  : String(item.queuePosition);
72
79
  const ordinalSeparator = item.reactionSuppressionEmoji ? "\u200A" : "";
73
- const label = `${position}${ordinalSeparator}. ${prefix}${item.statusSummary}`;
80
+ const state = item.attemptFrozen ? " · frozen" : item.dispatchProtected ? " · dispatch protected" :
81
+ item.exceedsIdleCapacity ? " · idle capacity" : "";
82
+ const label = `${position}${ordinalSeparator}. ${prefix}${item.statusSummary}${state}`;
74
83
  return [
75
84
  {
76
85
  text: label,
@@ -114,18 +123,29 @@ function escapeTelegramQueueMenuHtmlPreview(text) {
114
123
  }
115
124
  return truncated ? escaped + suffix : escaped;
116
125
  }
126
+ function getTelegramQueueMenuLaneBadge(item) {
127
+ if (item.queueLane === "control")
128
+ return "⚡";
129
+ if (item.queueLane === "steer")
130
+ return "🔥";
131
+ if (item.queueLane !== "priority")
132
+ return "";
133
+ const emoji = item.priorityEmoji?.replace(/\uFE0F/g, "");
134
+ return emoji === "❤" || emoji === "🕊" ? emoji : "👍";
135
+ }
117
136
  function getTelegramQueueMenuItemText(item) {
118
- const badge = item.reactionSuppressionEmoji
119
- ? ` ${item.reactionSuppressionEmoji}`
120
- : item.isPriority
121
- ? ` ${item.priorityEmoji ?? "⚡"}`
122
- : "";
137
+ const marker = item.reactionSuppressionEmoji ?? getTelegramQueueMenuLaneBadge(item);
138
+ const badge = marker ? ` ${marker}` : "";
123
139
  const position = item.reactionSuppressionEmoji
124
140
  ? `<s>${item.queuePosition}</s>.`
125
141
  : `<b>${item.queuePosition}.</b>`;
126
142
  const heading = `${position}${badge}`;
127
143
  const preview = `<pre>${escapeTelegramQueueMenuHtmlPreview(item.promptText)}</pre>`;
128
- return `${heading}\n${preview}`;
144
+ const note = item.attemptFrozen ? "\n<b>⚠️ Attempt frozen.</b>\nSDK/model use is unconfirmed; not editable or automatically retried." :
145
+ item.dispatchProtected ? "\n<b>⏳ Dispatch protected.</b>\nWaiting for agent_start; SDK/model use is unconfirmed." :
146
+ item.exceedsIdleCapacity ? "\n<b>⚠️ Idle Steer capacity exceeded.</b>\nEdit the source or choose Priority/Normal; source groups are never split." :
147
+ item.queueLane === "control" ? "\nControl lane is producer-owned." : "";
148
+ return `${heading}\n${preview}${note}`;
129
149
  }
130
150
  function buildTelegramGuestQueueItemSubmenuReplyMarkup(queueOrder) {
131
151
  return {
@@ -140,32 +160,20 @@ function buildTelegramGuestQueueItemSubmenuReplyMarkup(queueOrder) {
140
160
  ],
141
161
  };
142
162
  }
143
- function buildTelegramQueueItemSubmenuReplyMarkup(chatId, replyToMessageId, isPriority, isSkipped) {
144
- return {
145
- inline_keyboard: [
146
- [{ text: "⬆️ Back", callback_data: "queue:list" }],
147
- [
148
- {
149
- text: isPriority ? "🟡 Priority" : "⚫️ Priority",
150
- callback_data: `queue:prio-set:${chatId}:${replyToMessageId}:priority`,
151
- },
152
- {
153
- text: isPriority ? "⚫️ Normal" : "🔵 Normal",
154
- callback_data: `queue:prio-set:${chatId}:${replyToMessageId}:normal`,
155
- },
156
- ],
157
- [
158
- {
159
- text: isSkipped ? "⚫️ Keep" : "🟢 Keep",
160
- callback_data: `queue:skip-set:${chatId}:${replyToMessageId}:keep`,
161
- },
162
- {
163
- text: isSkipped ? "🔴 Skip" : "⚫️ Skip",
164
- callback_data: `queue:skip-set:${chatId}:${replyToMessageId}:skip`,
165
- },
166
- ],
167
- ],
168
- };
163
+ function buildTelegramQueueItemSubmenuReplyMarkup(item) {
164
+ const rows = [[{ text: "⬆️ Back", callback_data: "queue:list" }]];
165
+ if (item.canSelectLane)
166
+ rows.push([
167
+ { text: item.queueLane === "default" ? "🔵 Normal" : "⚫️ Normal", callback_data: `queue:lane-set:${item.chatId}:${item.replyToMessageId}:normal` },
168
+ { text: item.queueLane === "priority" ? "🟣 Priority" : "⚫️ Priority", callback_data: `queue:lane-set:${item.chatId}:${item.replyToMessageId}:priority` },
169
+ { text: item.queueLane === "steer" ? "🟡 Steer" : "⚫️ Steer", callback_data: `queue:lane-set:${item.chatId}:${item.replyToMessageId}:steer` },
170
+ ]);
171
+ if (item.canSelectDisposition)
172
+ rows.push([
173
+ { text: item.reactionSuppressionEmoji ? "⚫️ Keep" : "🟢 Keep", callback_data: `queue:skip-set:${item.chatId}:${item.replyToMessageId}:keep` },
174
+ { text: item.reactionSuppressionEmoji ? "🔴 Skip" : "⚫️ Skip", callback_data: `queue:skip-set:${item.chatId}:${item.replyToMessageId}:skip` },
175
+ ]);
176
+ return { inline_keyboard: rows };
169
177
  }
170
178
  async function handleTelegramQueueMenuCallback(callbackQueryId, data, replyChatId, replyMessageId, ctx, deps) {
171
179
  if (!data.startsWith("queue:"))
@@ -190,7 +198,13 @@ async function handleTelegramQueueMenuCallback(callbackQueryId, data, replyChatI
190
198
  }
191
199
  const guestSkipMatch = data.match(/^queue:guest-skip:(\d+)$/);
192
200
  if (guestSkipMatch) {
193
- const skipped = await deps.skipGuest(Number(guestSkipMatch[1]), ctx);
201
+ const queueOrder = Number(guestSkipMatch[1]);
202
+ const item = deps.findGuestItem(queueOrder);
203
+ if (item?.dispatchProtected || item?.attemptFrozen) {
204
+ await deps.answerCallbackQuery(callbackQueryId, "Queue item is protected");
205
+ return true;
206
+ }
207
+ const skipped = await deps.skipGuest(queueOrder, ctx);
194
208
  if (!skipped) {
195
209
  await refreshStaleTelegramQueueMenuItem(callbackQueryId, replyChatId, replyMessageId, deps);
196
210
  return true;
@@ -203,9 +217,9 @@ async function handleTelegramQueueMenuCallback(callbackQueryId, data, replyChatI
203
217
  await handleTelegramQueueMenuPick(callbackQueryId, replyChatId, replyMessageId, Number(pickMatch[1]), Number(pickMatch[2]), deps);
204
218
  return true;
205
219
  }
206
- const prioSetMatch = data.match(/^queue:prio-set:(\d+):(\d+):(priority|normal)$/);
220
+ const prioSetMatch = data.match(/^queue:(?:lane-set|prio-set):(\d+):(\d+):(steer|priority|normal)$/);
207
221
  if (prioSetMatch) {
208
- await handleTelegramQueueMenuPrioritySet(callbackQueryId, replyChatId, replyMessageId, Number(prioSetMatch[1]), Number(prioSetMatch[2]), prioSetMatch[3] === "priority", ctx, deps);
222
+ await handleTelegramQueueMenuLaneSet(callbackQueryId, replyChatId, replyMessageId, Number(prioSetMatch[1]), Number(prioSetMatch[2]), prioSetMatch[3] === "normal" ? "default" : prioSetMatch[3], ctx, deps);
209
223
  return true;
210
224
  }
211
225
  const prioMatch = data.match(/^queue:prio:(\d+):(\d+)$/);
@@ -240,7 +254,9 @@ async function handleTelegramGuestQueueMenuPick(callbackQueryId, replyChatId, re
240
254
  if (!item) {
241
255
  return refreshStaleTelegramQueueMenuItem(callbackQueryId, replyChatId, replyMessageId, deps);
242
256
  }
243
- await deps.updateQueueMessage(replyChatId, replyMessageId, getTelegramQueueMenuItemText(item), buildTelegramGuestQueueItemSubmenuReplyMarkup(queueOrder));
257
+ await deps.updateQueueMessage(replyChatId, replyMessageId, getTelegramQueueMenuItemText(item), item.dispatchProtected || item.attemptFrozen
258
+ ? buildTelegramQueueItemSubmenuReplyMarkup(item)
259
+ : buildTelegramGuestQueueItemSubmenuReplyMarkup(queueOrder));
244
260
  await deps.answerCallbackQuery(callbackQueryId);
245
261
  }
246
262
  async function handleTelegramQueueMenuPick(callbackQueryId, replyChatId, replyMessageId, chatId, msgId, deps) {
@@ -248,7 +264,7 @@ async function handleTelegramQueueMenuPick(callbackQueryId, replyChatId, replyMe
248
264
  if (!item) {
249
265
  return refreshStaleTelegramQueueMenuItem(callbackQueryId, replyChatId, replyMessageId, deps);
250
266
  }
251
- await deps.updateQueueMessage(replyChatId, replyMessageId, getTelegramQueueMenuItemText(item), buildTelegramQueueItemSubmenuReplyMarkup(chatId, msgId, item.isPriority, item.reactionSuppressionEmoji !== undefined));
267
+ await deps.updateQueueMessage(replyChatId, replyMessageId, getTelegramQueueMenuItemText(item), buildTelegramQueueItemSubmenuReplyMarkup(item));
252
268
  await deps.answerCallbackQuery(callbackQueryId);
253
269
  }
254
270
  async function handleTelegramQueueMenuPriority(callbackQueryId, replyChatId, replyMessageId, chatId, msgId, ctx, deps) {
@@ -256,37 +272,43 @@ async function handleTelegramQueueMenuPriority(callbackQueryId, replyChatId, rep
256
272
  if (!item) {
257
273
  return refreshStaleTelegramQueueMenuItem(callbackQueryId, replyChatId, replyMessageId, deps);
258
274
  }
259
- await updateTelegramQueueMenuPriority(callbackQueryId, replyChatId, replyMessageId, chatId, msgId, !item.isPriority, ctx, deps);
275
+ await updateTelegramQueueMenuLane(callbackQueryId, replyChatId, replyMessageId, chatId, msgId, item.queueLane === "priority" ? "default" : "priority", ctx, deps);
260
276
  }
261
- async function handleTelegramQueueMenuPrioritySet(callbackQueryId, replyChatId, replyMessageId, chatId, msgId, enabled, ctx, deps) {
277
+ async function handleTelegramQueueMenuLaneSet(callbackQueryId, replyChatId, replyMessageId, chatId, msgId, lane, ctx, deps) {
262
278
  const item = deps.findItem(chatId, msgId);
263
279
  if (!item) {
264
280
  return refreshStaleTelegramQueueMenuItem(callbackQueryId, replyChatId, replyMessageId, deps);
265
281
  }
266
- await updateTelegramQueueMenuPriority(callbackQueryId, replyChatId, replyMessageId, chatId, msgId, enabled, ctx, deps);
282
+ await updateTelegramQueueMenuLane(callbackQueryId, replyChatId, replyMessageId, chatId, msgId, lane, ctx, deps);
267
283
  }
268
- async function updateTelegramQueueMenuPriority(callbackQueryId, replyChatId, replyMessageId, chatId, msgId, enabled, ctx, deps) {
269
- deps.setPriority(chatId, msgId, enabled);
284
+ async function updateTelegramQueueMenuLane(callbackQueryId, replyChatId, replyMessageId, chatId, msgId, lane, ctx, deps) {
285
+ if (!deps.setLane(chatId, msgId, lane)) {
286
+ await deps.answerCallbackQuery(callbackQueryId, "Queue item is protected");
287
+ return;
288
+ }
270
289
  deps.updateStatus(ctx);
271
290
  const updated = deps.findItem(chatId, msgId);
272
291
  if (!updated) {
273
292
  return refreshStaleTelegramQueueMenuItem(callbackQueryId, replyChatId, replyMessageId, deps);
274
293
  }
275
- await deps.updateQueueMessage(replyChatId, replyMessageId, getTelegramQueueMenuItemText(updated), buildTelegramQueueItemSubmenuReplyMarkup(chatId, msgId, updated.isPriority, updated.reactionSuppressionEmoji !== undefined));
276
- await deps.answerCallbackQuery(callbackQueryId, updated.isPriority ? "Prioritized" : "Normal priority");
294
+ await deps.updateQueueMessage(replyChatId, replyMessageId, getTelegramQueueMenuItemText(updated), buildTelegramQueueItemSubmenuReplyMarkup(updated));
295
+ await deps.answerCallbackQuery(callbackQueryId, updated.queueLane === "steer" ? "Steer selected" : updated.queueLane === "priority" ? "Prioritized" : "Normal priority");
277
296
  }
278
297
  async function handleTelegramQueueMenuSkipSet(callbackQueryId, replyChatId, replyMessageId, chatId, msgId, skipped, ctx, deps) {
279
298
  const item = deps.findItem(chatId, msgId);
280
299
  if (!item) {
281
300
  return refreshStaleTelegramQueueMenuItem(callbackQueryId, replyChatId, replyMessageId, deps);
282
301
  }
283
- deps.setSkipped(chatId, msgId, skipped);
302
+ if (!deps.setSkipped(chatId, msgId, skipped)) {
303
+ await deps.answerCallbackQuery(callbackQueryId, "Queue item is protected");
304
+ return;
305
+ }
284
306
  deps.updateStatus(ctx);
285
307
  const updated = deps.findItem(chatId, msgId);
286
308
  if (!updated) {
287
309
  return refreshStaleTelegramQueueMenuItem(callbackQueryId, replyChatId, replyMessageId, deps);
288
310
  }
289
- await deps.updateQueueMessage(replyChatId, replyMessageId, getTelegramQueueMenuItemText(updated), buildTelegramQueueItemSubmenuReplyMarkup(chatId, msgId, updated.isPriority, updated.reactionSuppressionEmoji !== undefined));
311
+ await deps.updateQueueMessage(replyChatId, replyMessageId, getTelegramQueueMenuItemText(updated), buildTelegramQueueItemSubmenuReplyMarkup(updated));
290
312
  await deps.answerCallbackQuery(callbackQueryId);
291
313
  }
292
314
  export function createTelegramQueueMenuRuntime(deps) {
@@ -297,6 +319,7 @@ export function createTelegramQueueMenuRuntime(deps) {
297
319
  const delivery = createTelegramMenuDelivery({ chatId, threadId }, options, deps);
298
320
  return createOpenQueueMenu({
299
321
  getQueuedItems: deps.telegramQueueStore.getQueuedItems,
322
+ hasPendingDispatch: deps.hasPendingDispatch,
300
323
  getModelMenuState: (chat, context) => deps.getModelMenuState(chat, context, threadId),
301
324
  storeModelMenuState: delivery.storeModelMenuState,
302
325
  sendInteractiveMessage: delivery.assertAuthority
@@ -305,6 +328,7 @@ export function createTelegramQueueMenuRuntime(deps) {
305
328
  })(chatId, replyToMessageId, ctx);
306
329
  },
307
330
  handleCallbackQuery: createQueueMenuCallbackHandler({
331
+ hasPendingDispatch: deps.hasPendingDispatch,
308
332
  telegramQueueStore: deps.telegramQueueStore,
309
333
  queueMutationRuntime: deps.queueMutationRuntime,
310
334
  editInteractiveMessage: editQueueMenuMessage,
@@ -319,7 +343,7 @@ export function createTelegramQueueMenuRuntime(deps) {
319
343
  function createOpenQueueMenu(deps) {
320
344
  return async (chatId, replyToMessageId, ctx) => {
321
345
  const state = await deps.getModelMenuState(chatId, ctx);
322
- const menuItems = toTelegramQueueMenuItems(deps.getQueuedItems());
346
+ const menuItems = toTelegramQueueMenuItems(deps.getQueuedItems(), deps.hasPendingDispatch?.());
323
347
  const text = getTelegramQueueMenuListText(menuItems);
324
348
  const messageId = await deps.sendInteractiveMessage(chatId, replyToMessageId, text, buildTelegramQueueMenuReplyMarkup(menuItems));
325
349
  if (messageId === undefined)
@@ -342,7 +366,7 @@ function createQueueMenuCallbackHandler(deps) {
342
366
  await deps.answerCallbackQuery(query.id, "Interactive message expired");
343
367
  return true;
344
368
  }
345
- const menuItems = toTelegramQueueMenuItems(deps.telegramQueueStore.getQueuedItems());
369
+ const menuItems = toTelegramQueueMenuItems(deps.telegramQueueStore.getQueuedItems(), deps.hasPendingDispatch?.());
346
370
  await deps.editInteractiveMessage(chatId, messageId, getTelegramQueueMenuListText(menuItems), buildTelegramQueueMenuReplyMarkup(menuItems));
347
371
  state.mode = "queue";
348
372
  await deps.answerCallbackQuery(query.id);
@@ -354,7 +378,7 @@ function createQueueMenuCallbackHandler(deps) {
354
378
  return deps.telegramQueueStore.getQueuedItems();
355
379
  };
356
380
  const toMenuItems = () => {
357
- return toTelegramQueueMenuItems(getQueueSnapshot());
381
+ return toTelegramQueueMenuItems(getQueueSnapshot(), deps.hasPendingDispatch?.());
358
382
  };
359
383
  const findItem = (cId, rId) => {
360
384
  return findTelegramQueueItem(toMenuItems(), cId, rId);
@@ -367,6 +391,7 @@ function createQueueMenuCallbackHandler(deps) {
367
391
  const flagDeps = {
368
392
  getQueueSnapshot,
369
393
  queueMutationRuntime: deps.queueMutationRuntime,
394
+ hasPendingDispatch: deps.hasPendingDispatch,
370
395
  };
371
396
  return handleTelegramQueueMenuCallback(query.id, data, chatId, messageId, ctx, {
372
397
  getQueuedItems: toMenuItems,
@@ -379,17 +404,8 @@ function createQueueMenuCallbackHandler(deps) {
379
404
  dismissGuestPlaceholder: deps.dismissGuestPlaceholder,
380
405
  });
381
406
  },
382
- togglePriority: (cId, rId) => applyQueuedTelegramPromptMenuFlags(cId, rId, ctx, flagDeps, (item) => ({
383
- priority: item.queueLane !== "priority",
384
- skipped: isQueuedTelegramPromptSkipped(item),
385
- })),
386
- setPriority: (cId, rId, enabled) => applyQueuedTelegramPromptMenuFlags(cId, rId, ctx, flagDeps, (item) => ({
387
- priority: enabled,
388
- skipped: isQueuedTelegramPromptSkipped(item),
389
- })),
390
- setSkipped: (cId, rId, skipped) => applyQueuedTelegramPromptMenuFlags(cId, rId, ctx, flagDeps, (item) => item.kind === "prompt"
391
- ? { priority: item.queueLane === "priority", skipped }
392
- : undefined),
407
+ setLane: (cId, rId, lane) => applyQueuedTelegramPromptMenuFlags(cId, rId, ctx, flagDeps, () => ({ lane })),
408
+ setSkipped: (cId, rId, skipped) => applyQueuedTelegramPromptMenuFlags(cId, rId, ctx, flagDeps, () => ({ skipped })),
393
409
  updateQueueMessage: deps.editInteractiveMessage,
394
410
  answerCallbackQuery: deps.answerCallbackQuery,
395
411
  updateStatus: deps.updateStatus,
@@ -413,39 +429,26 @@ async function skipQueuedTelegramGuestPrompt(queueOrder, ctx, deps) {
413
429
  }
414
430
  return true;
415
431
  }
416
- function getQueueMenuReactionDisposition(item, priority, skipped) {
417
- if (priority && skipped) {
418
- return {
419
- kind: "priority-suppressed",
420
- priorityEmoji: item.kind === "prompt" ? (item.priorityEmoji ?? "⚡") : "⚡",
421
- suppressionEmoji: item.kind === "prompt" ? (item.reactionSuppressionEmoji ?? "👎") : "👎",
422
- };
423
- }
424
- if (priority) {
425
- return {
426
- kind: "priority",
427
- emoji: item.kind === "prompt" ? (item.priorityEmoji ?? "⚡") : "⚡",
428
- };
429
- }
430
- if (skipped) {
431
- return {
432
- kind: "suppressed",
433
- emoji: item.kind === "prompt" ? (item.reactionSuppressionEmoji ?? "👎") : "👎",
434
- };
435
- }
436
- return { kind: "default" };
437
- }
438
- function isQueuedTelegramPromptSkipped(item) {
439
- return item.kind === "prompt" && item.reactionSuppressionEmoji !== undefined;
440
- }
441
- /** Applies a menu Priority/Skip choice to one queued item through the reaction disposition path. */
432
+ /** Each menu selection changes one dimension through the existing Queue owner. */
442
433
  function applyQueuedTelegramPromptMenuFlags(chatId, replyToMessageId, ctx, deps, chooseFlags) {
443
434
  const item = findTelegramQueueItem(deps.getQueueSnapshot(), chatId, replyToMessageId);
444
435
  const flags = item ? chooseFlags(item) : undefined;
445
- if (!item || !flags)
436
+ if (!item || item.kind !== "prompt" || !flags)
446
437
  return false;
447
- deps.queueMutationRuntime.applyReactionByMessageId(replyToMessageId, getQueueMenuReactionDisposition(item, flags.priority, flags.skipped), ctx);
448
- return true;
438
+ const view = findTelegramQueueItem(toTelegramQueueMenuItems(deps.getQueueSnapshot(), deps.hasPendingDispatch?.()), chatId, replyToMessageId);
439
+ if (!view || (flags.lane !== undefined && !view.canSelectLane) || (flags.skipped !== undefined && !view.canSelectDisposition))
440
+ return false;
441
+ const disposition = { kind: "reaction-transition" };
442
+ if (flags.lane !== undefined) {
443
+ disposition.promptLane = flags.lane;
444
+ disposition.priorityEmoji = flags.lane === "steer" ? "🔥" : flags.lane === "priority" ?
445
+ getTelegramQueueMenuLaneBadge({ queueLane: "priority", priorityEmoji: item.queueLane === "priority" ? item.priorityEmoji : undefined }) : null;
446
+ }
447
+ if (flags.skipped !== undefined)
448
+ disposition.suppressionEmoji = flags.skipped ? (item.reactionSuppressionEmoji ?? "👎") : null;
449
+ return deps.queueMutationRuntime.applyReactionByMessageId(replyToMessageId, disposition, ctx) ||
450
+ ((flags.lane === undefined || flags.lane === item.queueLane) &&
451
+ (flags.skipped === undefined || flags.skipped === (item.reactionSuppressionEmoji !== undefined)));
449
452
  }
450
453
  function createQueueMenuSendMessageAdapter(sendInteractiveMessage) {
451
454
  return (chatId, _replyToMessageId, text, replyMarkup) => {
package/dist/lib/pi.d.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  * Owns direct pi SDK imports and exposes narrow bridge-facing helpers/types for the extension composition layer
5
5
  */
6
6
  import type { AssistantMessageEvent } from "@earendil-works/pi-ai";
7
- import { type AgentEndEvent, type AgentSettledEvent, type AgentStartEvent, type BeforeAgentStartEvent, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type InputEvent, type MessageEndEvent, type SessionBeforeCompactEvent, type SessionCompactEvent, type SessionShutdownEvent, type SessionStartEvent, type SlashCommandInfo, type UIPromptEndEvent, type UIPromptStartEvent } from "@earendil-works/pi-coding-agent";
8
- export type { AgentEndEvent, AgentSettledEvent, AgentStartEvent, AssistantMessageEvent, BeforeAgentStartEvent, ExtensionAPI, ExtensionCommandContext, ExtensionContext, InputEvent, MessageEndEvent, SessionBeforeCompactEvent, SessionCompactEvent, SessionShutdownEvent, SessionStartEvent, SlashCommandInfo, UIPromptEndEvent, UIPromptStartEvent, };
7
+ import { type AgentEndEvent, type AgentSettledEvent, type AgentStartEvent, type BeforeAgentStartEvent, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type InputEvent, type MessageEndEvent, type SessionBeforeCompactEvent, type SessionCompactEvent, type SessionShutdownEvent, type SessionStartEvent, type SlashCommandInfo, type TurnEndEvent, type TurnEndEventResult, type UIPromptEndEvent, type UIPromptStartEvent } from "@earendil-works/pi-coding-agent";
8
+ export type { AgentEndEvent, AgentSettledEvent, AgentStartEvent, AssistantMessageEvent, BeforeAgentStartEvent, ExtensionAPI, ExtensionCommandContext, ExtensionContext, InputEvent, MessageEndEvent, SessionBeforeCompactEvent, SessionCompactEvent, SessionShutdownEvent, SessionStartEvent, SlashCommandInfo, TurnEndEvent, TurnEndEventResult, UIPromptEndEvent, UIPromptStartEvent, };
9
9
  export interface SessionCompactFailedEvent {
10
10
  type: "session_compact_failed";
11
11
  reason: "manual" | "threshold" | "overflow";
@@ -72,5 +72,6 @@ export declare function getExtensionContextCwd(ctx: ExtensionContext): string;
72
72
  export declare function getExtensionContextSessionId(ctx: ExtensionContext): string;
73
73
  export declare function getExtensionContextSessionId(ctx: ExtensionContext | undefined): string | undefined;
74
74
  export declare function isExtensionContextIdle(ctx: ExtensionContext): boolean;
75
+ export declare function isExtensionContextAbortRequested(ctx: ExtensionContext): boolean;
75
76
  export declare function hasExtensionContextPendingMessages(ctx: ExtensionContext): boolean;
76
77
  export declare function compactExtensionContext(ctx: ExtensionContext, callbacks: Parameters<ExtensionContext["compact"]>[0]): ReturnType<ExtensionContext["compact"]>;
package/dist/lib/pi.js CHANGED
@@ -122,6 +122,9 @@ export function getExtensionContextSessionId(ctx) {
122
122
  export function isExtensionContextIdle(ctx) {
123
123
  return ctx.isIdle();
124
124
  }
125
+ export function isExtensionContextAbortRequested(ctx) {
126
+ return ctx.signal?.aborted === true;
127
+ }
125
128
  export function hasExtensionContextPendingMessages(ctx) {
126
129
  return ctx.hasPendingMessages();
127
130
  }
@@ -221,7 +221,9 @@ export interface TelegramThreadAwarePollingPorts<TContext, TOwner> {
221
221
  forceFreshLeaderThread?: boolean;
222
222
  }) => Promise<void>;
223
223
  stopPolling: () => Promise<void>;
224
- registerFollowerWithOwner: (ctx: TContext, owner: TOwner) => Promise<boolean | undefined>;
224
+ registerFollowerWithOwner: (ctx: TContext, owner: TOwner, options?: {
225
+ recoverConnection?: boolean;
226
+ }) => Promise<boolean | undefined>;
225
227
  restoreFollowerWithOwner: (ctx: TContext, owner: TOwner) => Promise<boolean | undefined>;
226
228
  stopFollowerRegistration: () => void;
227
229
  }
@@ -233,7 +235,9 @@ export interface TelegramThreadAwarePollingControls<TContext, TOwner> {
233
235
  stopBusLeaderPolling: () => Promise<void>;
234
236
  startLeaderHealth: () => void;
235
237
  stopLeaderHealth: () => void;
236
- registerFollowerWithLeader: (ctx: TContext, owner: TOwner) => Promise<boolean | undefined>;
238
+ registerFollowerWithLeader: (ctx: TContext, owner: TOwner, options?: {
239
+ recoverConnection?: boolean;
240
+ }) => Promise<boolean | undefined>;
237
241
  restoreFollowerWithLeader?: (ctx: TContext, owner: TOwner) => Promise<boolean | undefined>;
238
242
  hasRememberedWorkspaceBinding?: (ctx: TContext) => boolean;
239
243
  stopFollowerRegistration: () => void;
@@ -752,10 +752,10 @@ export function createTelegramThreadAwarePollingPorts(deps) {
752
752
  }
753
753
  return deps.topicTargetStore.getBotState().threadMode === "enabled";
754
754
  };
755
- const registerFollowerWithOwner = async (ctx, owner) => {
755
+ const registerFollowerWithOwner = async (ctx, owner, options) => {
756
756
  if (!(await refreshFollowerState()))
757
757
  return undefined;
758
- return deps.registerFollowerWithLeader(ctx, owner);
758
+ return deps.registerFollowerWithLeader(ctx, owner, options);
759
759
  };
760
760
  const restoreFollowerWithOwner = async (ctx, owner) => {
761
761
  if (!(await refreshFollowerState())) {
@@ -844,7 +844,9 @@ export function createTelegramThreadCapabilityMonitor(deps) {
844
844
  consecutiveDisabledProbes = 0;
845
845
  const botState = deps.topicTargetStore.getBotState();
846
846
  const current = botState.threadMode;
847
- if (threadModeEnabled && current === "enabled")
847
+ // Persisted capability is not proof that this owner started its local bus.
848
+ if (threadModeEnabled && current === "enabled" &&
849
+ (deps.getPollingStartedWithTelegramBus() || !deps.ownsLock(ctx)))
848
850
  return;
849
851
  if (!threadModeEnabled && current === "disabled") {
850
852
  if (!deps.ownsLock(ctx) ||
@@ -1,8 +1,9 @@
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
+ import { type TurnEndEvent, type TurnEndEventResult } from "./pi.ts";
6
7
  export interface QueuedAttachment {
7
8
  path: string;
8
9
  fileName: string;
@@ -18,7 +19,7 @@ export interface TelegramPromptImageContent {
18
19
  }
19
20
  export type TelegramPromptContent = TelegramPromptTextContent | TelegramPromptImageContent;
20
21
  export type TelegramQueueItemKind = "prompt" | "control";
21
- export type TelegramQueueLane = "control" | "priority" | "default";
22
+ export type TelegramQueueLane = "control" | "steer" | "priority" | "default";
22
23
  export type TelegramQueueReactionDisposition = {
23
24
  kind: "default";
24
25
  } | {
@@ -33,6 +34,7 @@ export type TelegramQueueReactionDisposition = {
33
34
  suppressionEmoji: string;
34
35
  } | {
35
36
  kind: "reaction-transition";
37
+ promptLane?: Exclude<TelegramQueueLane, "control">;
36
38
  priorityEmoji?: string | null;
37
39
  suppressionEmoji?: string | null;
38
40
  };
@@ -42,7 +44,7 @@ export interface TelegramQueueAdmissionReceipt {
42
44
  sourceUpdateIds: readonly number[];
43
45
  journalBindingKey?: string;
44
46
  }
45
- export type TelegramQueueAdmissionMode = "control-queue" | "priority-queue" | "default-queue";
47
+ export type TelegramQueueAdmissionMode = "control-queue" | "steer-queue" | "priority-queue" | "default-queue";
46
48
  export interface TelegramQueueLaneContract {
47
49
  lane: TelegramQueueLane;
48
50
  admissionMode: TelegramQueueAdmissionMode;
@@ -84,6 +86,11 @@ export interface PendingTelegramTurn extends TelegramQueueItemBase {
84
86
  historyText: string;
85
87
  priorityEmoji?: string;
86
88
  reactionSuppressionEmoji?: string;
89
+ /** Frozen before receipt mutation; failed/partial attempts must never become executable again. */
90
+ steerAttempted?: true;
91
+ /** Original tail objects of an issued idle packet; metadata only, not another waiting queue. */
92
+ steerPacketTail?: readonly PendingTelegramTurn[];
93
+ steerPacketSourceOrders?: readonly number[];
87
94
  /** Emit the explicit aborted-turn notice when /next settles this active turn. */
88
95
  announceNextAbortOnEnd?: boolean;
89
96
  /** Turn should preferably be delivered as voice (mirror mode + user sent voice) */
@@ -220,8 +227,11 @@ export declare function appendTelegramQueueItem<TContext = unknown, TItem extend
220
227
  export declare function createTelegramQueueHandoff<TContext>(input: {
221
228
  handoffToken: string;
222
229
  item: TelegramQueueItem<TContext>;
230
+ steerSupported?: boolean;
223
231
  }): TelegramQueueHandoff;
224
- export declare function createTelegramQueueHandoffPayload<TContext>(item: TelegramQueueItem<TContext>): TelegramQueueHandoffPayload;
232
+ export declare function createTelegramQueueHandoffPayload<TContext>(item: TelegramQueueItem<TContext>, options?: {
233
+ steerSupported?: boolean;
234
+ }): TelegramQueueHandoffPayload;
225
235
  export declare function restoreTelegramQueueHandoffPayload<TContext>(payload: TelegramQueueHandoffPayload, createControlExecution: (payload: TelegramControlQueueHandoffPayload) => PendingTelegramControlItem<TContext>["execute"]): TelegramQueueItem<TContext>;
226
236
  export declare function removeTelegramQueueItemByReceipt<TContext>(input: {
227
237
  receipt: TelegramQueueAdmissionReceipt;
@@ -258,7 +268,6 @@ export declare function consumeDispatchedTelegramPrompt<TContext = unknown>(item
258
268
  activeTurn?: PendingTelegramTurn;
259
269
  remainingItems: TelegramQueueItem<TContext>[];
260
270
  };
261
- export declare function formatQueuedTelegramItemsStatus<TContext = unknown>(items: TelegramQueueItem<TContext>[]): string;
262
271
  export declare function truncateTelegramQueueSummary(text: string, maxWords?: number, maxLength?: number): string;
263
272
  export declare function canDispatchTelegramTurnState(state: TelegramDispatchGuardState): boolean;
264
273
  export interface TelegramDispatchReadinessDeps<TContext> {
@@ -296,6 +305,11 @@ export declare function createTelegramControlItemBuilder<TContext = unknown>(dep
296
305
  export type TelegramQueueDispatchAction<TContext = unknown> = {
297
306
  kind: "none";
298
307
  remainingItems: TelegramQueueItem<TContext>[];
308
+ } | {
309
+ kind: "steer";
310
+ item: PendingTelegramTurn;
311
+ packet: readonly PendingTelegramTurn[];
312
+ remainingItems: TelegramQueueItem<TContext>[];
299
313
  } | {
300
314
  kind: "control";
301
315
  item: PendingTelegramControlItem<TContext>;
@@ -305,6 +319,38 @@ export type TelegramQueueDispatchAction<TContext = unknown> = {
305
319
  item: PendingTelegramTurn;
306
320
  remainingItems: TelegramQueueItem<TContext>[];
307
321
  };
322
+ export type TelegramSteerPacketPlan = {
323
+ kind: "none";
324
+ } | {
325
+ kind: "skip";
326
+ item: PendingTelegramTurn;
327
+ } | {
328
+ kind: "packet";
329
+ items: readonly PendingTelegramTurn[];
330
+ };
331
+ /** Plan a fixed ready prefix; publication/settlement remain with the Queue effect shell. */
332
+ export declare function planTelegramSteerPacket<TContext>(items: readonly TelegramQueueItem<TContext>[], options: {
333
+ blocked: boolean;
334
+ isItemReady: (item: TelegramQueueItem<TContext>) => boolean;
335
+ canAppend?: (packet: readonly PendingTelegramTurn[], item: PendingTelegramTurn) => boolean;
336
+ }): TelegramSteerPacketPlan;
337
+ export declare function isTelegramSteerItemWithinIdleCapacity(item: PendingTelegramTurn): boolean;
338
+ export interface TelegramSteerBoundaryRuntimeDeps<TContext> extends TelegramQueueStore<TContext> {
339
+ isContextCurrent: (ctx: TContext) => boolean;
340
+ isBlocked: (ctx: TContext) => boolean;
341
+ getGeneration: () => number;
342
+ isItemReady: (item: TelegramQueueItem<TContext>, ctx: TContext) => boolean;
343
+ isItemCurrent: (item: PendingTelegramTurn, ctx: TContext) => boolean;
344
+ getActiveTurn: () => PendingTelegramTurn | undefined;
345
+ commitPacket: (items: readonly PendingTelegramTurn[], ctx: TContext) => boolean;
346
+ discardItem: (item: PendingTelegramTurn, ctx: TContext) => boolean;
347
+ updateStatus: (ctx: TContext) => void;
348
+ recordRuntimeEvent?: TelegramRuntimeEventRecorderPort["recordRuntimeEvent"];
349
+ }
350
+ /** One publication attempt; journal ACK is source disposition, never SDK/model-use confirmation. */
351
+ export declare function createTelegramSteerBoundaryRuntime<TContext>(deps: TelegramSteerBoundaryRuntimeDeps<TContext>): {
352
+ onTurnEnd(event: Pick<TurnEndEvent, "turnIndex" | "outcome">, ctx: TContext): TurnEndEventResult | undefined;
353
+ };
308
354
  export declare function planNextTelegramQueueAction<TContext = unknown>(items: TelegramQueueItem<TContext>[], canDispatch: boolean): TelegramQueueDispatchAction<TContext>;
309
355
  export declare function shouldDispatchAfterTelegramAgentEnd(options: {
310
356
  hasTurn: boolean;
@@ -752,6 +798,12 @@ export interface TelegramQueueDispatchControllerDeps<TContext = unknown> extends
752
798
  sendUserMessage: TelegramDispatchRuntimeDeps<TContext>["sendUserMessage"];
753
799
  onPromptDispatchFailure: (ctx: TContext, message: string) => void;
754
800
  reconcileNextDispatchAnnouncementReplyOwnership?: (item: PendingTelegramTurn) => void;
801
+ steerDispatch?: {
802
+ commitPacket: (items: readonly PendingTelegramTurn[], ctx: TContext) => boolean;
803
+ isContextCurrent?: (ctx: TContext) => boolean;
804
+ /** Rechecks idle/native/abort guards without rejecting this packet's own pending flag. */
805
+ canSubmit?: (ctx: TContext) => boolean;
806
+ };
755
807
  isQueueItemTransportActive?: (item: TelegramQueueItem<TContext>) => boolean;
756
808
  hasPendingInboundQueueMutationForItem?: (item: TelegramQueueItem<TContext>) => boolean;
757
809
  isQueueItemAdmissionReady?: (item: TelegramQueueItem<TContext>) => boolean;