@llblab/pi-kit 0.1.9 → 0.1.11

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 (35) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +2 -2
  3. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +1 -1
  4. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +6 -0
  5. package/node_modules/@llblab/pi-grow-loop/README.md +2 -1
  6. package/node_modules/@llblab/pi-grow-loop/index.ts +11 -2
  7. package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
  8. package/node_modules/@llblab/pi-telegram/AGENTS.md +4 -4
  9. package/node_modules/@llblab/pi-telegram/BACKLOG.md +0 -1
  10. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +29 -0
  11. package/node_modules/@llblab/pi-telegram/README.md +1 -1
  12. package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -5
  13. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +22 -8
  14. package/node_modules/@llblab/pi-telegram/index.ts +18 -7
  15. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +49 -13
  16. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +15 -0
  17. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +17 -4
  18. package/node_modules/@llblab/pi-telegram/lib/bus.ts +56 -2
  19. package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +65 -4
  20. package/node_modules/@llblab/pi-telegram/lib/commands.ts +122 -28
  21. package/node_modules/@llblab/pi-telegram/lib/journal.ts +2 -9
  22. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +20 -18
  23. package/node_modules/@llblab/pi-telegram/lib/locks.ts +6 -1
  24. package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +15 -4
  25. package/node_modules/@llblab/pi-telegram/lib/menu.ts +10 -7
  26. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +2 -2
  27. package/node_modules/@llblab/pi-telegram/lib/queue.ts +49 -12
  28. package/node_modules/@llblab/pi-telegram/lib/routing.ts +5 -2
  29. package/node_modules/@llblab/pi-telegram/lib/status.ts +18 -6
  30. package/node_modules/@llblab/pi-telegram/lib/sync.ts +17 -0
  31. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +91 -45
  32. package/node_modules/@llblab/pi-telegram/lib/threads.ts +5 -0
  33. package/node_modules/@llblab/pi-telegram/lib/updates.ts +17 -9
  34. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  35. package/package.json +3 -3
@@ -255,7 +255,10 @@ export interface TelegramMenuActionRuntimeDeps<
255
255
  chatId: number,
256
256
  replyToMessageId: number,
257
257
  text: string,
258
- options?: { target?: { chatId: number; threadId?: number } },
258
+ options?: {
259
+ target?: { chatId: number; threadId?: number };
260
+ parseMode?: "HTML";
261
+ },
259
262
  ) => Promise<unknown>;
260
263
  sectionRegistry?: TelegramSectionRegistry;
261
264
  isVoiceReplyActive?: () => boolean;
@@ -798,8 +801,8 @@ export function createTelegramMenuActionRuntime<
798
801
  await deps.sendTextReply(
799
802
  chatId,
800
803
  replyToMessageId,
801
- "Cannot open status while Pi is busy. Send /abort, /next, or /stop.",
802
- { target: { chatId, threadId } },
804
+ "<b>⏳ Cannot open status while Pi is busy. Send /abort, /next, or /stop.</b>",
805
+ { target: { chatId, threadId }, parseMode: "HTML" },
803
806
  );
804
807
  },
805
808
  getModelMenuState: () => deps.getModelMenuState(chatId, ctx, threadId),
@@ -835,16 +838,16 @@ export function createTelegramMenuActionRuntime<
835
838
  await deps.sendTextReply(
836
839
  chatId,
837
840
  replyToMessageId,
838
- "Cannot switch model while Pi is busy. Send /abort, /next, or /stop.",
839
- { target: { chatId, threadId } },
841
+ "<b>⏳ Cannot switch model while Pi is busy. Send /abort, /next, or /stop.</b>",
842
+ { target: { chatId, threadId }, parseMode: "HTML" },
840
843
  );
841
844
  },
842
845
  sendNoModelsMessage: async () => {
843
846
  await deps.sendTextReply(
844
847
  chatId,
845
848
  replyToMessageId,
846
- "No available models with configured auth.",
847
- { target: { chatId, threadId } },
849
+ "<b>🚫 No available models with configured auth.</b>",
850
+ { target: { chatId, threadId }, parseMode: "HTML" },
848
851
  );
849
852
  },
850
853
  getModelMenuState: () => deps.getModelMenuState(chatId, ctx, threadId),
