@llblab/pi-telegram 0.8.1 → 0.8.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.2: Lock-Safe Delivery
4
+
5
+ - `[Lock Safety]` Active Telegram turns now re-check singleton ownership before preview flushes and final agent-end delivery. Impact: an old π instance stays silent after another instance takes the Telegram bridge lock, even if the old instance finishes a long-running prompt later.
6
+ - `[Inbound Handlers]` The first step of an inbound composition now receives the full configured handler timeout before elapsed-time accounting starts on later steps. Impact: composition timeout behavior is deterministic and avoids one-millisecond test/runtime drift at pipeline start.
7
+ - `[Menu UI]` Model and Thinking submenu headers now include their matching command icons (`🤖` and `🧠`). Impact: submenu headings match the Queue menu's icon-led style.
8
+ - `[Package]` Bumped package metadata to `0.8.2` and kept the lockfile in sync.
9
+
3
10
  ## 0.8.1: Outbound Voice Translation Hotfix
4
11
 
5
12
  - `[Outbound Voice]` Composed voice handlers now pass the original `telegram_voice` text to the first pipeline step through stdin, then continue piping each step's stdout into the next step. Impact: translate-from-stdin voice pipelines can translate hidden voice text before TTS instead of failing with an empty first-step input.
package/index.ts CHANGED
@@ -49,6 +49,8 @@ export default function (pi: Pi.ExtensionAPI) {
49
49
  const { abort, lifecycle, queue, setup, typing } = bridgeRuntime;
50
50
  const configStore = Config.createTelegramConfigStore();
51
51
  const lockRuntime = Locks.createTelegramLockRuntime<Pi.ExtensionContext>();
52
+ const lockOwnershipGuard =
53
+ Locks.createTelegramLockOwnershipGuard(lockRuntime);
52
54
  const activeTurnRuntime = Queue.createTelegramActiveTurnStore();
53
55
  const buttonActionStore = OutboundHandlers.createTelegramButtonActionStore();
54
56
  const pendingModelSwitchStore =
@@ -198,6 +200,7 @@ export default function (pi: Pi.ExtensionAPI) {
198
200
  sendDraft: sendMessageDraft,
199
201
  sendMessage,
200
202
  editMessageText: editTelegramMessageText,
203
+ canSend: lockOwnershipGuard.ownsCurrentProcess,
201
204
  ...replyTransport,
202
205
  });
203
206
  const { finalizeMarkdownPreview } =
