@llblab/pi-telegram 0.10.3 → 0.10.5

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.5: Queue Continuity And Input Resilience Hotfix
4
+
5
+ - `[Compaction]` `/compact` completion and failure callbacks now request deferred queue dispatch instead of dispatching immediately. Impact: queued Telegram turns resume after compaction state and π idle/pending-message state have a chance to settle.
6
+ - `[Text Groups]` Long-text split recovery is more aggressive where Telegram chunking actually drifts: the debounce is rounded to 1s, the conservative 3600-character start threshold is preserved, and continuation messages can span a much wider message-id gap while staying scoped to the same chat/user and non-command text. Impact: very large pasted prompts are more likely to arrive as one agent turn instead of several fragmented turns.
7
+ - `[Runtime Status]` Typing-loop and prompt-dispatch status updates are now best-effort and record stale-context failures as structured runtime events. Impact: status/Running indicators remain resilient after error paths without hiding diagnostics.
8
+ - `[Tests]` Added regressions for deferred compact dispatch, stale status failures in typing/dispatch paths, and many-part split-text grouping.
9
+
10
+ ## 0.10.4: Polling Status Resilience Hotfix
11
+
12
+ - `[Polling]` Status-bar updates from the polling loop are now best-effort and no longer crash the extension when a captured session context becomes stale after session reload. Failures are recorded as structured polling runtime events with `phase: "status-update"`. Impact: polling cleanup and retry status updates stay resilient without changing the Telegram API, config, or operator workflow.
13
+ - `[Tests]` Added stale-context polling regressions for startup, cleanup, and retry status updates. Impact: the external PR #43 fix is now covered by maintainer-side tests and kept aligned with local style.
14
+
3
15
  ## 0.10.3: Dependency Audit Hotfix
4
16
 
5
17
  - `[Dependencies]` Refreshed the lockfile transitive dependency set to resolve current `protobufjs` / `@protobufjs/utf8` npm audit advisories inherited through development peer installs. Impact: `npm run validate` is green again without changing runtime API or bridge behavior.
package/README.md CHANGED
@@ -240,9 +240,11 @@ Import from `@llblab/pi-telegram`, call `registerTelegramSection()`, and return
240
240
 
241
241
  Third-party extensions that integrate with `pi-telegram`:
242
242
 
