@llblab/pi-telegram 0.18.3 → 0.18.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/lib/preview.ts CHANGED
@@ -12,7 +12,6 @@ import {
12
12
  } from "./target.ts";
13
13
  import { shouldSuppressPreviewForVoice } from "./voice.ts";
14
14
 
15
- const TELEGRAM_PREVIEW_THROTTLE_MS = 0;
16
15
  const TELEGRAM_DRAFT_ID_MAX = 2_147_483_647;
17
16
  const TELEGRAM_DRAFT_PREVIEW_MAX_CHARS = 4096;
18
17
 
@@ -26,7 +25,6 @@ export interface TelegramPreviewState {
26
25
  }
27
26
 
28
27
  export interface TelegramPreviewRuntimeState extends TelegramPreviewState {
29
- flushTimer?: ReturnType<typeof setTimeout>;
30
28
  flushPromise?: Promise<void>;
31
29
  flushRequested?: boolean;
32
30
  }
@@ -36,7 +34,6 @@ export type TelegramPreviewReplyMarkup = unknown;
36
34
  export interface TelegramPreviewRuntimeDeps {
37
35
  getState: () => TelegramPreviewRuntimeState | undefined;
38
36
  setState: (state: TelegramPreviewRuntimeState | undefined) => void;
39
- clearScheduledFlush: (state: TelegramPreviewRuntimeState) => void;
40
37
  maxMessageLength: number;
41
38
  getDraftSupport: () => TelegramDraftSupport;
42
39
  setDraftSupport: (support: TelegramDraftSupport) => void;
@@ -132,13 +129,7 @@ export interface TelegramPreviewControllerDeps {
132
129
  },
133
130
  ) => Promise<unknown>;
134
131
  canSend?: () => boolean;
135
- throttleMs?: number;
136
132
  maxDraftId?: number;
