@llblab/pi-telegram 0.10.5 → 0.10.7

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,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.7: Stale Context Hardening Hotfix
4
+
5
+ - `[Session Reloads]` Context-sensitive command, pairing, queue, session-start, and update-dispatch paths now ignore only stale-session/stale-context failures instead of swallowing broad runtime errors. Impact: the bridge survives ctx replacement/fork/reload races while real bugs still surface for diagnostics.
6
+ - `[Runtime Status]` Restored status update error propagation so existing polling/dispatch safety wrappers can record stale status failures as structured runtime events instead of losing diagnostics inside the status domain.
7
+ - `[Release]` Added a tag-triggered GitHub Actions release workflow that verifies the `vX.Y.Z` tag matches `package.json`, extracts the matching `CHANGELOG.md` section, and publishes a GitHub Release automatically.
8
+ - `[Tests]` Added focused regressions proving the newly guarded call sites tolerate stale context errors and still rethrow unrelated failures.
9
+
10
+ ## 0.10.6: Native Typing Keepalive Hotfix
11
+
12
+ - `[Typing]` Telegram native `typing` chat actions now refresh every 2.5s instead of every 4s. Impact: the bot's Telegram-side typing animation has more headroom to stay visible during model retries, transient model/API errors, and other long-running agent work.
13
+ - `[Queue Menu]` Empty queue refresh now rotates through a wider set of small status phrases. Impact: repeatedly refreshing an empty queue feels less repetitive while preserving the same callbacks and menu layout.
14
+ - `[Tests]` Added coverage for the default native typing keepalive cadence.
15
+
3
16
  ## 0.10.5: Queue Continuity And Input Resilience Hotfix
4
17
 
