@llblab/pi-kit 0.18.0 → 0.18.2

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 (42) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -2
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +1 -1
  4. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +5 -0
  5. package/node_modules/@llblab/pi-state-flow/README.md +1 -1
  6. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +3 -3
  7. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +1 -1
  8. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +1 -1
  9. package/node_modules/@llblab/pi-state-flow/dist/package.json +5 -5
  10. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +27 -82
  11. package/node_modules/@llblab/pi-state-flow/docs/performance.md +2 -2
  12. package/node_modules/@llblab/pi-state-flow/docs/usage.md +1 -1
  13. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +3 -3
  14. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +1 -1
  15. package/node_modules/@llblab/pi-state-flow/package.json +5 -5
  16. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +7 -0
  17. package/node_modules/@llblab/pi-telegram/README.md +1 -2
  18. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +1 -3
  19. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +1 -6
  20. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +1 -4
  21. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +13 -33
  22. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +1 -4
  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 +72 -1
  25. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +7 -0
  26. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +27 -0
  27. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +3 -0
  28. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +17 -0
  29. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +8 -1
  30. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  31. package/node_modules/@llblab/pi-telegram/docs/architecture.md +2 -0
  32. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +1 -1
  33. package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
  34. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +0 -10
  35. package/node_modules/@llblab/pi-telegram/lib/commands.ts +18 -41
  36. package/node_modules/@llblab/pi-telegram/lib/extension.ts +1 -8
  37. package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +126 -1
  38. package/node_modules/@llblab/pi-telegram/lib/queue.ts +46 -0
  39. package/node_modules/@llblab/pi-telegram/lib/replies.ts +19 -0
  40. package/node_modules/@llblab/pi-telegram/lib/sync.ts +8 -1
  41. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  42. package/package.json +3 -3
@@ -4,6 +4,8 @@
4
4
  * Owns Telegram slash-command normalization, bot command metadata, pi-side command registration, and command-initiated session replacement orchestration behind runtime ports
5
5
  */
6
6
 