137
- setTimer?: (
138
- callback: () => void,
139
- ms: number,
140
- ) => ReturnType<typeof setTimeout>;
141
- clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
142
133
  recordRuntimeEvent?: (
143
134
  category: string,
144
135
  error: unknown,
@@ -154,7 +145,7 @@ export interface TelegramPreviewController {
154
145
  resetState: () => void;
155
146
  clear: (
156
147
  chatId: number,
157
- options?: { target?: TelegramTarget },
148
+ options?: { awaitFlush?: boolean; target?: TelegramTarget },
158
149
  ) => Promise<void>;
159
150
  flush: (
160
151
  chatId: number,
@@ -171,7 +162,8 @@ export interface TelegramPreviewController {
171
162
  ) => Promise<boolean>;
172
163
  }
173
164
 
174
- export type TelegramPreviewControllerRuntimeDeps = TelegramPreviewControllerDeps;
165
+ export type TelegramPreviewControllerRuntimeDeps =
166
+ TelegramPreviewControllerDeps;
175
167
 
176
168
  export function createTelegramPreviewControllerRuntime(
177
169
  deps: TelegramPreviewControllerRuntimeDeps,
@@ -181,10 +173,7 @@ export function createTelegramPreviewControllerRuntime(
181
173
  maxMessageLength: deps.maxMessageLength,
182
174
  initialDraftSupport: deps.initialDraftSupport,
183
175
  sendDraft: deps.sendDraft,
184
- throttleMs: deps.throttleMs,
185
176
  maxDraftId: deps.maxDraftId,
186
- setTimer: deps.setTimer,
187
- clearTimer: deps.clearTimer,
188
177
  recordRuntimeEvent: deps.recordRuntimeEvent,
189
178
  });
190
179
  }
@@ -223,7 +212,7 @@ export function createTelegramNativeMarkdownPreviewFinalizer<
223
212
  getState: () => TelegramPreviewRuntimeState | undefined;
224
213
  clear: (
225
214
  chatId: number,
226
- options?: { target?: TelegramTarget },
215
+ options?: { awaitFlush?: boolean; target?: TelegramTarget },
227
216
  ) => Promise<void>;
228
217
  discard?: () => void;
229
218
  sendMarkdownReply: (
@@ -283,12 +272,6 @@ export function createTelegramPreviewController(
283
272
  deps: TelegramPreviewControllerDeps,
284
273
  ): TelegramPreviewController {
285
274
  let state: TelegramPreviewRuntimeState | undefined;
286
- const clearTimer = deps.clearTimer ?? clearTimeout;
287
- const setTimer =
288
- deps.setTimer ??
289
- ((callback: () => void, ms: number): ReturnType<typeof setTimeout> =>
290
- setTimeout(callback, ms));
291
- const throttleMs = deps.throttleMs ?? TELEGRAM_PREVIEW_THROTTLE_MS;
292
275
  const maxDraftId = deps.maxDraftId ?? TELEGRAM_DRAFT_ID_MAX;
293
276
  const maxMessageLength =
294
277
  deps.maxMessageLength ?? TELEGRAM_DRAFT_PREVIEW_MAX_CHARS;
@@ -299,11 +282,6 @@ export function createTelegramPreviewController(
299
282
  setState: (nextState) => {
300
283
  state = nextState;
301
284
  },
302
- clearScheduledFlush: (nextState) => {
303
- if (!nextState.flushTimer) return;
304
- clearTimer(nextState.flushTimer);
305
- nextState.flushTimer = undefined;
306
- },
307
285
  maxMessageLength,
308
286
  getDraftSupport: () => draftSupport,
309
287
  setDraftSupport: (support) => {
@@ -334,15 +312,8 @@ export function createTelegramPreviewController(
334
312
  flush: (chatId, options) =>
335
313
  flushTelegramPreview(chatId, getRuntimeDeps(), options),
336
314
  scheduleFlush: (chatId, options) => {
337
- if (!state || state.flushTimer) return;
338
- if (throttleMs <= 0) {
339
- void flushTelegramPreview(chatId, getRuntimeDeps(), options);
340
- return;
341
- }
342
- state.flushTimer = setTimer(() => {
343
- void flushTelegramPreview(chatId, getRuntimeDeps(), options);
344
- }, throttleMs);
345
- state.flushTimer.unref?.();
315
+ if (!state) return;
316
+ void flushTelegramPreview(chatId, getRuntimeDeps(), options);
346
317
  },
347
318
  finalize: (chatId, _replyToMessageId, options) =>
348
319
  finalizeTelegramPreview(chatId, getRuntimeDeps(), options),
@@ -464,7 +435,6 @@ export async function clearTelegramPreview(
464
435
  ): Promise<void> {
465
436
  const state = deps.getState();
466
437
  if (!state) return;
467
- deps.clearScheduledFlush(state);
468
438
  if (state.flushPromise && options.awaitFlush !== false) {
469
439
  state.flushRequested = false;
470
440
  await state.flushPromise.catch(() => {});
@@ -699,6 +669,10 @@ function findSafeTelegramRichMarkdownDraftEnd(markdown: string): number {
699
669
  return safeEnd;
700
670
  }
701
671
 
672
+ function hasTelegramPreviewVisibleContent(markdown: string): boolean {
673
+ return /[\p{L}\p{N}]/u.test(markdown);
674
+ }
675
+
702
676
  export function getSafeTelegramRichMarkdownDraftPrefix(
703
677
  markdown: string,
704
678
  maxMessageLength: number,
@@ -710,13 +684,21 @@ export function getSafeTelegramRichMarkdownDraftPrefix(
710
684
  ? source.slice(0, maxMessageLength)
711
685
  : source;
712
686
  const safeEnd = findSafeTelegramRichMarkdownDraftEnd(limited);
713
- if (safeEnd > 0) return limited.slice(0, safeEnd).trimEnd() || undefined;
687
+ if (safeEnd > 0) {
688
+ const safePrefix = limited.slice(0, safeEnd).trimEnd();
689
+ return hasTelegramPreviewVisibleContent(safePrefix)
690
+ ? safePrefix
691
+ : undefined;
692
+ }
714
693
  let candidateEnd = limited.length;
715
694
  while (candidateEnd > 0) {
716
695
  candidateEnd = limited.lastIndexOf(" ", candidateEnd - 1);
717
696
  if (candidateEnd <= 0) return undefined;
718
697
  const candidate = limited.slice(0, candidateEnd).trimEnd();
719
- if (findSafeTelegramRichMarkdownDraftEnd(candidate) === candidate.length) {
698
+ if (
699
+ hasTelegramPreviewVisibleContent(candidate) &&
700
+ findSafeTelegramRichMarkdownDraftEnd(candidate) === candidate.length
701
+ ) {
720
702
  return candidate || undefined;
721
703
  }
722
704
  }
@@ -795,7 +777,6 @@ export async function flushTelegramPreview(
795
777
  await state.flushPromise;
796
778
  return;
797
779
  }
798
- state.flushTimer = undefined;
799
780
  state.flushPromise = (async () => {
800
781
  do {
801
782
  state.flushRequested = false;
package/lib/queue.ts CHANGED
@@ -1920,6 +1920,76 @@ export function createTelegramDeferredQueueDispatchRuntime<TContext = unknown>(
1920
1920
  };
1921
1921
  }
1922
1922
 
1923
+ // --- Dispatch Watchdog Runtime ---
1924
+
1925
+ export interface TelegramQueueDispatchWatchdogRuntime<TContext = unknown> {
1926
+ start: (ctx: TContext) => void;
1927
+ stop: () => void;
1928
+ poke: () => void;
1929
+ }
1930
+
1931
+ export interface TelegramQueueDispatchWatchdogRuntimeDeps<
1932
+ TContext = unknown,
1933
+ > extends TelegramRuntimeEventRecorderPort {
1934
+ hasQueuedItems: () => boolean;
1935
+ dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
1936
+ intervalMs?: number;
1937
+ setInterval?: (
1938
+ callback: () => void,
1939
+ ms: number,
1940
+ ) => ReturnType<typeof setInterval>;
1941
+ clearInterval?: (timer: ReturnType<typeof setInterval>) => void;
1942
+ }
1943
+
1944
+ export function createTelegramQueueDispatchWatchdogRuntime<
1945
+ TContext = unknown,
1946
+ >(
1947
+ deps: TelegramQueueDispatchWatchdogRuntimeDeps<TContext>,
1948
+ ): TelegramQueueDispatchWatchdogRuntime<TContext> {
1949
+ const intervalMs = deps.intervalMs ?? 1000;
1950
+ const setIntervalFn: NonNullable<
1951
+ TelegramQueueDispatchWatchdogRuntimeDeps<TContext>["setInterval"]
1952
+ > = deps.setInterval ?? ((callback, ms) => setInterval(callback, ms));
1953
+ const clearIntervalFn: NonNullable<
1954
+ TelegramQueueDispatchWatchdogRuntimeDeps<TContext>["clearInterval"]
1955
+ > = deps.clearInterval ?? ((timer) => clearInterval(timer));
1956
+ let ctx: TContext | undefined;
1957
+ let interval: ReturnType<typeof setInterval> | undefined;
1958
+ let dispatchInFlight = false;
1959
+ const tick = (): void => {
1960
+ if (ctx === undefined || dispatchInFlight || !deps.hasQueuedItems()) return;
1961
+ dispatchInFlight = true;
1962
+ try {
1963
+ deps.dispatchNextQueuedTelegramTurn(ctx);
1964
+ } catch (error) {
1965
+ deps.recordRuntimeEvent?.("dispatch", error, {
1966
+ phase: "queue-watchdog",
1967
+ });
1968
+ } finally {
1969
+ dispatchInFlight = false;
1970
+ }
1971
+ };
1972
+ const stop = (): void => {
1973
+ ctx = undefined;
1974
+ if (!interval) return;
1975
+ clearIntervalFn(interval);
1976
+ interval = undefined;
1977
+ };
1978
+ return {
1979
+ start: (nextCtx) => {
1980
+ ctx = nextCtx;
1981
+ if (!interval) {
1982
+ const nextInterval = setIntervalFn(tick, intervalMs);
1983
+ interval = nextInterval;
1984
+ nextInterval.unref?.();
1985
+ }
1986
+ tick();
1987
+ },
1988
+ stop,
1989
+ poke: tick,
1990
+ };
1991
+ }
1992
+
1923
1993
  // --- Dispatch Runtime ---
1924
1994
 
1925
1995
  export interface TelegramPromptDeliveryOptions {
@@ -4,7 +4,14 @@
4
4
  * Owns session-local append-only runtime evidence for debugging without becoming routing state
5
5
  */
6
6
 
7
- import { existsSync, mkdirSync, statSync, writeFileSync, appendFile } from "node:fs";
7
+ import {
8
+ copyFileSync,
9
+ existsSync,
10
+ mkdirSync,
11
+ statSync,
12
+ writeFileSync,
13
+ appendFile,
14
+ } from "node:fs";
8
15
  import { homedir } from "node:os";
9
16
  import { dirname, join, resolve } from "node:path";
10
17
 
@@ -17,6 +24,7 @@ export interface TelegramRuntimeJsonlEvent {
17
24
 
18
25
  export interface TelegramRuntimeJsonlLogOptions {
19
26
  path?: string;
27
+ previousPath?: string;
20
28
  maxBytes?: number;
21
29
  getNowMs?: () => number;
22
30
  }
@@ -57,6 +65,8 @@ export function createTelegramRuntimeJsonlLog(
57
65
  options: TelegramRuntimeJsonlLogOptions = {},
58
66
  ): TelegramRuntimeJsonlLog {
59
67
  const path = options.path ?? getTelegramRuntimeLogPath();
68
+ const previousPath =
69
+ options.previousPath ?? path.replace(/\.jsonl$/u, ".previous.jsonl");
60
70
  const maxBytes = options.maxBytes ?? DEFAULT_MAX_LOG_BYTES;
61
71
  const getNowMs = options.getNowMs ?? Date.now;
62
72
  let scopeKey: string | undefined;
@@ -66,8 +76,15 @@ export function createTelegramRuntimeJsonlLog(
66
76
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
67
77
  };
68
78
 
79
+ const preserveCurrentLog = () => {
80
+ if (!existsSync(path)) return;
81
+ mkdirSync(dirname(previousPath), { recursive: true, mode: 0o700 });
82
+ copyFileSync(path, previousPath);
83
+ };
84
+
69
85
  const writeReset = (reason: string, scope?: Record<string, unknown>) => {
70
86
  ensureParent();
87
+ preserveCurrentLog();
71
88
  writeFileSync(
72
89
  path,
73
90
  safeJsonLine({
@@ -75,6 +92,7 @@ export function createTelegramRuntimeJsonlLog(
75
92
  kind: "reset",
76
93
  reason,
77
94
  scope,
95
+ previousPath,
78
96
  }) + "\n",
79
97
  { mode: 0o600 },
80
98
  );
package/lib/status.ts CHANGED
@@ -113,6 +113,17 @@ export interface TelegramBridgeStatusBusFollower {
113
113
  status?: string;
114
114
  }
115
115
 
116
+ export interface TelegramBridgeStatusLocalBus {
117
+ leaderSocketPath?: string;
118
+ leaderTransport?: "pipe" | "socket";
119
+ followerSocketPath?: string;
120
+ followerTransport?: "pipe" | "socket";
121
+ followerRegistered?: boolean;
122
+ followerTarget?: { chatId: number; threadId?: number };
123
+ followerSlot?: string;
124
+ followerThreadName?: string;
125
+ }
126
+
116
127
  export interface TelegramBridgeStatusTopicTarget {
117
128
  instanceId?: string;
118
129
  status?: string;
@@ -187,6 +198,7 @@ export interface TelegramBridgeStatusLineState {
187
198
  pendingModelSwitch: boolean;
188
199
  queuedItems: Array<{ queueLane: TelegramStatusQueueLane }>;
189
200
  busFollowers?: TelegramBridgeStatusBusFollower[];
201
+ localBus?: TelegramBridgeStatusLocalBus;
190
202
  topicTargets?: TelegramBridgeStatusTopicTarget[];
191
203
  threadReservations?: TelegramBridgeStatusThreadReservation[];
192
204
  topicSyncObservations?: TelegramBridgeStatusSyncObservation[];
@@ -266,6 +278,7 @@ export interface TelegramBridgeStatusRuntimeDeps<
266
278
  }
267
279
  | undefined;
268
280
  getBusFollowers?: () => TelegramBridgeStatusBusFollower[];
281
+ getLocalBus?: () => TelegramBridgeStatusLocalBus | undefined;
269
282
  getTopicTargets?: () => TelegramBridgeStatusTopicTarget[];
270
283
  getThreadReservations?: () => TelegramBridgeStatusThreadReservation[];
271
284
  getTopicSyncObservations?: () => TelegramBridgeStatusSyncObservation[];
@@ -626,6 +639,7 @@ export function createTelegramBridgeStatusRuntime<
626
639
  pendingModelSwitch: deps.hasPendingModelSwitch(),
627
640
  queuedItems: deps.getQueuedItems(),
628
641
  busFollowers: deps.getBusFollowers?.(),
642
+ localBus: deps.getLocalBus?.(),
629
643
  topicTargets: deps.getTopicTargets?.(),
630
644
  threadReservations: deps.getThreadReservations?.(),
631
645
  topicSyncObservations: deps.getTopicSyncObservations?.(),
@@ -682,6 +696,7 @@ export function createTelegramStatusSnapshot(
682
696
  },
683
697
  liveRoster: {
684
698
  busFollowers: state.busFollowers ?? [],
699
+ ...(state.localBus ? { localBus: state.localBus } : {}),
685
700
  topicTargets: state.topicTargets ?? [],
686
701
  reservations: state.threadReservations ?? [],
687
702
  syncObservations: state.topicSyncObservations ?? [],
@@ -824,6 +839,38 @@ function buildTelegramBusFollowerLines(
824
839
  ];
825
840
  }
826
841
 
842
+ function buildTelegramLocalBusLines(
843
+ state: Pick<TelegramBridgeStatusLineState, "localBus">,
844
+ options: { verbose?: boolean } = {},
845
+ ): string[] {
846
+ const localBus = state.localBus;
847
+ if (!localBus) return [];
848
+ const target = formatTelegramStatusTarget(localBus.followerTarget);
849
+ const label = formatTelegramThreadStatusLabel({
850
+ slot: localBus.followerSlot,
851
+ threadName: localBus.followerThreadName,
852
+ });
853
+ const followerLine = `- follower registered: ${localBus.followerRegistered ? "yes" : "no"}${label ? ` ${label}` : ""}${target}`;
854
+ const lines = ["", "local bus:", followerLine];
855
+ if (options.verbose) {
856
+ if (localBus.leaderSocketPath) {
857
+ const transport = localBus.leaderTransport
858
+ ? ` [${localBus.leaderTransport}]`
859
+ : "";
860
+ lines.push(`- leader endpoint${transport}: ${localBus.leaderSocketPath}`);
861
+ }
862
+ if (localBus.followerSocketPath) {
863
+ const transport = localBus.followerTransport
864
+ ? ` [${localBus.followerTransport}]`
865
+ : "";
866
+ lines.push(
867
+ `- follower endpoint${transport}: ${localBus.followerSocketPath}`,
868
+ );
869
+ }
870
+ }
871
+ return lines;
872
+ }
873
+
827
874
  function buildTelegramSyncSliceLines(
828
875
  state: Pick<TelegramBridgeStatusLineState, "syncState">,
829
876
  ): string[] {
@@ -1038,6 +1085,8 @@ function buildTelegramBridgeCompactStatusLines(
1038
1085
  : []),
1039
1086
  ...(state.pendingModelSwitch ? ["- pending model switch: yes"] : []),
1040
1087
  ...buildTelegramBridgeCompactThreadLines(state),
1088
+ ...buildTelegramBusFollowerLines(state),
1089
+ ...buildTelegramLocalBusLines(state),
1041
1090
  ...buildTelegramThreadReconciliationLines(state),
1042
1091
  "",
1043
1092
  "diagnostics:",
@@ -1100,6 +1149,7 @@ export function buildTelegramBridgeDiagnosticStatusLines(
1100
1149
  `- queued turns: ${state.queuedItems.length}`,
1101
1150
  `- lanes: control=${controlQueueCount}, priority=${priorityQueueCount}, default=${defaultQueueCount}`,
1102
1151
  ...buildTelegramBusFollowerLines(state),
1152
+ ...buildTelegramLocalBusLines(state, { verbose: true }),
1103
1153
  ...buildTelegramTopicTargetDiagnosticLines(state),
1104
1154
  ...buildTelegramThreadReconciliationLines(state),
1105
1155
  ...buildTelegramSyncSliceLines(state),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.18.3",
3
+ "version": "0.18.5",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"