243
- | Extension | Description | Install |
244
- |-----------|-------------|---------|
245
- | [`pi-telegram-tool-status`](https://github.com/Timur00Kh/pi-telegram-tool-status) | Live-updating service message listing tools used by the agent. One message per Telegram prompt, edited in-place as tools execute. | `pi install npm:pi-telegram-tool-status` |
243
+ - [`pi-telegram-tool-status`](https://github.com/Timur00Kh/pi-telegram-tool-status) Live-updating service messages that list tools used by the agent. It keeps one message per Telegram prompt and edits it in place as tools execute.
244
+
245
+ ```bash
246
+ pi install npm:pi-telegram-tool-status
247
+ ```
246
248
 
247
249
  ## License
248
250
 
package/index.ts CHANGED
@@ -352,6 +352,8 @@ export default function (pi: Pi.ExtensionAPI) {
352
352
  inboundHandlerRuntime,
353
353
  updateStatus,
354
354
  dispatchNextQueuedTelegramTurn,
355
+ requestDeferredDispatchNextQueuedTelegramTurn:
356
+ deferredQueueDispatchRuntime.request,
355
357
  answerCallbackQuery,
356
358
  editInteractiveMessage,
357
359
  sendInteractiveMessage,
package/lib/commands.ts CHANGED
@@ -317,6 +317,9 @@ export interface TelegramCompactCommandDeps extends TelegramRuntimeEventRecorder
317
317
  setCompactionInProgress: (inProgress: boolean) => void;
318
318
  updateStatus: () => void;
319
319
  dispatchNextQueuedTelegramTurn: () => void;
320
+ requestDeferredDispatchNextQueuedTelegramTurn?: (
321
+ dispatch: () => void,
322
+ ) => void;
320
323
  compact: (callbacks: {
321
324
  onComplete: () => void;
322
325
  onError: (error: unknown) => void;
@@ -547,6 +550,9 @@ export interface TelegramCommandRuntimeDeps<
547
550
  setCompactionInProgress: (inProgress: boolean) => void;
548
551
  updateStatus: (ctx: TContext) => void;
549
552
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
553
+ requestDeferredDispatchNextQueuedTelegramTurn?: (
554
+ dispatch: (ctx: TContext) => void,
555
+ ) => void;
550
556
  enqueueContinueTurn: (message: TMessage, ctx: TContext) => Promise<void>;
551
557
  compact: (
552
558
  ctx: TContext,
@@ -758,6 +764,22 @@ export async function handleTelegramContinueCommand<TMessage, TContext>(
758
764
  await deps.enqueueContinueTurn(message, ctx);
759
765
  }
760
766
 
767
+ function dispatchNextQueuedTelegramTurnAfterCompact(
768
+ deps: Pick<
769
+ TelegramCompactCommandDeps,
770
+ | "dispatchNextQueuedTelegramTurn"
771
+ | "requestDeferredDispatchNextQueuedTelegramTurn"
772
+ >,
773
+ ): void {
774
+ if (deps.requestDeferredDispatchNextQueuedTelegramTurn) {
775
+ deps.requestDeferredDispatchNextQueuedTelegramTurn(
776
+ deps.dispatchNextQueuedTelegramTurn,
777
+ );
778
+ return;
779
+ }
780
+ deps.dispatchNextQueuedTelegramTurn();
781
+ }
782
+
761
783
  export async function handleTelegramCompactCommand(
762
784
  deps: TelegramCompactCommandDeps,
763
785
  ): Promise<void> {
@@ -781,13 +803,13 @@ export async function handleTelegramCompactCommand(
781
803
  onComplete: () => {
782
804
  deps.setCompactionInProgress(false);
783
805
  deps.updateStatus();
784
- deps.dispatchNextQueuedTelegramTurn();
806
+ dispatchNextQueuedTelegramTurnAfterCompact(deps);
785
807
  void deps.sendTextReply("Compaction completed.");
786
808
  },
787
809
  onError: (error) => {
788
810
  deps.setCompactionInProgress(false);
789
811
  deps.updateStatus();
790
- deps.dispatchNextQueuedTelegramTurn();
812
+ dispatchNextQueuedTelegramTurnAfterCompact(deps);
791
813
  deps.recordRuntimeEvent?.("compact", error);
792
814
  const errorMessage = getTelegramCommandErrorMessage(error);
793
815
  void deps.sendTextReply(`Compaction failed: ${errorMessage}`);
@@ -1069,6 +1091,13 @@ async function handleTelegramCommandRuntime<
1069
1091
  updateStatus: updateStatusFor(commandCtx),
1070
1092
  dispatchNextQueuedTelegramTurn: () =>
1071
1093
  deps.dispatchNextQueuedTelegramTurn(commandCtx),
1094
+ requestDeferredDispatchNextQueuedTelegramTurn:
1095
+ deps.requestDeferredDispatchNextQueuedTelegramTurn
1096
+ ? (dispatch) =>
1097
+ deps.requestDeferredDispatchNextQueuedTelegramTurn?.(() =>
1098
+ dispatch(),
1099
+ )
1100
+ : undefined,
1072
1101
  compact: (callbacks) => deps.compact(commandCtx, callbacks),
1073
1102
  sendTextReply: sendReplyFor(nextMessage),
1074
1103
  recordRuntimeEvent: deps.recordRuntimeEvent,
package/lib/polling.ts CHANGED
@@ -91,7 +91,8 @@ export function createTelegramPollingActivityReader(
91
91
  return () => isTelegramPollingControllerActive(state);
92
92
  }
93
93
 
94
- export interface TelegramPollingRuntimeDeps<TContext> {
94
+ export interface TelegramPollingRuntimeDeps<TContext>
95
+ extends TelegramRuntimeEventRecorderPort {
95
96
  hasBotToken: () => boolean;
96
97
  getPollingPromise: () => Promise<void> | undefined;
97
98
  setPollingPromise: (promise: Promise<void> | undefined) => void;
@@ -150,6 +151,7 @@ export function createTelegramPollingControllerRuntime<
150
151
  }),
151
152
  updateStatus: deps.updateStatus,
152
153
  createAbortController: deps.createAbortController,
154
+ recordRuntimeEvent: deps.recordRuntimeEvent,
153
155
  });
154
156
  }
155
157
 
@@ -191,6 +193,22 @@ export async function stopTelegramPollingRuntime<TContext>(
191
193
  deps.setPollingPromise(undefined);
192
194
  }
193
195
 
196
+ function updateTelegramPollingStatusSafely<TContext>(
197
+ updateStatus: (ctx: TContext, message?: string) => void,
198
+ ctx: TContext,
199
+ options: {
200
+ message?: string;
201
+ recordRuntimeEvent?: TelegramRuntimeEventRecorderPort["recordRuntimeEvent"];
202
+ } = {},
203
+ ): void {
204
+ try {
205
+ updateStatus(ctx, options.message);
206
+ } catch (error) {
207
+ // The polling loop can outlive the session context it captured.
208
+ options.recordRuntimeEvent?.("polling", error, { phase: "status-update" });
209
+ }
210
+ }
211
+
194
212
  export function startTelegramPollingRuntime<TContext>(
195
213
  ctx: TContext,
196
214
  deps: TelegramPollingRuntimeDeps<TContext>,
@@ -208,10 +226,14 @@ export function startTelegramPollingRuntime<TContext>(
208
226
  const promise = deps.runPollLoop(ctx, controller.signal).finally(() => {
209
227
  deps.setPollingPromise(undefined);
210
228
  deps.setPollingController(undefined);
211
- deps.updateStatus(ctx);
229
+ updateTelegramPollingStatusSafely(deps.updateStatus, ctx, {
230
+ recordRuntimeEvent: deps.recordRuntimeEvent,
231
+ });
212
232
  });
213
233
  deps.setPollingPromise(promise);
214
- deps.updateStatus(ctx);
234
+ updateTelegramPollingStatusSafely(deps.updateStatus, ctx, {
235
+ recordRuntimeEvent: deps.recordRuntimeEvent,
236
+ });
215
237
  }
216
238
 
217
239
  export interface TelegramRuntimeEventRecorderPort {
@@ -281,10 +303,15 @@ export function createTelegramPollLoopRunner<
281
303
  persistConfig: deps.persistConfig,
282
304
  handleUpdate: deps.handleUpdate,
283
305
  onErrorStatus: (message) => {
284
- deps.updateStatus(ctx, message);
306
+ updateTelegramPollingStatusSafely(deps.updateStatus, ctx, {
307
+ message,
308
+ recordRuntimeEvent: deps.recordRuntimeEvent,
309
+ });
285
310
  },
286
311
  onStatusReset: () => {
287
- deps.updateStatus(ctx);
312
+ updateTelegramPollingStatusSafely(deps.updateStatus, ctx, {
313
+ recordRuntimeEvent: deps.recordRuntimeEvent,
314
+ });
288
315
  },
289
316
  sleep,
290
317
  maxUpdateFailures: deps.maxUpdateFailures,
package/lib/routing.ts CHANGED
@@ -79,6 +79,9 @@ export interface TelegramInboundRouteRuntimeDeps<
79
79
  inboundHandlerRuntime: TelegramInboundHandlerRuntime<TContext>;
80
80
  updateStatus: (ctx: TContext, error?: string) => void;
81
81
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
82
+ requestDeferredDispatchNextQueuedTelegramTurn?: (
83
+ dispatch: (ctx: TContext) => void,
84
+ ) => void;
82
85
  answerCallbackQuery: (
83
86
  callbackQueryId: string,
84
87
  text?: string,
@@ -339,6 +342,8 @@ export function createTelegramInboundRouteRuntime<
339
342
  deps.bridgeRuntime.lifecycle.setCompactionInProgress,
340
343
  updateStatus: deps.updateStatus,
341
344
  dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
345
+ requestDeferredDispatchNextQueuedTelegramTurn:
346
+ deps.requestDeferredDispatchNextQueuedTelegramTurn,
342
347
  enqueueContinueTurn,
343
348
  compact: deps.compact,
344
349
  allocateItemOrder: deps.bridgeRuntime.queue.allocateItemOrder,
package/lib/runtime.ts CHANGED
@@ -328,6 +328,25 @@ export interface TelegramRuntimeEventRecorderPort {
328
328
  ) => void;
329
329
  }
330
330
 
331
+ function updateTelegramRuntimeStatusSafely<TContext>(
332
+ updateStatus: (ctx: TContext, error?: string) => void,
333
+ ctx: TContext,
334
+ options: {
335
+ error?: string;
336
+ category: string;
337
+ phase: string;
338
+ recordRuntimeEvent?: TelegramRuntimeEventRecorderPort["recordRuntimeEvent"];
339
+ },
340
+ ): void {
341
+ try {
342
+ updateStatus(ctx, options.error);
343
+ } catch (statusError) {
344
+ options.recordRuntimeEvent?.(options.category, statusError, {
345
+ phase: options.phase,
346
+ });
347
+ }
348
+ }
349
+
331
350
  export interface TelegramTypingLoopStarterDeps<
332
351
  TContext,
333
352
  > extends TelegramRuntimeEventRecorderPort {
@@ -351,7 +370,12 @@ export function createTelegramTypingLoopStarter<TContext>(
351
370
  } catch (error) {
352
371
  const message =
353
372
  error instanceof Error ? error.message : String(error);
354
- deps.updateStatus(ctx, message);
373
+ updateTelegramRuntimeStatusSafely(deps.updateStatus, ctx, {
374
+ error: message,
375
+ category: "typing",
376
+ phase: "status-update",
377
+ recordRuntimeEvent: deps.recordRuntimeEvent,
378
+ });
355
379
  deps.recordRuntimeEvent?.("typing", error, {
356
380
  chatId: targetChatId,
357
381
  });
@@ -468,13 +492,22 @@ export function createTelegramPromptDispatchLifecycle<TContext>(
468
492
  onPromptDispatchStart: (ctx: TContext, chatId?: number): void => {
469
493
  deps.lifecycle.setDispatchPending(true);
470
494
  deps.startTypingLoop(ctx, chatId);
471
- deps.updateStatus(ctx);
495
+ updateTelegramRuntimeStatusSafely(deps.updateStatus, ctx, {
496
+ category: "dispatch",
497
+ phase: "status-update",
498
+ recordRuntimeEvent: deps.recordRuntimeEvent,
499
+ });
472
500
  },
473
501
  onPromptDispatchFailure: (ctx: TContext, message: string): void => {
474
502
  deps.lifecycle.clearDispatchPending();
475
503
  deps.typing.stop();
476
504
  deps.recordRuntimeEvent?.("dispatch", new Error(message));
477
- deps.updateStatus(ctx, `dispatch failed: ${message}`);
505
+ updateTelegramRuntimeStatusSafely(deps.updateStatus, ctx, {
506
+ error: `dispatch failed: ${message}`,
507
+ category: "dispatch",
508
+ phase: "status-update",
509
+ recordRuntimeEvent: deps.recordRuntimeEvent,
510
+ });
478
511
  },
479
512
  };
480
513
  }
@@ -4,8 +4,9 @@
4
4
  * Owns conservative delayed grouping for Telegram text messages that look like automatic long-message splits
5
5
  */
6
6
 
7
- const TELEGRAM_TEXT_GROUP_DEBOUNCE_MS = 700;
7
+ const TELEGRAM_TEXT_GROUP_DEBOUNCE_MS = 1000;
8
8
  const TELEGRAM_TEXT_GROUP_MIN_SPLIT_LENGTH = 3600;
9
+ const TELEGRAM_TEXT_GROUP_MAX_MESSAGE_ID_GAP = 10;
9
10
 
10
11
  export interface TelegramTextGroupMessage {
11
12
  message_id: number;
@@ -91,7 +92,7 @@ function canAppendTelegramTextGroupMessage<
91
92
  return (
92
93
  !!previous &&
93
94
  message.message_id > previous.message_id &&
94
- message.message_id <= previous.message_id + 2 &&
95
+ message.message_id <= previous.message_id + TELEGRAM_TEXT_GROUP_MAX_MESSAGE_ID_GAP &&
95
96
  text.length > 0 &&
96
97
  !isTelegramTextGroupCommand(text)
97
98
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.10.3",
3
+ "version": "0.10.5",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"