@@ -459,6 +462,7 @@ export default function (pi: Pi.ExtensionAPI) {
459
462
  sendQueuedAttachments: queuedAttachmentSender,
460
463
  planOutboundReply: outboundReplyPlanner,
461
464
  sendOutboundReplyArtifacts: outboundReplyArtifactSender,
465
+ isCurrentOwner: lockOwnershipGuard.ownsContext,
462
466
  getActiveToolExecutions: lifecycle.getActiveToolExecutions,
463
467
  setActiveToolExecutions: lifecycle.setActiveToolExecutions,
464
468
  triggerPendingModelSwitchAbort: modelSwitchController.triggerPendingAbort,
@@ -258,6 +258,15 @@ function getRemainingTelegramInboundTimeout(
258
258
  return Math.max(1, timeout - (Date.now() - startedAt));
259
259
  }
260
260
 
261
+ function getTelegramInboundInitialCompositionStepTimeout(
262
+ handler: TelegramInboundHandlerConfig,
263
+ step: TelegramInboundCommandTemplateConfig,
264
+ ): number {
265
+ const timeout = getTelegramInboundHandlerTimeout(handler);
266
+ const stepTimeout = getTelegramInboundHandlerConfiguredTimeout(step);
267
+ return stepTimeout === undefined ? timeout : Math.min(stepTimeout, timeout);
268
+ }
269
+
261
270
  function getTelegramInboundCompositionStepTimeout(
262
271
  handler: TelegramInboundHandlerConfig,
263
272
  step: TelegramInboundCommandTemplateConfig,
@@ -421,7 +430,9 @@ async function executeTelegramTextHandler(
421
430
  output,
422
431
  cwd,
423
432
  deps,
424
- getTelegramInboundCompositionStepTimeout(handler, step, startedAt),
433
+ index === 0
434
+ ? getTelegramInboundInitialCompositionStepTimeout(handler, step)
435
+ : getTelegramInboundCompositionStepTimeout(handler, step, startedAt),
425
436
  );
426
437
  } catch (error) {
427
438
  if (typeof step === "object" && step.critical) throw error;
@@ -498,7 +509,9 @@ async function executeTelegramInboundHandler(
498
509
  cwd,
499
510
  deps,
500
511
  false,
501
- getTelegramInboundCompositionStepTimeout(handler, step, startedAt),
512
+ index === 0
513
+ ? getTelegramInboundInitialCompositionStepTimeout(handler, step)
514
+ : getTelegramInboundCompositionStepTimeout(handler, step, startedAt),
502
515
  index === 0 ? undefined : output,
503
516
  );
504
517
  } catch (error) {
package/lib/locks.ts CHANGED
@@ -61,6 +61,11 @@ export interface TelegramLockRuntime<TContext extends TelegramLockContext> {
61
61
  owns: (ctx?: TelegramLockContext) => boolean;
62
62
  }
63
63
 
64
+ export interface TelegramLockOwnershipGuard<TContext extends TelegramLockContext> {
65
+ ownsCurrentProcess: () => boolean;
66
+ ownsContext: (ctx: TContext) => boolean;
67
+ }
68
+
64
69
  export interface TelegramLockRuntimeOptions {
65
70
  key?: string;
66
71
  locksPath?: string;
@@ -234,6 +239,17 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
234
239
  };
235
240
  }
236
241
 
242
+ export function createTelegramLockOwnershipGuard<
243
+ TContext extends TelegramLockContext,
244
+ >(
245
+ lock: TelegramLockRuntime<TContext>,
246
+ ): TelegramLockOwnershipGuard<TContext> {
247
+ return {
248
+ ownsCurrentProcess: () => lock.owns(),
249
+ ownsContext: (ctx) => lock.owns(ctx),
250
+ };
251
+ }
252
+
237
253
  export function createTelegramLockedPollingRuntime<
238
254
  TContext extends TelegramLockContext,
239
255
  >(
package/lib/menu-model.ts CHANGED
@@ -236,7 +236,7 @@ export interface TelegramModelMenuRuntime<
236
236
 
237
237
  export const TELEGRAM_MODEL_PAGE_SIZE = 6;
238
238
  const TELEGRAM_MODEL_PAGE_PICKER_ROW_SIZE = 4;
239
- export const MODEL_MENU_TITLE = "<b>Choose a model:</b>";
239
+ export const MODEL_MENU_TITLE = "<b>🤖 Choose a model:</b>";
240
240
  export const MODEL_PAGE_MENU_TITLE = "<b>Choose a page:</b>";
241
241
 
242
242
  function truncateTelegramButtonLabel(label: string, maxLength = 56): string {
@@ -109,7 +109,7 @@ export async function handleTelegramThinkingMenuCallbackAction(
109
109
  }
110
110
 
111
111
  export function buildThinkingMenuText(): string {
112
- return "<b>Choose a thinking level:</b>";
112
+ return "<b>🧠 Choose a thinking level:</b>";
113
113
  }
114
114
 
115
115
  export function buildThinkingMenuReplyMarkup(
package/lib/preview.ts CHANGED
@@ -89,6 +89,7 @@ export interface TelegramPreviewRuntimeDeps<
89
89
  chunks: TelegramRenderedChunk[],
90
90
  options?: { replyMarkup?: TReplyMarkup },
91
91
  ) => Promise<number | undefined>;
92
+ canSend?: () => boolean;
92
93
  }
93
94
 
94
95
  export interface TelegramPreviewActiveTurn {
@@ -182,6 +183,7 @@ export interface TelegramPreviewControllerDeps<
182
183
  chunks: TelegramRenderedChunk[],
183
184
  options?: { replyMarkup?: TReplyMarkup },
184
185
  ) => Promise<number | undefined>;
186
+ canSend?: () => boolean;
185
187
  throttleMs?: number;
186
188
  maxDraftId?: number;
187
189
  setTimer?: (
@@ -418,6 +420,7 @@ export function createTelegramPreviewController<
418
420
  options,
419
421
  ),
420
422
  editRenderedMessage: deps.editRenderedMessage,
423
+ canSend: deps.canSend,
421
424
  });
