@llblab/pi-telegram 0.51.0 → 0.51.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 (46) hide show
  1. package/AGENTS.md +4 -3
  2. package/BACKLOG.md +3 -8
  3. package/CHANGELOG.md +11 -0
  4. package/README.md +1 -1
  5. package/dist/lib/bindings.d.ts +2 -0
  6. package/dist/lib/bindings.js +3 -4
  7. package/dist/lib/bus-api.d.ts +0 -1
  8. package/dist/lib/bus-api.js +0 -3
  9. package/dist/lib/bus-follower.d.ts +3 -1
  10. package/dist/lib/bus-follower.js +6 -1
  11. package/dist/lib/commands.d.ts +5 -0
  12. package/dist/lib/commands.js +9 -0
  13. package/dist/lib/extension.js +6 -3
  14. package/dist/lib/menu-model.d.ts +3 -3
  15. package/dist/lib/menu-model.js +17 -3
  16. package/dist/lib/menu.d.ts +5 -7
  17. package/dist/lib/menu.js +4 -5
  18. package/dist/lib/model.d.ts +12 -12
  19. package/dist/lib/model.js +49 -35
  20. package/dist/lib/queue.d.ts +8 -2
  21. package/dist/lib/queue.js +64 -19
  22. package/dist/lib/replies.d.ts +4 -0
  23. package/dist/lib/replies.js +14 -0
  24. package/dist/lib/routing.d.ts +1 -0
  25. package/dist/lib/routing.js +5 -1
  26. package/dist/lib/runtime.d.ts +3 -6
  27. package/dist/lib/runtime.js +4 -55
  28. package/dist/lib/telegram-api.js +36 -10
  29. package/dist/package.json +1 -1
  30. package/docs/architecture.md +9 -9
  31. package/docs/multi-instance-bus.md +3 -3
  32. package/docs/public-api.md +1 -1
  33. package/lib/bindings.ts +14 -4
  34. package/lib/bus-api.ts +0 -7
  35. package/lib/bus-follower.ts +13 -2
  36. package/lib/commands.ts +19 -0
  37. package/lib/extension.ts +10 -3
  38. package/lib/menu-model.ts +25 -4
  39. package/lib/menu.ts +14 -9
  40. package/lib/model.ts +77 -44
  41. package/lib/queue.ts +76 -24
  42. package/lib/replies.ts +18 -0
  43. package/lib/routing.ts +7 -1
  44. package/lib/runtime.ts +7 -58
  45. package/lib/telegram-api.ts +43 -17
  46. package/package.json +1 -1
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 = 2500;
7
+ const TELEGRAM_TYPING_ACTION_INTERVAL_MS = 3_000;
8
8
  const TELEGRAM_TYPING_IDLE_DRAIN_MAX_MS = 250;
9
9
 
10
10
  export interface TelegramRuntimeQueueCounters {
@@ -319,7 +319,6 @@ export interface TelegramTypingLoopDeps {
319
319
  chatId: number,
320
320
  options?: { message_thread_id?: number },
321
321
  ) => Promise<unknown>;
322
- sendAggregateTypingAction?: (chatId: number) => Promise<unknown>;
323
322
  shouldContinue?: () => boolean;
324
323
  onStopped?: () => void;
325
324
  }
@@ -364,7 +363,6 @@ export interface TelegramTypingLoopStarterDeps<
364
363
  chatId: number,
365
364
  options?: { message_thread_id?: number },
366
365
  ) => Promise<unknown>;
367
- sendAggregateTypingAction?: (chatId: number) => Promise<unknown>;
368
366
  updateStatus: (ctx: TContext, error?: string) => void;
369
367
  isContextActive?: (ctx: TContext) => boolean;
370
368
  isTransportAvailable?: () => boolean;