@@ -14,11 +14,11 @@ export const TELEGRAM_DISCONNECTED_CONTEXT_MESSAGE =
14
14
 
15
15
  const LOCAL_SYSTEM_PROMPT_SUFFIX = `
16
16
 
17
- ${TELEGRAM_CONNECTED_CONTEXT_MESSAGE} Load the \`telegram-bridge\` Skill for Telegram-originated turns or explicit requests involving Telegram delivery, actions, Threaded Mode, or diagnosis. Do not use Telegram-specific features from unrelated local/TUI prompts.`;
17
+ ${TELEGRAM_CONNECTED_CONTEXT_MESSAGE} For Telegram work, consult bundled Skills in routing order: \`telegram-bridge\` for the transport and turn protocol, \`generated-control-surface\` when contextual controls materially shorten feedback, then \`generative-apps\` when the interaction warrants a reusable deterministic app. Load a Skill only if its instructions are not already present in the current context. Do not use Telegram-specific features from unrelated local/TUI prompts.`;
18
18
 
19
19
  const TELEGRAM_TURN_SYSTEM_PROMPT_SUFFIX = `
20
20
 
21
- Telegram turn note: Load and follow the \`telegram-bridge\` Skill.`;
21
+ Telegram turn note: Follow the applicable bundled Telegram Skills in routing order; load only missing instructions.`;
22
22
 
23
23
  export const TELEGRAM_ATTACH_PROMPT_SNIPPET =
24
24
  "Queue files for the active Telegram reply; outside Telegram turns, send files directly to Telegram.";
@@ -427,12 +427,22 @@ export function createTelegramTransportStampedQueueStore<TContext>(
427
427
  };
428
428
  }
429
429
 
430
+ export function isTelegramQueueItemSkipped<TContext = unknown>(
431
+ item: TelegramQueueItem<TContext>,
432
+ ): boolean {
433
+ return item.kind === "prompt" && Boolean(item.reactionSuppressionEmoji);
434
+ }
435
+
436
+ export function countExecutableTelegramQueueItems<TContext = unknown>(
437
+ items: readonly TelegramQueueItem<TContext>[],
438
+ ): number {
439
+ return items.filter((item) => !isTelegramQueueItemSkipped(item)).length;
440
+ }
441
+
430
442
  export function createTelegramQueueItemCountGetter<TContext = unknown>(
431
443
  store: Pick<TelegramQueueStore<TContext>, "getQueuedItems">,
432
444
  ): () => number {
433
- return () => {
434
- return store.getQueuedItems().length;
435
- };
445
+ return () => countExecutableTelegramQueueItems(store.getQueuedItems());
436
446
  }
437
447
 
438
448
  export function createTelegramActiveTurnStore<
@@ -993,7 +1003,8 @@ export function consumeDispatchedTelegramPrompt<TContext = unknown>(
993
1003
  export function formatQueuedTelegramItemsStatus<TContext = unknown>(
994
1004
  items: TelegramQueueItem<TContext>[],
995
1005
  ): string {
996
- return items.length === 0 ? "" : ` +${items.length}`;
1006
+ const count = countExecutableTelegramQueueItems(items);
1007
+ return count === 0 ? "" : ` +${count}`;
997
1008
  }
998
1009
 
999
1010
  export function truncateTelegramQueueSummary(
@@ -2062,6 +2073,7 @@ export interface TelegramSessionStartRuntimeDeps<TContext, TModel = unknown> {
2062
2073
  export interface TelegramSessionShutdownRuntimeDeps<TQueueItem> {
2063
2074
  isSessionActive?: () => boolean;
2064
2075
  unbindDeferredDispatchContext?: () => void;
2076
+ discardQueuedItems?: () => void;
2065
2077
  applyState: (state: TelegramSessionShutdownState<TQueueItem>) => void;
2066
2078
  clearPendingMediaGroups: () => void;
2067
2079
  clearModelMenuState: () => void;
@@ -2090,6 +2102,7 @@ export interface TelegramSessionLifecycleHookRuntimeDeps<
2090
2102
  updateStatus: (ctx: TContext) => void;
2091
2103
  isSessionActive?: (ctx: TContext) => boolean;
2092
2104
  unbindDeferredDispatchContext?: () => void;
2105
+ discardQueuedItems?: (ctx: TContext) => void;
2093
2106
  applySessionShutdownState: (
2094
2107
  state: TelegramSessionShutdownState<TQueueItem>,
2095
2108
  ) => void;
@@ -2286,6 +2299,7 @@ export async function shutdownTelegramSessionRuntime<TQueueItem>(
2286
2299
  deps.unbindDeferredDispatchContext?.();
2287
2300
  await deps.stopPolling();
2288
2301
  if (deps.isSessionActive?.() === false) return;
2302
+ deps.discardQueuedItems?.();
2289
2303
  deps.applyState(buildTelegramSessionShutdownState<TQueueItem>());
2290
2304
  deps.clearPendingMediaGroups();
2291
2305
  deps.clearModelMenuState();
@@ -2339,6 +2353,7 @@ export function createTelegramSessionLifecycleRuntime<
2339
2353
  updateStatus: deps.updateStatus,
2340
2354
  isSessionActive: deps.isSessionActive,
2341
2355
  unbindDeferredDispatchContext: deps.unbindDeferredDispatchContext,
2356
+ discardQueuedItems: deps.discardQueuedItems,
2342
2357
  applySessionShutdownState: stateApplier.applyShutdownState,
2343
2358
  clearPendingMediaGroups: deps.clearPendingMediaGroups,
2344
2359
  clearModelMenuState: deps.clearModelMenuState,
@@ -2387,6 +2402,10 @@ export function createTelegramSessionLifecycleHooks<
2387
2402
  isSessionActive: () =>
2388
2403
  ctx === undefined ? true : (deps.isSessionActive?.(ctx) ?? true),
2389
2404
  unbindDeferredDispatchContext: deps.unbindDeferredDispatchContext,
2405
+ discardQueuedItems:
2406
+ ctx === undefined || !deps.discardQueuedItems
2407
+ ? undefined
2408
+ : () => deps.discardQueuedItems!(ctx),
2390
2409
  applyState: deps.applySessionShutdownState,
2391
2410
  clearPendingMediaGroups: deps.clearPendingMediaGroups,
2392
2411
  clearModelMenuState: deps.clearModelMenuState,
@@ -2476,14 +2495,8 @@ export function clearTelegramQueueItemsRuntime<TContext>(
2476
2495
  const removedItems = deps.getQueuedItems();
2477
2496
  const removedCount = removedItems.length;
2478
2497
  if (removedCount === 0) return 0;
2498
+ deps.onItemsDiscarded?.(removedItems, deps.ctx);
2479
2499
  deps.setQueuedItems([]);
2480
- try {
2481
- deps.onItemsDiscarded?.(removedItems, deps.ctx);
2482
- } catch (error) {
2483
- deps.recordRuntimeEvent?.("queue", error, {
2484
- phase: "discard-receipt-settlement",
2485
- });
2486
- }
2487
2500
  updateTelegramQueueStatusRuntime(deps);
2488
2501
  return removedCount;
2489
2502
  }
@@ -2877,6 +2890,7 @@ export interface TelegramQueueDispatchControllerDeps<
2877
2890
  item: PendingTelegramControlItem<TContext>,
2878
2891
  ctx: TContext,
2879
2892
  ) => void;
2893
+ onPromptSkipped?: (item: PendingTelegramTurn, ctx: TContext) => boolean;
2880
2894
  }
2881
2895
 
2882
2896
  export interface TelegramQueueDispatchController<TContext = unknown> {
@@ -2940,6 +2954,7 @@ export function createTelegramQueueDispatchRuntime<TContext = unknown>(
2940
2954
  deps.hasPendingInboundQueueMutationForItem,
2941
2955
  isQueueItemAdmissionReady: deps.isQueueItemAdmissionReady,
2942
2956
  onControlSettled: deps.onControlSettled,
2957
+ onPromptSkipped: deps.onPromptSkipped,
2943
2958
  recordRuntimeEvent: deps.recordRuntimeEvent,
2944
2959
  });
2945
2960
  }
@@ -3007,7 +3022,29 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3007
3022
  deps.updateStatus(ctx);
3008
3023
  return;
3009
3024
  }
3025
+ try {
3026
+ if (deps.onPromptSkipped && !deps.onPromptSkipped(candidate, ctx)) {
3027
+ deps.updateStatus(
3028
+ ctx,
3029
+ "Telegram skipped prompt could not be settled durably.",
3030
+ );
3031
+ return;
3032
+ }
3033
+ } catch (error) {
3034
+ deps.recordRuntimeEvent?.("dispatch", error, {
3035
+ phase: "skip-receipt-settlement",
3036
+ });
3037
+ deps.updateStatus(
3038
+ ctx,
3039
+ "Telegram skipped prompt could not be settled durably.",
3040
+ );
3041
+ return;
3042
+ }
3010
3043
  nextActiveIndex += 1;
3044
+ deps.setQueuedItems([
3045
+ ...activeItems.slice(nextActiveIndex),
3046
+ ...protectedInactiveItems,
3047
+ ]);
3011
3048
  }
3012
3049
  }
3013
3050
  const dispatchableItems = activeItems.slice(nextActiveIndex);
@@ -3031,7 +3068,7 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3031
3068
  dispatchableItems,
3032
3069
  canDispatch,
3033
3070
  );
3034
- if (nextActiveIndex > 0 || dispatchPlan.kind !== "none") {
3071
+ if (dispatchPlan.kind !== "none") {
3035
3072
  deps.setQueuedItems([
3036
3073
  ...dispatchPlan.remainingItems,
3037
3074
  ...protectedInactiveItems,
@@ -1677,9 +1677,12 @@ export function createTelegramInboundRouteRuntime<
1677
1677
  })
1678
1678
  : undefined,
1679
1679
  stopTypingLoop: deps.stopTypingLoop,
1680
- sendTextReply: (text) =>
1680
+ sendTextReply: (text, options) =>
1681
1681
  deps
1682
- .sendTextReply(chatId, replyToMessageId, text, { target })
1682
+ .sendTextReply(chatId, replyToMessageId, text, {
1683
+ target,
1684
+ parseMode: options?.parseMode,
1685
+ })
1683
1686
  .then(() => {}),
1684
1687
  suppressStartNotice: true,
1685
1688
  recordRuntimeEvent: deps.recordRuntimeEvent,
@@ -344,6 +344,7 @@ export interface TelegramBridgeStatusRuntimeDeps<
344
344
  getActiveToolExecutions: () => number;
345
345
  hasPendingModelSwitch: () => boolean;
346
346
  getQueuedItems: () => TQueueItem[];
347
+ getQueuedItemCount?: (items: TQueueItem[]) => number;
347
348
  formatQueuedStatus: (items: TQueueItem[]) => string;
348
349
  getRecentRuntimeEvents: () => TelegramRuntimeEvent[];
349
350
  getRuntimeLockState?: () => string;
@@ -627,18 +628,28 @@ export function createTelegramStatusHtmlBuilder<TContext>(deps: {
627
628
  );
628
629
  }
629
630
 
631
+ function resolveStatusBarTheme(
632
+ ctx: TelegramStatusRuntimeContext,
633
+ ): TelegramStatusBarTheme | undefined {
634
+ try {
635
+ const theme = ctx.ui?.theme;
636
+ return typeof theme?.fg === "function" ? theme : undefined;
637
+ } catch {
638
+ return undefined;
639
+ }
640
+ }
641
+
630
642
  export function createTelegramStatusRuntime<
631
643
  TContext extends TelegramStatusRuntimeContext,
632
644
  >(deps: TelegramStatusRuntimeDeps<TContext>): TelegramStatusRuntime<TContext> {
633
645
  const statusKey = deps.statusKey ?? "telegram";
634
646
  return {
635
647
  updateStatus: (ctx, error) => {
648
+ const theme = resolveStatusBarTheme(ctx);
649
+ if (!theme) return;
636
650
  ctx.ui.setStatus(
637
651
  statusKey,
638
- buildTelegramStatusBarText(
639
- ctx.ui.theme,
640
- deps.getStatusBarState(ctx, error),
641
- ),
652
+ buildTelegramStatusBarText(theme, deps.getStatusBarState(ctx, error)),
642
653
  );
643
654
  },
644
655
  getStatusLines: (options) =>
@@ -658,6 +669,7 @@ export function createTelegramBridgeStatusRuntime<
658
669
  getStatusBarState: (_ctx, error) => {
659
670
  const config = deps.getConfig();
660
671
  const queuedItems = deps.getQueuedItems();
672
+ const queuedItemCount = deps.getQueuedItemCount?.(queuedItems) ?? queuedItems.length;
661
673
  const hasActiveTurn = deps.hasActiveTurn();
662
674
  const hasPendingDispatch = deps.hasDispatchPending();
663
675
  const hasPendingModelSwitch = deps.hasPendingModelSwitch();
@@ -677,13 +689,13 @@ export function createTelegramBridgeStatusRuntime<
677
689
  hasPendingDispatch ||
678
690
  hasPendingModelSwitch ||
679
691
  activeToolExecutions > 0 ||
680
- queuedItems.length > 0,
692
+ queuedItemCount > 0,
681
693
  processingStatus: getTelegramStatusBarProcessingStatus({
682
694
  hasActiveTurn,
683
695
  hasPendingDispatch,
684
696
  hasPendingModelSwitch,
685
697
  activeToolExecutions,
686
- queuedItems: queuedItems.length,
698
+ queuedItems: queuedItemCount,
687
699
  }),
688
700
  queuedStatus: deps.formatQueuedStatus(queuedItems),
689
701
  error,
@@ -4,6 +4,7 @@
4
4
  * Owns pure contracts for deciding when local Telegram mirror state should be refreshed without querying Telegram on every action
5
5
  */
6
6
 
7
+ import { getTelegramApiErrorRequestTarget } from "./telegram-api.ts";
7
8
  import { getTelegramTargetKey, type TelegramTarget } from "./target.ts";
8
9
  import * as ThreadReconciler from "./thread-reconciler.ts";
9
10
  import {
@@ -423,6 +424,22 @@ export function createTelegramStaleTopicApiErrorRecoveryRuntime<
423
424
  recoverStaleTelegramTopicApiError(apiBody, error, deps);
424
425
  }
425
426
 
427
+ export async function settleStaleTelegramTopicExecutionFailure<
428
+ TSyncState extends TelegramSyncState,
429
+ >(
430
+ error: unknown,
431
+ deps: TelegramStaleTopicApiErrorRecoveryDeps<TSyncState>,
432
+ ): Promise<boolean> {
433
+ const target = getTelegramApiErrorRequestTarget(error);
434
+ if (!target || !isTelegramTopicTargetStaleError(error)) return false;
435
+ await recoverStaleTelegramTopicApiError(
436
+ { chat_id: target.chatId, message_thread_id: target.threadId },
437
+ error,
438
+ deps,
439
+ );
440
+ return true;
441
+ }
442
+
426
443
  export async function recoverStaleTelegramTopicApiError<
427
444
  TSyncState extends TelegramSyncState,
428
445
  >(
@@ -587,6 +587,7 @@ export function isTelegramApiCommitUnknownError(
587
587
  class TelegramApiHttpError extends Error {
588
588
  readonly status: number | undefined;
589
589
  readonly retryAfterSeconds: number | undefined;
590
+ requestTarget?: { chatId: number; threadId: number };
590
591
  constructor(
591
592
  message: string,
592
593
  status: number | undefined,
@@ -598,6 +599,41 @@ class TelegramApiHttpError extends Error {
598
599
  }
599
600
  }
600
601
 
602
+ function attachTelegramApiRequestTarget(
603
+ error: unknown,
604
+ body: Record<string, unknown> | Record<string, string>,
605
+ ): void {
606
+ if (!(error instanceof TelegramApiHttpError)) return;
607
+ const chatId = Number(body.chat_id);
608
+ const threadId = Number(body.message_thread_id);
609
+ if (!Number.isSafeInteger(chatId) || !Number.isSafeInteger(threadId)) return;
610
+ error.requestTarget = { chatId, threadId };
611
+ }
612
+
613
+ export class TelegramApiStaleTargetError extends Error {
614
+ readonly requestTarget: { chatId: number; threadId: number };
615
+
616
+ constructor(
617
+ message: string,
618
+ requestTarget: { chatId: number; threadId: number },
619
+ ) {
620
+ super(message);
621
+ this.name = "TelegramApiStaleTargetError";
622
+ this.requestTarget = { ...requestTarget };
623
+ }
624
+ }
625
+
626
+ export function getTelegramApiErrorRequestTarget(
627
+ error: unknown,
628
+ ): { chatId: number; threadId: number } | undefined {
629
+ const target =
630
+ error instanceof TelegramApiHttpError ||
631
+ error instanceof TelegramApiStaleTargetError
632
+ ? error.requestTarget
633
+ : undefined;
634
+ return target ? { ...target } : undefined;
635
+ }
636
+
601
637
  export function isTelegramMessageNotModifiedError(error: unknown): boolean {
602
638
  return (
603
639
  error instanceof Error && error.message.includes("message is not modified")
@@ -1156,21 +1192,26 @@ export async function callTelegram<TResponse>(
1156
1192
  options?: TelegramApiCallOptions,
1157
1193
  ): Promise<TResponse> {
1158
1194
  const configuredBotToken = assertTelegramBotTokenConfigured(botToken);
1159
- return callTelegramWithRetry(
1160
- method,
1161
- async (family) =>
1162
- telegramFetch(
1163
- `${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
1164
- {
1165
- method: "POST",
1166
- headers: { "content-type": "application/json" },
1167
- body: JSON.stringify(body),
1168
- signal: options?.signal,
1169
- },
1170
- family,
1171
- ),
1172
- options,
1173
- );
1195
+ try {
1196
+ return await callTelegramWithRetry(
1197
+ method,
1198
+ async (family) =>
1199
+ telegramFetch(
1200
+ `${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
1201
+ {
1202
+ method: "POST",
1203
+ headers: { "content-type": "application/json" },
1204
+ body: JSON.stringify(body),
1205
+ signal: options?.signal,
1206
+ },
1207
+ family,
1208
+ ),
1209
+ options,
1210
+ );
1211
+ } catch (error) {
1212
+ attachTelegramApiRequestTarget(error, body);
1213
+ throw error;
1214
+ }
1174
1215
  }
1175
1216
 
1176
1217
  export type TelegramBotIdentityResponse = Pick<
@@ -1206,43 +1247,48 @@ export async function callTelegramMultipart<TResponse>(
1206
1247
  ): Promise<TResponse> {
1207
1248
  const configuredBotToken = assertTelegramBotTokenConfigured(botToken);
1208
1249
  const fileBlob = await openAsBlob(filePath);
1209
- return callTelegramWithRetry(
1210
- method,
1211
- async (family) => {
1212
- if (family) {
1213
- const multipart = await buildTelegramMultipartBody(
1214
- fields,
1215
- fileField,
1216
- fileBlob,
1217
- fileName,
1218
- );
1250
+ try {
1251
+ return await callTelegramWithRetry(
1252
+ method,
1253
+ async (family) => {
1254
+ if (family) {
1255
+ const multipart = await buildTelegramMultipartBody(
1256
+ fields,
1257
+ fileField,
1258
+ fileBlob,
1259
+ fileName,
1260
+ );
1261
+ return telegramFetch(
1262
+ `${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
1263
+ {
1264
+ method: "POST",
1265
+ headers: { "content-type": multipart.contentType },
1266
+ body: multipart.body as unknown as BodyInit,
1267
+ signal: options?.signal,
1268
+ },
1269
+ family,
1270
+ );
1271
+ }
1272
+ const form = new FormData();
1273
+ for (const [key, value] of Object.entries(fields)) {
1274
+ form.set(key, value);
1275
+ }
1276
+ form.set(fileField, fileBlob, fileName);
1219
1277
  return telegramFetch(
1220
1278
  `${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
1221
1279
  {
1222
1280
  method: "POST",
1223
- headers: { "content-type": multipart.contentType },
1224
- body: multipart.body as unknown as BodyInit,
1281
+ body: form,
1225
1282
  signal: options?.signal,
1226
1283
  },
1227
- family,
1228
1284
  );
1229
- }
1230
- const form = new FormData();
1231
- for (const [key, value] of Object.entries(fields)) {
1232
- form.set(key, value);
1233
- }
1234
- form.set(fileField, fileBlob, fileName);
1235
- return telegramFetch(
1236
- `${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
1237
- {
1238
- method: "POST",
1239
- body: form,
1240
- signal: options?.signal,
1241
- },
1242
- );
1243
- },
1244
- options,
1245
- );
1285
+ },
1286
+ options,
1287
+ );
1288
+ } catch (error) {
1289
+ attachTelegramApiRequestTarget(error, fields);
1290
+ throw error;
1291
+ }
1246
1292
  }