7
+ import { randomUUID } from "node:crypto";
8
+
7
9
  import {
8
10
  pairTelegramUserIfNeeded,
9
11
  type TelegramConfigStore,
@@ -366,7 +368,6 @@ export function createTelegramBotCommandRegistrar(
366
368
  export interface TelegramBridgeCommandStartPollingOptions {
367
369
  force?: boolean;
368
370
  forceFreshLeaderThread?: boolean;
369
- requestedThreadName?: string;
370
371
  }
371
372
 
372
373
  export interface TelegramBridgeCommandStartPollingResult {
@@ -407,7 +408,6 @@ export interface TelegramBridgeCommandRegistrationDeps {
407
408
  ctx: ExtensionCommandContext,
408
409
  profileName: string,
409
410
  ) => Promise<boolean>;
410
- validateThreadName?: (threadName: string) => string | undefined;
411
411
  }
412
412
 
413
413
  export type TelegramThreadDisplayNameRenamePort = (
@@ -466,17 +466,6 @@ function parseTelegramProfileArg(args: string): string | undefined {
466
466
  return word === TELEGRAM_DEFAULT_PROFILE_NAME ? undefined : word;
467
467
  }
468
468
 
469
- export function parseTelegramRequestedThreadName(
470
- args: string,
471
- ): string | undefined {
472
- const token = args
473
- .trim()
474
- .split(/\s+/)
475
- .find((word) => /^as=/i.test(word));
476
- const value = token?.slice(3).trim();
477
- return value || undefined;
478
- }
479
-
480
469
  function formatTelegramTakeoverTitle(ctx: ExtensionCommandContext): string {
481
470
  return ctx.ui.theme.fg("accent", "pi-telegram");
482
471
  }
@@ -498,7 +487,7 @@ export function registerTelegramBridgeCommands(
498
487
  deps: TelegramBridgeCommandRegistrationDeps,
499
488
  ): void {
500
489
  pi.registerCommand("telegram-setup", {
501
- description: "Configure Telegram bot token. Use /telegram-setup <name> for named profiles.",
490
+ description: "<profile> — Configure Telegram bot token",
502
491
  handler: async (args, ctx) => {
503
492
  await deps.promptForConfig(ctx, parseTelegramProfileArg(args));
504
493
  },
@@ -513,28 +502,17 @@ export function registerTelegramBridgeCommands(
513
502
  },
514
503
  });
515
504
  pi.registerCommand("telegram-connect", {
516
- description:
517
- "Start the Telegram bridge. Use /telegram-connect <profile> and optional as=Name for a fresh Workspace Thread.",
505
+ description: "<profile> — Start Telegram bridge",
518
506
  handler: async (args, ctx) => {
519
- const profileName = parseTelegramProfileArg(args);
520
- const requestedNameTokens = args
521
- .trim()
522
- .split(/\s+/)
523
- .filter((word) => /^as=/i.test(word));
524
- const requestedThreadName = parseTelegramRequestedThreadName(args);
525
- const requestedNameError =
526
- requestedNameTokens.length > 1
527
- ? "Specify at most one as=Name Workspace Thread name."
528
- : requestedNameTokens.length === 1 && !requestedThreadName
529
- ? "Usage: /telegram-connect [profile] as=Flightprice"
530
- : requestedThreadName
531
- ? deps.validateThreadName?.(requestedThreadName)
532
- : undefined;
533
- if (requestedNameError) {
534
- ctx.ui.notify(requestedNameError, "warning");
507
+ if (args.trim().split(/\s+/).some((word) => /^as=/i.test(word))) {
508
+ ctx.ui.notify(
509
+ "Thread names are configured from Telegram, not from Pi commands.",
510
+ "warning",
511
+ );
535
512
  deps.updateStatus(ctx);
536
513
  return;
537
514
  }
515
+ const profileName = parseTelegramProfileArg(args);
538
516
  if (profileName && deps.activateProfileConfig) {
539
517
  const ok = await deps.activateProfileConfig(ctx, profileName);
540
518
  if (!ok) {
@@ -597,7 +575,6 @@ export function registerTelegramBridgeCommands(
597
575
  };
598
576
  let result = await startWithRecovery({
599
577
  forceFreshLeaderThread: true,
600
- ...(requestedThreadName ? { requestedThreadName } : {}),
601
578
  });
602
579
  if (result && !result.ok && result.canTakeover) {
603
580
  const confirmed = await ctx.ui.confirm(
@@ -612,7 +589,6 @@ export function registerTelegramBridgeCommands(
612
589
  result = await startWithRecovery({
613
590
  force: true,
614
591
  forceFreshLeaderThread: true,
615
- ...(requestedThreadName ? { requestedThreadName } : {}),
616
592
  });
617
593
  }
618
594
  if (result?.message) {
@@ -625,8 +601,7 @@ export function registerTelegramBridgeCommands(
625
601
  },
626
602
  });
627
603
  pi.registerCommand("telegram-disconnect", {
628
- description:
629
- "Stop Telegram; in Threaded Mode, delete this instance's current thread",
604
+ description: "Stop Telegram and delete current thread in Threaded Mode",
630
605
  handler: async (_args, ctx) => {
631
606
  const threadName = deps.getDisconnectThreadName?.();
632
607
  if (threadName) {
@@ -2337,7 +2312,7 @@ async function handleTelegramCommandRuntime<
2337
2312
 
2338
2313
  export const TELEGRAM_INTERNAL_COMMAND_NAME = "telegram-internal";
2339
2314
  export const TELEGRAM_INTERNAL_COMMAND_DESCRIPTION =
2340
- "(internal) dispatch one settled Telegram lifecycle action";
2315
+ "Internal Telegram command cannot be run manually";
2341
2316
  export const TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE =
2342
2317
  "This internal Telegram command cannot be run manually.";
2343
2318
 
@@ -2542,6 +2517,7 @@ export function createTelegramSessionActionAssembly(
2542
2517
 
2543
2518
  type TelegramPendingInternalAction = {
2544
2519
  kind: "replace-session";
2520
+ token: string;
2545
2521
  updateId: number;
2546
2522
  target: { chatId: number; threadId?: number; messageId: number };
2547
2523
  };
@@ -2582,9 +2558,9 @@ export function createTelegramSessionActionRuntime(
2582
2558
  registered = true;
2583
2559
  deps.registerCommand(TELEGRAM_INTERNAL_COMMAND_NAME, {
2584
2560
  description: TELEGRAM_INTERNAL_COMMAND_DESCRIPTION,
2585
- handler: async (_args, ctx) => {
2561
+ handler: async (args, ctx) => {
2586
2562
  const action = pendingAction;
2587
- if (!action) {
2563
+ if (!action || args.trim() !== action.token) {
2588
2564
  ctx.ui.notify(TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE, "warning");
2589
2565
  return;
2590
2566
  }
@@ -2616,10 +2592,11 @@ export function createTelegramSessionActionRuntime(
2616
2592
  const target = pendingTarget;
2617
2593
  pendingTarget = undefined;
2618
2594
  if (!target) return;
2619
- pendingAction = { kind: "replace-session", updateId, target };
2595
+ const token = randomUUID();
2596
+ pendingAction = { kind: "replace-session", token, updateId, target };
2620
2597
  void Promise.resolve()
2621
2598
  .then(() =>
2622
- deps.sendUserMessage(`/${TELEGRAM_INTERNAL_COMMAND_NAME}`, {
2599
+ deps.sendUserMessage(`/${TELEGRAM_INTERNAL_COMMAND_NAME} ${token}`, {
2623
2600
  expandPromptTemplates: true,
2624
2601
  }),
2625
2602
  )
@@ -871,6 +871,7 @@ export default function (pi: Pi.ExtensionAPI) {
871
871
  storeModelMenuState: modelMenuRuntime.storeState,
872
872
  updateStatusMessage: menuActions.updateStatusMessage,
873
873
  updateStatus,
874
+ dismissGuestPlaceholder: guestPlaceholderRuntime.dismiss,
874
875
  });
875
876
  const threadDisplaySettingsRuntime =
876
877
  ThreadDisplay.createTelegramThreadDisplaySettingsRuntime({
@@ -1731,14 +1732,6 @@ export default function (pi: Pi.ExtensionAPI) {
1731
1732
  if (!record?.target.threadId) return undefined;
1732
1733
  return record.threadName ?? "current Telegram thread";
1733
1734
  },
1734
- setRequestedThreadNameForPollingStart:
1735
- telegramThreadCapabilityState.setRequestedThreadName,
1736
- validateThreadName(threadName) {
1737
- return Threads.getTelegramTopicThreadNameValidationError(
1738
- threadName,
1739
- undefined,
1740
- );
1741
- },
1742
1735
  onTransportChanged() {
1743
1736
  deliveryLifecycleRuntime.onSessionStart();
1744
1737
  activityVerbosityRuntime.reset();
@@ -31,6 +31,8 @@ type TelegramQueueMenuReplyMarkup = TelegramInlineKeyboardMarkup;
31
31
  interface TelegramQueueMenuItem {
32
32
  chatId: number;
33
33
  replyToMessageId: number;
34
+ queueOrder: number;
35
+ isGuest: boolean;
34
36
  queuePosition: number;
35
37
  isPriority: boolean;
36
38
  priorityEmoji?: string;
@@ -60,6 +62,8 @@ function toTelegramQueueMenuItems<Context>(
60
62
  return {
61
63
  chatId: item.chatId,
62
64
  replyToMessageId: item.replyToMessageId,
65
+ queueOrder: item.queueOrder,
66
+ isGuest: item.kind === "prompt" && item.guestQueryId !== undefined,
63
67
  queuePosition: index + 1,
64
68
  isPriority: item.queueLane === "priority",
65
69
  priorityEmoji: item.kind === "prompt" ? item.priorityEmoji : undefined,
@@ -106,7 +110,9 @@ function buildTelegramQueueMenuReplyMarkup(
106
110
  return [
107
111
  {
108
112
  text: label,
109
- callback_data: `queue:pick:${item.chatId}:${item.replyToMessageId}`,
113
+ callback_data: item.isGuest
114
+ ? `queue:guest-pick:${item.queueOrder}`
115
+ : `queue:pick:${item.chatId}:${item.replyToMessageId}`,
110
116
  },
111
117
  ];
112
118
  });
@@ -178,6 +184,22 @@ function getTelegramQueueMenuItemText(item: TelegramQueueMenuItem): string {
178
184
  return `${heading}\n${preview}`;
179
185
  }
180
186
 
187
+ function buildTelegramGuestQueueItemSubmenuReplyMarkup(
188
+ queueOrder: number,
189
+ ): TelegramQueueMenuReplyMarkup {
190
+ return {
191
+ inline_keyboard: [
192
+ [{ text: "⬆️ Back", callback_data: "queue:list" }],
193
+ [
194
+ {
195
+ text: "🔴 Skip",
196
+ callback_data: `queue:guest-skip:${queueOrder}`,
197
+ },
198
+ ],
199
+ ],
200
+ };
201
+ }
202
+
181
203
  function buildTelegramQueueItemSubmenuReplyMarkup(
182
204
  chatId: number,
183
205
  replyToMessageId: number,
@@ -217,6 +239,8 @@ interface TelegramQueueMenuCallbackDeps<Context = unknown> {
217
239
  chatId: number,
218
240
  replyToMessageId: number,
219
241
  ) => TelegramQueueMenuItem | undefined;
242
+ findGuestItem: (queueOrder: number) => TelegramQueueMenuItem | undefined;
243
+ skipGuest: (queueOrder: number, ctx: Context) => Promise<boolean>;
220
244
  togglePriority: (chatId: number, replyToMessageId: number) => boolean;
221
245
  setPriority: (
222
246
  chatId: number,
@@ -275,6 +299,38 @@ async function handleTelegramQueueMenuCallback<Context>(
275
299
  );
276
300
  return true;
277
301
  }
302
+ const guestPickMatch = data.match(/^queue:guest-pick:(\d+)$/);
303
+ if (guestPickMatch) {
304
+ await handleTelegramGuestQueueMenuPick(
305
+ callbackQueryId,
306
+ replyChatId,
307
+ replyMessageId,
308
+ Number(guestPickMatch[1]),
309
+ deps,
310
+ );
311
+ return true;
312
+ }
313
+ const guestSkipMatch = data.match(/^queue:guest-skip:(\d+)$/);
314
+ if (guestSkipMatch) {
315
+ const skipped = await deps.skipGuest(Number(guestSkipMatch[1]), ctx);
316
+ if (!skipped) {
317
+ await refreshStaleTelegramQueueMenuItem(
318
+ callbackQueryId,
319
+ replyChatId,
320
+ replyMessageId,
321
+ deps,
322
+ );
323
+ return true;
324
+ }
325
+ await updateTelegramQueueMenuList(
326
+ callbackQueryId,
327
+ replyChatId,
328
+ replyMessageId,
329
+ deps,
330
+ "Guest prompt skipped.",
331
+ );
332
+ return true;
333
+ }
278
334
  const pickMatch = data.match(/^queue:pick:(\d+):(\d+)$/);
279
335
  if (pickMatch) {
280
336
  await handleTelegramQueueMenuPick(
@@ -379,6 +435,31 @@ async function refreshStaleTelegramQueueMenuItem<Context>(
379
435
  );
380
436
  }
381
437
 
438
+ async function handleTelegramGuestQueueMenuPick<Context>(
439
+ callbackQueryId: string,
440
+ replyChatId: number,
441
+ replyMessageId: number,
442
+ queueOrder: number,
443
+ deps: TelegramQueueMenuCallbackDeps<Context>,
444
+ ): Promise<void> {
445
+ const item = deps.findGuestItem(queueOrder);
446
+ if (!item) {
447
+ return refreshStaleTelegramQueueMenuItem(
448
+ callbackQueryId,
449
+ replyChatId,
450
+ replyMessageId,
451
+ deps,
452
+ );
453
+ }
454
+ await deps.updateQueueMessage(
455
+ replyChatId,
456
+ replyMessageId,
457
+ getTelegramQueueMenuItemText(item),
458
+ buildTelegramGuestQueueItemSubmenuReplyMarkup(queueOrder),
459
+ );
460
+ await deps.answerCallbackQuery(callbackQueryId);
461
+ }
462
+
382
463
  async function handleTelegramQueueMenuPick<Context>(
383
464
  callbackQueryId: string,
384
465
  replyChatId: number,
@@ -606,6 +687,7 @@ export function createTelegramQueueMenuRuntime<
606
687
  ctx: Context,
607
688
  ) => Promise<void>;
608
689
  updateStatus: (ctx: Context) => void;
690
+ dismissGuestPlaceholder?: (inlineMessageId: string) => Promise<void>;
609
691
  }): TelegramQueueMenuRuntime<Context> {
610
692
  const sendQueueMenuMessage = createQueueMenuSendMessageAdapter(
611
693
  deps.sendInteractiveMessage,
@@ -628,6 +710,7 @@ export function createTelegramQueueMenuRuntime<
628
710
  updateStatusMessage: deps.updateStatusMessage,
629
711
  answerCallbackQuery: deps.answerCallbackQuery,
630
712
  updateStatus: deps.updateStatus,
713
+ dismissGuestPlaceholder: deps.dismissGuestPlaceholder,
631
714
  }),
632
715
  };
633
716
  }
@@ -695,6 +778,7 @@ function createQueueMenuCallbackHandler<
695
778
  text?: string,
696
779
  ) => Promise<void>;
697
780
  updateStatus: (ctx: Context) => void;
781
+ dismissGuestPlaceholder?: (inlineMessageId: string) => Promise<void>;
698
782
  }) {
699
783
  return async (
700
784
  query: TelegramQueueMenuCallbackQuery,
@@ -737,6 +821,11 @@ function createQueueMenuCallbackHandler<
737
821
  const findItem = (cId: number, rId: number) => {
738
822
  return findTelegramQueueMenuItem(toMenuItems(), cId, rId);
739
823
  };
824
+ const findGuestItem = (queueOrder: number) => {
825
+ return toMenuItems().find((item) => {
826
+ return item.isGuest && item.queueOrder === queueOrder;
827
+ });
828
+ };
740
829
  return handleTelegramQueueMenuCallback(
741
830
  query.id,
742
831
  data,
@@ -746,6 +835,14 @@ function createQueueMenuCallbackHandler<
746
835
  {
747
836
  getQueuedItems: toMenuItems,
748
837
  findItem,
838
+ findGuestItem,
839
+ skipGuest: (queueOrder, callbackContext) => {
840
+ return skipQueuedTelegramGuestPrompt(queueOrder, callbackContext, {
841
+ getQueueSnapshot,
842
+ queueMutationRuntime: deps.queueMutationRuntime,
843
+ dismissGuestPlaceholder: deps.dismissGuestPlaceholder,
844
+ });
845
+ },
749
846
  togglePriority: (cId, rId) => {
750
847
  return toggleQueuedTelegramPromptPriority(cId, rId, ctx, {
751
848
  getQueueSnapshot,
@@ -772,6 +869,34 @@ function createQueueMenuCallbackHandler<
772
869
  };
773
870
  }
774
871
 
872
+ async function skipQueuedTelegramGuestPrompt<Context>(
873
+ queueOrder: number,
874
+ ctx: Context,
875
+ deps: {
876
+ getQueueSnapshot: () => Queue.TelegramQueueItem<Context>[];
877
+ queueMutationRuntime: Queue.TelegramQueueMutationController<Context>;
878
+ dismissGuestPlaceholder?: (inlineMessageId: string) => Promise<void>;
879
+ },
880
+ ): Promise<boolean> {
881
+ const item = deps.getQueueSnapshot().find((candidate) => {
882
+ return candidate.kind === "prompt" &&
883
+ candidate.guestQueryId !== undefined &&
884
+ candidate.queueOrder === queueOrder;
885
+ });
886
+ if (!item || !deps.queueMutationRuntime.removeGuestPromptByQueueOrder) {
887
+ return false;
888
+ }
889
+ const removed = deps.queueMutationRuntime.removeGuestPromptByQueueOrder(
890
+ queueOrder,
891
+ ctx,
892
+ );
893
+ if (!removed) return false;
894
+ if (item.guestInlineMessageId && deps.dismissGuestPlaceholder) {
895
+ await deps.dismissGuestPlaceholder(item.guestInlineMessageId);
896
+ }
897
+ return true;
898
+ }
899
+
775
900
  function getQueueMenuReactionDisposition<Context>(
776
901
  item: Queue.TelegramQueueItem<Context>,
777
902
  priority: boolean,
@@ -922,6 +922,28 @@ export function removeTelegramQueueItemsByMessageIds<TContext = unknown>(
922
922
  };
923
923
  }
924
924
 
925
+ export function removeTelegramQueuedGuestPromptByOrder<TContext = unknown>(
926
+ items: TelegramQueueItem<TContext>[],
927
+ queueOrder: number,
928
+ ): {
929
+ items: TelegramQueueItem<TContext>[];
930
+ removedItems: PendingTelegramTurn[];
931
+ removedCount: number;
932
+ } {
933
+ const index = items.findIndex((item) => {
934
+ return isPendingTelegramTurn(item) &&
935
+ item.guestQueryId !== undefined &&
936
+ item.queueOrder === queueOrder;
937
+ });
938
+ if (index < 0) return { items, removedItems: [], removedCount: 0 };
939
+ const removedItem = items[index] as PendingTelegramTurn;
940
+ return {
941
+ items: [...items.slice(0, index), ...items.slice(index + 1)],
942
+ removedItems: [removedItem],
943
+ removedCount: 1,
944
+ };
945
+ }
946
+
925
947
  export function applyTelegramQueuePromptReactionDisposition<
926
948
  TContext = unknown,
927
949
  >(
@@ -2331,6 +2353,10 @@ export interface TelegramQueueMutationController<TContext> {
2331
2353
  ctx: TContext,
2332
2354
  scope?: TelegramQueueMessageScope,
2333
2355
  ) => number;
2356
+ removeGuestPromptByQueueOrder?: (
2357
+ queueOrder: number,
2358
+ ctx: TContext,
2359
+ ) => boolean;
2334
2360
  applyReactionByMessageId: (
2335
2361
  messageId: number,
2336
2362
  disposition: TelegramQueueReactionDisposition,
@@ -2604,6 +2630,11 @@ export function createTelegramQueueMutationController<TContext>(
2604
2630
  buildRuntimeDeps(ctx),
2605
2631
  scope,
2606
2632
  ),
2633
+ removeGuestPromptByQueueOrder: (queueOrder, ctx) =>
2634
+ removeTelegramQueuedGuestPromptByOrderRuntime(
2635
+ queueOrder,
2636
+ buildRuntimeDeps(ctx),
2637
+ ),
2607
2638
  applyReactionByMessageId: (messageId, disposition, ctx, scope) =>
2608
2639
  applyTelegramQueuePromptReactionDispositionRuntime(
2609
2640
  messageId,
@@ -2684,6 +2715,21 @@ export function removeTelegramQueueItemsByMessageIdsRuntime<TContext>(
2684
2715
  return removedCount;
2685
2716
  }
2686
2717
 
2718
+ export function removeTelegramQueuedGuestPromptByOrderRuntime<TContext>(
2719
+ queueOrder: number,
2720
+ deps: TelegramQueueMutationRuntimeDeps<TContext>,
2721
+ ): boolean {
2722
+ const { items, removedItems, removedCount } =
2723
+ removeTelegramQueuedGuestPromptByOrder(deps.getQueuedItems(), queueOrder);
2724
+ if (removedCount === 0) return false;
2725
+ // A menu Skip means permanent discard, so settle durable admission before
2726
+ // removing the live item. A failed settlement keeps the prompt retryable.
2727
+ deps.onItemsDiscarded?.(removedItems, deps.ctx);
2728
+ deps.setQueuedItems(items);
2729
+ updateTelegramQueueStatusRuntime(deps);
2730
+ return true;
2731
+ }
2732
+
2687
2733
  export function applyTelegramQueuePromptReactionDispositionRuntime<TContext>(
2688
2734
  messageId: number,
2689
2735
  disposition: TelegramQueueReactionDisposition,
@@ -1018,6 +1018,10 @@ export const TELEGRAM_GUEST_PLACEHOLDER_FRAMES = [
1018
1018
  "<b>🌏 Working on it...</b>",
1019
1019
  ] as const;
1020
1020
 
1021
+ // Telegram does not expose deletion for inline messages. This non-printing,
1022
+ // non-empty edit visually clears a guest placeholder after its query is skipped.
1023
+ export const TELEGRAM_DISMISSED_GUEST_PLACEHOLDER_TEXT = "\u2063";
1024
+
1021
1025
  export function buildTelegramGuestPlaceholderFrame(step: number): string {
1022
1026
  const frames: readonly string[] = TELEGRAM_GUEST_PLACEHOLDER_FRAMES;
1023
1027
  const index = ((step % frames.length) + frames.length) % frames.length;
@@ -1052,6 +1056,8 @@ export interface TelegramGuestPlaceholderRuntime {
1052
1056
  stop: (inlineMessageId: string) => Promise<void>;
1053
1057
  /** Cancels every loop without waiting for in-flight edits (session shutdown). */
1054
1058
  stopAll: () => void;
1059
+ /** Stops rotation and visually clears the inline placeholder. */
1060
+ dismiss: (inlineMessageId: string) => Promise<void>;
1055
1061
  }
1056
1062
 
1057
1063
  interface TelegramGuestPlaceholderSession {
@@ -1202,5 +1208,18 @@ export function createTelegramGuestPlaceholderRuntime(
1202
1208
  }
1203
1209
  sessions.clear();
1204
1210
  },
1211
+ async dismiss(inlineMessageId) {
1212
+ await stop(inlineMessageId);
1213
+ try {
1214
+ await deps.editGuestInlineMessage(inlineMessageId, {
1215
+ text: TELEGRAM_DISMISSED_GUEST_PLACEHOLDER_TEXT,
1216
+ parseMode: "HTML",
1217
+ });
1218
+ } catch (error) {
1219
+ deps.recordRuntimeEvent?.("guest", error, {
1220
+ phase: "guest-placeholder-dismiss",
1221
+ });
1222
+ }
1223
+ },
1205
1224
  };
1206
1225
  }
@@ -684,7 +684,14 @@ export async function ensureTelegramLeaderThreadBinding(
684
684
  assertLeaderEpoch("before-workspace-persist");
685
685
  await deps.topicTargetStore.persist();
686
686
  assertLeaderEpoch("after-workspace-persist");
687
- return { ...result, ...(committed.displayTitle ? { displayTitle: committed.displayTitle } : {}) };
687
+ const { target: _target, slot: _slot, threadName: _threadName, displayTitle: _displayTitle, ...rest } = result;
688
+ return {
689
+ ...rest,
690
+ target: { ...committed.target },
691
+ slot: committed.slot ?? result.slot,
692
+ ...(committed.threadName ? { threadName: committed.threadName } : {}),
693
+ ...(committed.displayTitle ? { displayTitle: committed.displayTitle } : {}),
694
+ };
688
695
  };
689
696
  try {
690
697
  const unavailableTargetKeys = new Set([
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.50.0",
3
+ "version": "0.50.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-kit",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -44,8 +44,8 @@
44
44
  "@llblab/pi-clean-room": "0.1.1",
45
45
  "@llblab/pi-codex-usage": "0.10.0",
46
46
  "@llblab/pi-grow-loop": "0.8.1",
47
- "@llblab/pi-state-flow": "0.16.2",
48
- "@llblab/pi-telegram": "0.50.0",
47
+ "@llblab/pi-state-flow": "0.16.3",
48
+ "@llblab/pi-telegram": "0.50.1",
49
49
  "@llblab/skills": "1.15.0"
50
50
  },
51
51
  "bundledDependencies": [