5
18
  - `[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.
package/lib/commands.ts CHANGED
@@ -579,7 +579,7 @@ export interface TelegramCommandRuntimeDeps<
579
579
  }
580
580
 
581
581
  export const TELEGRAM_APP_MENU_INTRO_HTML = [
582
- "<b>π Telegram bridge</b>",
582
+ "<b>π Telegram</b>",
583
583
  "",
584
584
  `${formatTelegramCommandEmojiPrefix("start")}/start — Open menu / Pair bridge`,
585
585
  `${formatTelegramCommandEmojiPrefix("compact")}/compact — Compact current session`,
@@ -826,18 +826,34 @@ export async function handleTelegramCompactCommand(
826
826
  await deps.sendTextReply("Compaction started.");
827
827
  }
828
828
 
829
+ function isTelegramStaleContextError(error: unknown): boolean {
830
+ return (
831
+ error instanceof Error &&
832
+ (error.message.includes("stale after session") ||
833
+ error.message.includes("stale ctx"))
834
+ );
835
+ }
836
+
829
837
  export async function handleTelegramStatusCommand<TContext>(deps: {
830
838
  ctx: TContext;
831
839
  showStatus: (ctx: TContext) => Promise<void>;
832
840
  }): Promise<void> {
833
- await deps.showStatus(deps.ctx);
841
+ try {
842
+ await deps.showStatus(deps.ctx);
843
+ } catch (error) {
844
+ if (!isTelegramStaleContextError(error)) throw error;
845
+ }
834
846
  }
835
847
 
836
848
  export async function handleTelegramModelCommand<TContext>(deps: {
837
849
  ctx: TContext;
838
850
  openModelMenu: (ctx: TContext) => Promise<void>;
839
851
  }): Promise<void> {
840
- await deps.openModelMenu(deps.ctx);
852
+ try {
853
+ await deps.openModelMenu(deps.ctx);
854
+ } catch (error) {
855
+ if (!isTelegramStaleContextError(error)) throw error;
856
+ }
841
857
  }
842
858
 
843
859
  export async function executeTelegramCommandAction<TMessage, TContext>(
package/lib/config.ts CHANGED
@@ -185,6 +185,14 @@ export function getTelegramAuthorizationState(
185
185
  return { kind: "deny" };
186
186
  }
187
187
 
188
+ function isTelegramStaleContextError(error: unknown): boolean {
189
+ return (
190
+ error instanceof Error &&
191
+ (error.message.includes("stale after session") ||
192
+ error.message.includes("stale ctx"))
193
+ );
194
+ }
195
+
188
196
  export async function pairTelegramUserIfNeeded<TContext>(
189
197
  userId: number,
190
198
  deps: TelegramUserPairingDeps<TContext>,
@@ -196,7 +204,11 @@ export async function pairTelegramUserIfNeeded<TContext>(
196
204
  if (authorization.kind !== "pair") return false;
197
205
  deps.setAllowedUserId(authorization.userId);
198
206
  await deps.persistConfig();
199
- deps.updateStatus(deps.ctx);
207
+ try {
208
+ deps.updateStatus(deps.ctx);
209
+ } catch (error) {
210
+ if (!isTelegramStaleContextError(error)) throw error;
211
+ }
200
212
  return true;
201
213
  }
202
214
 
package/lib/menu-queue.ts CHANGED
@@ -18,6 +18,14 @@ const EMPTY_QUEUE_REFRESH_TITLES = [
18
18
  "<b>🫙 Still nothing in queue.</b>",
19
19
  "<b>🍃 Queue remains empty.</b>",
20
20
  "<b>🕳 Nothing queued yet.</b>",
21
+ "<b>🦗 Queue crickets continue.</b>",
22
+ "<b>🌙 Queue is peacefully idle.</b>",
23
+ "<b>🧘 Nothing waiting. Very zen.</b>",
24
+ "<b>🪐 Queue orbit is clear.</b>",
25
+ "<b>🧺 Basket is empty.</b>",
26
+ "<b>🔭 No prompts on the horizon.</b>",
27
+ "<b>🫧 Queue bubbles: none.</b>",
28
+ "<b>🛸 No queued signals detected.</b>",
21
29
  ] as const;
22
30
  type TelegramQueueMenuReplyMarkup = TelegramInlineKeyboardMarkup;
23
31
  interface TelegramQueueMenuItem {
package/lib/queue.ts CHANGED
@@ -1289,6 +1289,14 @@ export interface TelegramPromptEnqueueController<TMessage, TContext = unknown> {
1289
1289
  enqueue: (messages: TMessage[], ctx: TContext) => Promise<void>;
1290
1290
  }
1291
1291
 
1292
+ function isTelegramStaleContextError(error: unknown): boolean {
1293
+ return (
1294
+ error instanceof Error &&
1295
+ (error.message.includes("stale after session") ||
1296
+ error.message.includes("stale ctx"))
1297
+ );
1298
+ }
1299
+
1292
1300
  export function buildTelegramSessionStartState<TModel = unknown>(
1293
1301
  currentModel: TModel | undefined,
1294
1302
  ): TelegramSessionStartState<TModel> {
@@ -1326,7 +1334,11 @@ export async function startTelegramSessionRuntime<TContext, TModel = unknown>(
1326
1334
  await deps.loadConfig();
1327
1335
  deps.applyState(buildTelegramSessionStartState(deps.currentModel));
1328
1336
  await deps.prepareTempDir();
1329
- deps.bindDeferredDispatchContext?.(deps.ctx);
1337
+ try {
1338
+ deps.bindDeferredDispatchContext?.(deps.ctx);
1339
+ } catch (error) {
1340
+ if (!isTelegramStaleContextError(error)) throw error;
1341
+ }
1330
1342
  deps.updateStatus();
1331
1343
  }
1332
1344
 
@@ -1478,7 +1490,11 @@ export function reorderTelegramQueueItemsRuntime<TContext>(
1478
1490
  deps.setQueuedItems(
1479
1491
  [...deps.getQueuedItems()].sort(compareTelegramQueueItems),
1480
1492
  );
1481
- deps.updateStatus(deps.ctx);
1493
+ try {
1494
+ deps.updateStatus(deps.ctx);
1495
+ } catch (error) {
1496
+ if (!isTelegramStaleContextError(error)) throw error;
1497
+ }
1482
1498
  }
1483
1499
 
1484
1500
  export function clearTelegramQueueItemsRuntime<TContext>(
@@ -1487,7 +1503,11 @@ export function clearTelegramQueueItemsRuntime<TContext>(
1487
1503
  const removedCount = deps.getQueuedItems().length;
1488
1504
  if (removedCount === 0) return 0;
1489
1505
  deps.setQueuedItems([]);
1490
- deps.updateStatus(deps.ctx);
1506
+ try {
1507
+ deps.updateStatus(deps.ctx);
1508
+ } catch (error) {
1509
+ if (!isTelegramStaleContextError(error)) throw error;
1510
+ }
1491
1511
  return removedCount;
1492
1512
  }
1493
1513
 
@@ -1501,7 +1521,11 @@ export function removeTelegramQueueItemsByMessageIdsRuntime<TContext>(
1501
1521
  );
1502
1522
  if (removedCount === 0) return 0;
1503
1523
  deps.setQueuedItems(items);
1504
- deps.updateStatus(deps.ctx);
1524
+ try {
1525
+ deps.updateStatus(deps.ctx);
1526
+ } catch (error) {
1527
+ if (!isTelegramStaleContextError(error)) throw error;
1528
+ }
1505
1529
  return removedCount;
1506
1530
  }
1507
1531
 
package/lib/runtime.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Owns small session-local runtime primitives that are shared by orchestration but are not specific to queueing, rendering, polling, or Telegram transport
5
5
  */
6
6
 
7
- const TELEGRAM_TYPING_ACTION_INTERVAL_MS = 4000;
7
+ const TELEGRAM_TYPING_ACTION_INTERVAL_MS = 2500;
8
8
 
9
9
  export interface TelegramRuntimeQueueCounters {
10
10
  nextQueuedTelegramItemOrder: number;
package/lib/updates.ts CHANGED
@@ -746,6 +746,14 @@ export async function handleAuthorizedTelegramReactionUpdate<TContext>(
746
746
  );
747
747
  }
748
748
 
749
+ function isTelegramStaleContextError(error: unknown): boolean {
750
+ return (
751
+ error instanceof Error &&
752
+ (error.message.includes("stale after session") ||
753
+ error.message.includes("stale ctx"))
754
+ );
755
+ }
756
+
749
757
  export async function executeTelegramUpdatePlan<
750
758
  TContext = unknown,
751
759
  TReactionUpdate extends TelegramMessageReactionUpdated =
@@ -761,81 +769,85 @@ export async function executeTelegramUpdatePlan<
761
769
  TMessage
762
770
  >,
763
771
  ): Promise<void> {
764
- if (plan.kind === "ignore") return;
765
- if (plan.kind === "deleted") {
766
- deps.removePendingMediaGroupMessages(plan.messageIds);
767
- deps.removeQueuedTelegramTurnsByMessageIds(plan.messageIds, deps.ctx);
768
- return;
769
- }
770
- if (plan.kind === "reaction") {
771
- await deps.handleAuthorizedTelegramReactionUpdate(
772
- plan.reactionUpdate,
773
- deps.ctx,
774
- );
775
- return;
776
- }
777
- if (plan.kind === "callback") {
778
- if (plan.shouldPair) {
779
- await deps.pairTelegramUserIfNeeded(plan.query.from.id, deps.ctx);
780
- }
781
- if (plan.shouldDeny) {
782
- const callbackQueryId = getTelegramCallbackQueryId(plan.query);
783
- if (callbackQueryId) {
784
- await deps.answerCallbackQuery(
785
- callbackQueryId,
786
- "This bot is not authorized for your account.",
787
- );
788
- }
772
+ try {
773
+ if (plan.kind === "ignore") return;
774
+ if (plan.kind === "deleted") {
775
+ deps.removePendingMediaGroupMessages(plan.messageIds);
776
+ deps.removeQueuedTelegramTurnsByMessageIds(plan.messageIds, deps.ctx);
789
777
  return;
790
778
  }
791
- await deps.handleAuthorizedTelegramCallbackQuery(plan.query, deps.ctx);
792
- return;
793
- }
794
- if (plan.kind === "guest") {
795
- if (plan.shouldDeny) {
796
- await deps.answerGuestQuery(
797
- plan.guestMessage.guest_query_id,
798
- "Access denied.",
779
+ if (plan.kind === "reaction") {
780
+ await deps.handleAuthorizedTelegramReactionUpdate(
781
+ plan.reactionUpdate,
782
+ deps.ctx,
799
783
  );
800
784
  return;
801
785
  }
802
- if (deps.handleAuthorizedTelegramGuestMessage) {
803
- await deps.handleAuthorizedTelegramGuestMessage(
804
- plan.guestMessage,
805
- deps.ctx,
806
- );
786
+ if (plan.kind === "callback") {
787
+ if (plan.shouldPair) {
788
+ await deps.pairTelegramUserIfNeeded(plan.query.from.id, deps.ctx);
789
+ }
790
+ if (plan.shouldDeny) {
791
+ const callbackQueryId = getTelegramCallbackQueryId(plan.query);
792
+ if (callbackQueryId) {
793
+ await deps.answerCallbackQuery(
794
+ callbackQueryId,
795
+ "This bot is not authorized for your account.",
796
+ );
797
+ }
798
+ return;
799
+ }
800
+ await deps.handleAuthorizedTelegramCallbackQuery(plan.query, deps.ctx);
801
+ return;
807
802
  }
808
- return;
809
- }
810
- const pairedNow = plan.shouldPair
811
- ? await deps.pairTelegramUserIfNeeded(plan.message.from.id, deps.ctx)
812
- : false;
813
- const replyTarget = getTelegramMessageReplyTarget(plan.message);
814
- if (
815
- plan.kind === "message" &&
816
- pairedNow &&
817
- plan.shouldNotifyPaired &&
818
- replyTarget
819
- ) {
820
- await deps.sendTextReply(
821
- replyTarget.chatId,
822
- replyTarget.messageId,
823
- "Telegram bridge paired with this account.",
824
- );
825
- }
826
- if (plan.shouldDeny) {
827
- if (replyTarget) {
803
+ if (plan.kind === "guest") {
804
+ if (plan.shouldDeny) {
805
+ await deps.answerGuestQuery(
806
+ plan.guestMessage.guest_query_id,
807
+ "Access denied.",
808
+ );
809
+ return;
810
+ }
811
+ if (deps.handleAuthorizedTelegramGuestMessage) {
812
+ await deps.handleAuthorizedTelegramGuestMessage(
813
+ plan.guestMessage,
814
+ deps.ctx,
815
+ );
816
+ }
817
+ return;
818
+ }
819
+ const pairedNow = plan.shouldPair
820
+ ? await deps.pairTelegramUserIfNeeded(plan.message.from.id, deps.ctx)
821
+ : false;
822
+ const replyTarget = getTelegramMessageReplyTarget(plan.message);
823
+ if (
824
+ plan.kind === "message" &&
825
+ pairedNow &&
826
+ plan.shouldNotifyPaired &&
827
+ replyTarget
828
+ ) {
828
829
  await deps.sendTextReply(
829
830
  replyTarget.chatId,
830
831
  replyTarget.messageId,
831
- "This bot is not authorized for your account.",
832
+ "Telegram bridge paired with this account.",
832
833
  );
833
834
  }
834
- return;
835
- }
836
- if (plan.kind === "edited-message") {
837
- await deps.handleAuthorizedTelegramEditedMessage(plan.message, deps.ctx);
838
- return;
835
+ if (plan.shouldDeny) {
836
+ if (replyTarget) {
837
+ await deps.sendTextReply(
838
+ replyTarget.chatId,
839
+ replyTarget.messageId,
840
+ "This bot is not authorized for your account.",
841
+ );
842
+ }
843
+ return;
844
+ }
845
+ if (plan.kind === "edited-message") {
846
+ await deps.handleAuthorizedTelegramEditedMessage(plan.message, deps.ctx);
847
+ return;
848
+ }
849
+ await deps.handleAuthorizedTelegramMessage(plan.message, deps.ctx);
850
+ } catch (error) {
851
+ if (!isTelegramStaleContextError(error)) throw error;
839
852
  }
840
- await deps.handleAuthorizedTelegramMessage(plan.message, deps.ctx);
841
853
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.10.5",
3
+ "version": "0.10.7",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"