422
425
  return {
423
426
  getState: () => state,
@@ -574,6 +577,10 @@ async function performTelegramPreviewFlush<
574
577
  state: TelegramPreviewRuntimeState,
575
578
  deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
576
579
  ): Promise<void> {
580
+ if (deps.canSend && !deps.canSend()) {
581
+ await clearTelegramPreview(chatId, deps);
582
+ return;
583
+ }
577
584
  const snapshot = buildTelegramPreviewSnapshot({
578
585
  state,
579
586
  maxMessageLength: deps.maxMessageLength,
@@ -658,6 +665,10 @@ export async function finalizeTelegramPreview<
658
665
  ): Promise<boolean> {
659
666
  const state = deps.getState();
660
667
  if (!state) return false;
668
+ if (deps.canSend && !deps.canSend()) {
669
+ await clearTelegramPreview(chatId, deps);
670
+ return false;
671
+ }
661
672
  await flushTelegramPreview(chatId, deps);
662
673
  const finalText = buildTelegramPreviewFinalText(state);
663
674
  if (!finalText) {
@@ -683,6 +694,10 @@ export async function finalizeTelegramMarkdownPreview<
683
694
  ): Promise<boolean> {
684
695
  const state = deps.getState();
685
696
  if (!state) return false;
697
+ if (deps.canSend && !deps.canSend()) {
698
+ await clearTelegramPreview(chatId, deps);
699
+ return false;
700
+ }
686
701
  await flushTelegramPreview(chatId, deps);
687
702
  const chunks = deps.renderTelegramMessage(markdown, { mode: "markdown" });
688
703
  if (chunks.length === 0) {
package/lib/queue.ts CHANGED
@@ -771,6 +771,7 @@ export interface TelegramAgentEndRuntimeDeps<
771
771
  preserveQueuedTurnsAsHistory: boolean;
772
772
  resetRuntimeState: () => void;
773
773
  updateStatus: () => void;
774
+ isCurrentOwner?: () => boolean;
774
775
  dispatchNextQueuedTelegramTurn: () => void;
775
776
  clearPreview: (chatId: number) => Promise<void>;
776
777
  setPreviewPendingText: (text: string) => void;
@@ -815,6 +816,7 @@ export interface TelegramAgentEndHookRuntimeDeps<
815
816
  getPreserveQueuedTurnsAsHistory: () => boolean;
816
817
  resetRuntimeState: () => void;
817
818
  updateStatus: (ctx: TContext) => void;
819
+ isCurrentOwner?: (ctx: TContext) => boolean;
818
820
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
819
821
  requestDeferredDispatchNextQueuedTelegramTurn: (
820
822
  dispatch: (ctx: TContext) => void,
@@ -932,6 +934,9 @@ export function createTelegramAgentEndHook<
932
934
  preserveQueuedTurnsAsHistory: deps.getPreserveQueuedTurnsAsHistory(),
933
935
  resetRuntimeState: deps.resetRuntimeState,
934
936
  updateStatus: () => deps.updateStatus(ctx),
937
+ isCurrentOwner: deps.isCurrentOwner
938
+ ? () => deps.isCurrentOwner?.(ctx) ?? false
939
+ : undefined,
935
940
  dispatchNextQueuedTelegramTurn: () => {
936
941
  deps.requestDeferredDispatchNextQueuedTelegramTurn(
937
942
  deps.dispatchNextQueuedTelegramTurn,
@@ -964,6 +969,10 @@ export async function handleTelegramAgentEndRuntime<
964
969
  const replyMarkup = outboundReply?.replyMarkup;
965
970
  deps.resetRuntimeState();
966
971
  deps.updateStatus();
972
+ if (turn && deps.isCurrentOwner && !deps.isCurrentOwner()) {
973
+ await deps.clearPreview(turn.chatId);
974
+ return;
975
+ }
967
976
  const endPlan = buildTelegramAgentEndPlan({
968
977
  hasTurn: !!turn,
969
978
  stopReason: assistant.stopReason,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "private": false,
5
5
  "description": "Better Telegram DM bridge extension for π",
6
6
  "type": "module",