@@ -378,7 +376,7 @@ export function createTelegramTypingLoopStarter<TContext>(
378
376
  ctx: TContext,
379
377
  chatId?: number,
380
378
  options?: { target?: TelegramTypingLoopTarget },
381
- ) => void {
379
+ ) => boolean {
382
380
  return (ctx, chatId, options) => {
383
381
  const transportAuthority = deps.getTransportAuthority?.();
384
382
  const hasTransport = (): boolean =>
@@ -386,9 +384,9 @@ export function createTelegramTypingLoopStarter<TContext>(
386
384
  ? transportAuthority !== undefined &&
387
385
  Object.is(deps.getTransportAuthority(), transportAuthority)
388
386
  : deps.isTransportAvailable?.() !== false;
389
- if (!hasTransport()) return;
387
+ if (!hasTransport()) return false;
390
388
  let active = true;
391
- deps.typing.start({
389
+ return deps.typing.start({
392
390
  chatId: chatId ?? deps.getDefaultChatId(),
393
391
  target: options?.target,
394
392
  intervalMs: deps.intervalMs ?? TELEGRAM_TYPING_ACTION_INTERVAL_MS,
@@ -407,14 +405,6 @@ export function createTelegramTypingLoopStarter<TContext>(
407
405
  deps.typing.stop();
408
406
  return;
409
407
  }
410
- const message =
411
- error instanceof Error ? error.message : String(error);
412
- updateTelegramRuntimeStatusSafely(deps.updateStatus, ctx, {
413
- error: message,
414
- category: "typing",
415
- phase: "status-update",
416
- recordRuntimeEvent: deps.recordRuntimeEvent,
417
- });
418
408
  try {
419
409
  deps.recordRuntimeEvent?.("typing", error, {
420
410
  chatId: targetChatId,
@@ -424,41 +414,6 @@ export function createTelegramTypingLoopStarter<TContext>(
424
414
  }
425
415
  }
426
416
  },
427
- sendAggregateTypingAction: deps.sendAggregateTypingAction
428
- ? async (targetChatId) => {
429
- if (!active) return;
430
- if (!hasTransport()) {
431
- deps.typing.stop();
432
- return;
433
- }
434
- try {
435
- await deps.sendAggregateTypingAction?.(targetChatId);
436
- } catch (error) {
437
- if (deps.isContextActive?.(ctx) === false) return;
438
- if (!active) return;
439
- if (!hasTransport()) {
440
- deps.typing.stop();
441
- return;
442
- }
443
- const message =
444
- error instanceof Error ? error.message : String(error);
445
- updateTelegramRuntimeStatusSafely(deps.updateStatus, ctx, {
446
- error: message,
447
- category: "typing",
448
- phase: "status-update",
449
- recordRuntimeEvent: deps.recordRuntimeEvent,
450
- });
451
- try {
452
- deps.recordRuntimeEvent?.("typing", error, {
453
- chatId: targetChatId,
454
- aggregate: true,
455
- });
456
- } catch {
457
- // Typing diagnostics cannot escape the in-flight action owner.
458
- }
459
- }
460
- }
461
- : undefined,
462
417
  shouldContinue: hasTransport,
463
418
  onStopped: () => {
464
419
  active = false;
@@ -502,12 +457,7 @@ export function startTelegramTypingLoop(
502
457
  const targetChatId = activeDeps.chatId;
503
458
  const threadParams = getTelegramTypingLoopThreadParams(activeDeps.target);
504
459
  const typing = Promise.resolve()
505
- .then(async () => {
506
- await activeDeps.sendTypingAction(targetChatId, threadParams);
507
- if (threadParams?.message_thread_id !== undefined) {
508
- await activeDeps.sendAggregateTypingAction?.(targetChatId);
509
- }
510
- })
460
+ .then(() => activeDeps.sendTypingAction(targetChatId, threadParams))
511
461
  .then(() => undefined)
512
462
  .catch(() => undefined);
513
463
  state.typingInFlight = typing;
@@ -602,7 +552,7 @@ export interface TelegramPromptDispatchLifecycleDeps<
602
552
  ctx: TContext,
603
553
  chatId?: number,
604
554
  options?: { target?: TelegramTypingLoopTarget },
605
- ) => void;
555
+ ) => boolean | void;
606
556
  updateStatus: (ctx: TContext, error?: string) => void;
607
557
  }
608
558
 
@@ -616,7 +566,6 @@ export interface TelegramPromptDispatchRuntimeDeps<
616
566
  chatId: number,
617
567
  options?: { message_thread_id?: number },
618
568
  ) => Promise<unknown>;
619
- sendAggregateTypingAction?: (chatId: number) => Promise<unknown>;
620
569
  updateStatus: (ctx: TContext, error?: string) => void;
621
570
  isContextActive?: (ctx: TContext) => boolean;
622
571
  isTransportAvailable?: () => boolean;
@@ -629,7 +578,7 @@ export interface TelegramPromptDispatchRuntime<TContext> {
629
578
  ctx: TContext,
630
579
  chatId?: number,
631
580
  options?: { target?: TelegramTypingLoopTarget },
632
- ) => void;
581
+ ) => boolean | void;
633
582
  onPromptDispatchStart: (ctx: TContext, chatId?: number) => void;
634
583
  onPromptDispatchFailure: (ctx: TContext, message: string) => void;
635
584
  }
@@ -1866,14 +1866,16 @@ export function createTelegramBridgeApiRuntime(
1866
1866
  deps.chatActionMinIntervalMs ?? 2_000,
1867
1867
  );
1868
1868
  const chatActionMaxGates = Math.max(1, deps.chatActionMaxGates ?? 256);
1869
- const chatActionGates = new Map<
1870
- string,
1871
- { inFlight?: Promise<unknown>; notBeforeMs: number }
1872
- >();
1873
- const getChatActionKey = (
1869
+ type ChatActionGate = {
1870
+ inFlight?: Promise<unknown>;
1871
+ notBeforeMs: number;
1872
+ };
1873
+ const chatActionGates = new Map<string, ChatActionGate>();
1874
+ const chatActionChatGates = new Map<string, ChatActionGate>();
1875
+ const getChatActionKeys = (
1874
1876
  method: string,
1875
1877
  body: Record<string, unknown>,
1876
- ): string | undefined => {
1878
+ ): { action: string; chat: string } | undefined => {
1877
1879
  if (method !== "sendChatAction") return undefined;
1878
1880
  const chatId = body.chat_id;
1879
1881
  const action = body.action;
@@ -1883,12 +1885,16 @@ export function createTelegramBridgeApiRuntime(
1883
1885
  ) {
1884
1886
  return undefined;
1885
1887
  }
1888
+ const chat = String(chatId);
1886
1889
  const threadId = body.message_thread_id;
1887
- return `${String(chatId)}:${
1888
- typeof threadId === "number" || typeof threadId === "string"
1889
- ? String(threadId)
1890
- : "all"
1891
- }:${action}`;
1890
+ return {
1891
+ action: `${chat}:${
1892
+ typeof threadId === "number" || typeof threadId === "string"
1893
+ ? String(threadId)
1894
+ : "all"
1895
+ }:${action}`,
1896
+ chat,
1897
+ };
1892
1898
  };
1893
1899
  const callRecorded = async <TResponse>(
1894
1900
  method: string,
@@ -1896,22 +1902,38 @@ export function createTelegramBridgeApiRuntime(
1896
1902
  options?: TelegramApiCallOptions,
1897
1903
  ): Promise<TResponse> => {
1898
1904
  const recoverError = deps.captureRequestErrorHandler?.(body);
1899
- const chatActionKey = getChatActionKey(method, body);
1900
- if (chatActionKey) {
1905
+ const chatActionKeys = getChatActionKeys(method, body);
1906
+ if (chatActionKeys) {
1901
1907
  const nowMs = now();
1902
1908
  for (const [key, candidate] of chatActionGates) {
1903
1909
  if (!candidate.inFlight && nowMs >= candidate.notBeforeMs) {
1904
1910
  chatActionGates.delete(key);
1905
1911
  }
1906
1912
  }
1907
- let gate = chatActionGates.get(chatActionKey);
1913
+ for (const [key, candidate] of chatActionChatGates) {
1914
+ if (!candidate.inFlight && nowMs >= candidate.notBeforeMs) {
1915
+ chatActionChatGates.delete(key);
1916
+ }
1917
+ }
1918
+ let gate = chatActionGates.get(chatActionKeys.action);
1908
1919
  if (!gate) {
1909
1920
  if (chatActionGates.size >= chatActionMaxGates) return true as TResponse;
1910
1921
  gate = { notBeforeMs: 0 };
1911
- chatActionGates.set(chatActionKey, gate);
1922
+ chatActionGates.set(chatActionKeys.action, gate);
1923
+ }
1924
+ let chatGate = chatActionChatGates.get(chatActionKeys.chat);
1925
+ if (!chatGate) {
1926
+ if (chatActionChatGates.size >= chatActionMaxGates) {
1927
+ return true as TResponse;
1928
+ }
1929
+ chatGate = { notBeforeMs: 0 };
1930
+ chatActionChatGates.set(chatActionKeys.chat, chatGate);
1912
1931
  }
1913
1932
  if (gate.inFlight) return (await gate.inFlight) as TResponse;
1914
- if (now() < gate.notBeforeMs) return true as TResponse;
1933
+ if (chatGate.inFlight) return true as TResponse;
1934
+ if (nowMs < gate.notBeforeMs || nowMs < chatGate.notBeforeMs) {
1935
+ return true as TResponse;
1936
+ }
1915
1937
  let request: Promise<TResponse>;
1916
1938
  request = Promise.resolve()
1917
1939
  .then(() =>
@@ -1931,7 +1953,9 @@ export function createTelegramBridgeApiRuntime(
1931
1953
  chatActionMinIntervalMs,
1932
1954
  (error.retryAfterSeconds ?? 0) * 1_000,
1933
1955
  );
1934
- gate.notBeforeMs = now() + retryAfterMs;
1956
+ const notBeforeMs = now() + retryAfterMs;
1957
+ gate.notBeforeMs = notBeforeMs;
1958
+ chatGate.notBeforeMs = Math.max(chatGate.notBeforeMs, notBeforeMs);
1935
1959
  deps.recordRuntimeEvent(
1936
1960
  "api",
1937
1961
  error,
@@ -1952,8 +1976,10 @@ export function createTelegramBridgeApiRuntime(
1952
1976
  })
1953
1977
  .finally(() => {
1954
1978
  if (gate.inFlight === request) gate.inFlight = undefined;
1979
+ if (chatGate.inFlight === request) chatGate.inFlight = undefined;
1955
1980
  });
1956
1981
  gate.inFlight = request;
1982
+ chatGate.inFlight = request;
1957
1983
  return request;
1958
1984
  }
1959
1985
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.51.0",
3
+ "version": "0.51.2",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"