1247
1293
 
1248
1294
  export async function downloadTelegramFile(
@@ -2882,6 +2882,11 @@ export function getTelegramTargetFromApiBody(
2882
2882
 
2883
2883
  export function isTelegramTopicTargetStaleError(error: unknown): boolean {
2884
2884
  if (!(error instanceof Error)) return false;
2885
+ const status =
2886
+ "status" in error && typeof error.status === "number"
2887
+ ? error.status
2888
+ : undefined;
2889
+ if (status !== undefined && status !== 400) return false;
2885
2890
  const message = error.message.toLowerCase();
2886
2891
  return (
2887
2892
  message.includes("topic_id_invalid") ||
@@ -1782,6 +1782,7 @@ export interface TelegramUpdateWorkerRuntimeDeps<TContext> {
1782
1782
  classifyExecutionFailure?: (
1783
1783
  error: unknown,
1784
1784
  ) => TelegramUpdateExecutionFailureClassification;
1785
+ settleTerminalExecutionFailure?: (error: unknown) => Promise<boolean>;
1785
1786
  scheduleRetry?: (callback: () => void, delayMs: number) => unknown;
1786
1787
  cancelRetry?: (handle: unknown) => void;
1787
1788
  batchSize?: number;
@@ -4583,11 +4584,11 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
4583
4584
  try {
4584
4585
  deps.recordRuntimeEvent?.(
4585
4586
  "inbound-worker",
4586
- "Recovered queued authority from a confirmed-dead process.",
4587
+ "Discarded session-owned queue authority from a confirmed-dead process.",
4587
4588
  {
4588
- phase: "dead-queue-owner-recovery",
4589
+ phase: "dead-queue-owner-cleanup",
4589
4590
  receiptId,
4590
- recoveredUpdateCount: result.recoveredUpdateIds.length,
4591
+ removedUpdateCount: result.recoveredUpdateIds.length,
4591
4592
  },
4592
4593
  );
4593
4594
  } catch {
@@ -4833,13 +4834,20 @@ export function createTelegramUpdateAdmissionWorkerRuntime<
4833
4834
  });
4834
4835
  worker = createTelegramUpdateWorkerRuntime({
4835
4836
  ...deps,
4836
- executeUpdate: (update, ctx, signal) => {
4837
+ async executeUpdate(update, ctx, signal) {
4837
4838
  const typedUpdate = update as TUpdate;
4838
- return executeUpdate(
4839
- deps.prepareUpdateForExecution?.(typedUpdate) ?? typedUpdate,
4840
- ctx,
4841
- signal,
4842
- );
4839
+ try {
4840
+ return await executeUpdate(
4841
+ deps.prepareUpdateForExecution?.(typedUpdate) ?? typedUpdate,
4842
+ ctx,
4843
+ signal,
4844
+ );
4845
+ } catch (error) {
4846
+ if (await deps.settleTerminalExecutionFailure?.(error)) {
4847
+ return { kind: "complete" };
4848
+ }
4849
+ throw error;
4850
+ }
4843
4851
  },
4844
4852
  });
4845
4853
  return worker;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.38.0",
3
+ "version": "0.39.4",
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.1.9",
3
+ "version": "0.1.11",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -42,8 +42,8 @@
42
42
  "dependencies": {
43
43
  "@llblab/pi-actors": "0.50.0",
44
44
  "@llblab/pi-codex-usage": "0.9.3",
45
- "@llblab/pi-grow-loop": "0.7.2",
46
- "@llblab/pi-telegram": "0.38.0"
45
+ "@llblab/pi-grow-loop": "0.7.3",
46
+ "@llblab/pi-telegram": "0.39.4"
47
47
  },
48
48
  "bundledDependencies": [
49
49
  "@llblab/pi-actors",