@llblab/pi-telegram 0.17.4 → 0.18.0
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/AGENTS.md +67 -32
- package/BACKLOG.md +59 -14
- package/CHANGELOG.md +40 -15
- package/README.md +63 -35
- package/docs/README.md +3 -1
- package/docs/architecture.md +55 -23
- package/docs/callback-namespaces.md +1 -1
- package/docs/inbound.md +1 -1
- package/docs/locks.md +0 -2
- package/docs/multi-instance-bus.md +483 -0
- package/docs/outbound.md +4 -3
- package/docs/public-api.md +12 -10
- package/docs/sections.md +2 -2
- package/docs/ui-style.md +76 -0
- package/index.ts +789 -32
- package/lib/bindings.ts +68 -12
- package/lib/bus-api.ts +314 -0
- package/lib/bus-follower.ts +853 -0
- package/lib/bus-leader.ts +915 -0
- package/lib/bus.ts +866 -0
- package/lib/command-templates.ts +9 -11
- package/lib/commands.ts +133 -47
- package/lib/config.ts +53 -5
- package/lib/lifecycle.ts +23 -7
- package/lib/locks.ts +230 -66
- package/lib/media.ts +30 -2
- package/lib/menu-model.ts +48 -17
- package/lib/menu-queue.ts +51 -20
- package/lib/menu-settings.ts +9 -5
- package/lib/menu-status.ts +3 -0
- package/lib/menu-thinking.ts +3 -0
- package/lib/menu.ts +67 -26
- package/lib/outbound-attachments.ts +102 -17
- package/lib/outbound-buttons.ts +6 -2
- package/lib/outbound-voice.ts +31 -11
- package/lib/outbound.ts +6 -4
- package/lib/ownership.ts +119 -0
- package/lib/pi.ts +26 -3
- package/lib/polling.ts +477 -7
- package/lib/preview.ts +141 -88
- package/lib/prompt-templates.ts +3 -3
- package/lib/prompts.ts +80 -30
- package/lib/queue.ts +193 -91
- package/lib/rendering.ts +0 -25
- package/lib/replies.ts +187 -55
- package/lib/routing.ts +1673 -9
- package/lib/runtime-log.ts +123 -0
- package/lib/runtime.ts +84 -12
- package/lib/sections.ts +28 -21
- package/lib/setup.ts +1 -1
- package/lib/status.ts +532 -9
- package/lib/sync.ts +618 -0
- package/lib/target.ts +49 -0
- package/lib/telegram-api.ts +405 -40
- package/lib/text-groups.ts +5 -1
- package/lib/thread-reconciler.ts +915 -0
- package/lib/threads.ts +2205 -0
- package/lib/turns.ts +48 -3
- package/lib/updates.ts +355 -32
- package/package.json +24 -2
- package/screenshot.png +0 -0
- package/docs/telegram-bot-api-rich-messages.md +0 -890
package/lib/polling.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Owns polling request builders, stop conditions, and the long-poll loop runtime for Telegram updates
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
8
|
+
|
|
7
9
|
export interface TelegramPollingConfig {
|
|
8
10
|
botToken?: string;
|
|
9
11
|
lastUpdateId?: number;
|
|
@@ -13,6 +15,19 @@ export interface TelegramUpdate {
|
|
|
13
15
|
update_id: number;
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
const TELEGRAM_INITIAL_SYNC_OFFSET = -1;
|
|
19
|
+
const TELEGRAM_INITIAL_SYNC_LIMIT = 1;
|
|
20
|
+
const TELEGRAM_INITIAL_SYNC_TIMEOUT_SECONDS = 0;
|
|
21
|
+
const TELEGRAM_LONG_POLL_LIMIT = 10;
|
|
22
|
+
const TELEGRAM_LONG_POLL_TIMEOUT_SECONDS = 30;
|
|
23
|
+
const TELEGRAM_THREAD_CAPABILITY_MONITOR_INTERVAL_MS = 5_000;
|
|
24
|
+
const TELEGRAM_THREAD_CAPABILITY_DISABLED_CONFIRMATION_PROBES = 2;
|
|
25
|
+
const TELEGRAM_POLLING_DEFAULT_MAX_UPDATE_FAILURES = 3;
|
|
26
|
+
const TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_LIMIT = 3;
|
|
27
|
+
const TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_MS = 1_000;
|
|
28
|
+
const TELEGRAM_GET_UPDATES_CONFLICT_SLOW_RETRY_MS = 3_000;
|
|
29
|
+
const TELEGRAM_POLLING_RETRY_MS = 3_000;
|
|
30
|
+
|
|
16
31
|
// Standard Telegram DM polling does not expose ordinary message-deletion events,
|
|
17
32
|
// so queue removal stays reaction-driven while delete-like business updates remain defensive-only.
|
|
18
33
|
export const TELEGRAM_ALLOWED_UPDATES = [
|
|
@@ -29,9 +44,9 @@ export function buildTelegramInitialSyncRequest(): {
|
|
|
29
44
|
timeout: number;
|
|
30
45
|
} {
|
|
31
46
|
return {
|
|
32
|
-
offset:
|
|
33
|
-
limit:
|
|
34
|
-
timeout:
|
|
47
|
+
offset: TELEGRAM_INITIAL_SYNC_OFFSET,
|
|
48
|
+
limit: TELEGRAM_INITIAL_SYNC_LIMIT,
|
|
49
|
+
timeout: TELEGRAM_INITIAL_SYNC_TIMEOUT_SECONDS,
|
|
35
50
|
};
|
|
36
51
|
}
|
|
37
52
|
|
|
@@ -43,8 +58,8 @@ export function buildTelegramLongPollRequest(lastUpdateId?: number): {
|
|
|
43
58
|
} {
|
|
44
59
|
return {
|
|
45
60
|
offset: lastUpdateId !== undefined ? lastUpdateId + 1 : undefined,
|
|
46
|
-
limit:
|
|
47
|
-
timeout:
|
|
61
|
+
limit: TELEGRAM_LONG_POLL_LIMIT,
|
|
62
|
+
timeout: TELEGRAM_LONG_POLL_TIMEOUT_SECONDS,
|
|
48
63
|
allowed_updates: TELEGRAM_ALLOWED_UPDATES,
|
|
49
64
|
};
|
|
50
65
|
}
|
|
@@ -258,6 +273,438 @@ export interface TelegramRuntimeEventRecorderPort {
|
|
|
258
273
|
) => void;
|
|
259
274
|
}
|
|
260
275
|
|
|
276
|
+
export type TelegramThreadCapabilityMode = "enabled" | "disabled" | "unknown";
|
|
277
|
+
|
|
278
|
+
export interface TelegramThreadCapabilityState {
|
|
279
|
+
threadMode?: TelegramThreadCapabilityMode;
|
|
280
|
+
updatedAtMs?: number;
|
|
281
|
+
lastSlot?: string;
|
|
282
|
+
lastReconcileAction?: string;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export interface TelegramThreadCapabilityRecordView {
|
|
286
|
+
status?: string;
|
|
287
|
+
target?: { chatId?: number; threadId?: number };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export interface TelegramThreadCapabilityStore {
|
|
291
|
+
load: () => Promise<void>;
|
|
292
|
+
persist: () => Promise<void>;
|
|
293
|
+
getBotState: () => TelegramThreadCapabilityState;
|
|
294
|
+
setBotState: (state: TelegramThreadCapabilityState) => void;
|
|
295
|
+
list?: () => TelegramThreadCapabilityRecordView[];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export interface TelegramThreadCapabilityReaderDeps {
|
|
299
|
+
getAllowedUserId: () => number | undefined;
|
|
300
|
+
callApi: <TResponse>(
|
|
301
|
+
method: string,
|
|
302
|
+
body: Record<string, unknown>,
|
|
303
|
+
) => Promise<TResponse>;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export interface TelegramStartupThreadCapabilityProbeDeps extends TelegramThreadCapabilityReaderDeps {
|
|
307
|
+
topicTargetStore: TelegramThreadCapabilityStore;
|
|
308
|
+
recordEvent: (
|
|
309
|
+
category: string,
|
|
310
|
+
message: unknown,
|
|
311
|
+
details?: Record<string, unknown>,
|
|
312
|
+
) => void;
|
|
313
|
+
setTopicModeUnavailable: (unavailable: boolean) => void;
|
|
314
|
+
getNowMs?: () => number;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export interface TelegramThreadCapabilityRuntimeDeps<
|
|
318
|
+
TContext,
|
|
319
|
+
> extends TelegramThreadCapabilityReaderDeps {
|
|
320
|
+
topicTargetStore: TelegramThreadCapabilityStore;
|
|
321
|
+
isBusConfigured: () => boolean;
|
|
322
|
+
ownsLock: (ctx: TContext) => boolean;
|
|
323
|
+
getPollingStartedWithTelegramBus: () => boolean;
|
|
324
|
+
setPollingStartedWithTelegramBus: (started: boolean) => void;
|
|
325
|
+
setTopicModeUnavailable: (unavailable: boolean) => void;
|
|
326
|
+
stopFollowerRegistration: () => void;
|
|
327
|
+
startClassicPolling: (ctx: TContext) => MaybePromise<void>;
|
|
328
|
+
stopClassicPolling: () => MaybePromise<void>;
|
|
329
|
+
startBusPolling: (ctx: TContext) => MaybePromise<void>;
|
|
330
|
+
stopBusPolling: () => MaybePromise<void>;
|
|
331
|
+
startLeaderHealth: () => void;
|
|
332
|
+
stopLeaderHealth: () => void;
|
|
333
|
+
isTopicModeUnavailableError?: (error: unknown) => boolean;
|
|
334
|
+
updateStatus: (ctx: TContext) => void;
|
|
335
|
+
recordEvent: (
|
|
336
|
+
category: string,
|
|
337
|
+
message: unknown,
|
|
338
|
+
details?: Record<string, unknown>,
|
|
339
|
+
) => void;
|
|
340
|
+
getNowMs?: () => number;
|
|
341
|
+
intervalMs?: number;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export interface TelegramThreadCapabilityMonitor<TContext> {
|
|
345
|
+
start: (ctx: TContext) => void;
|
|
346
|
+
stop: () => void;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export type TelegramThreadTargetObservationHandler<TContext> = (
|
|
350
|
+
ctx: TContext,
|
|
351
|
+
) => Promise<void>;
|
|
352
|
+
|
|
353
|
+
export interface TelegramThreadAwarePollingPorts<TContext, TOwner> {
|
|
354
|
+
startPolling: (
|
|
355
|
+
ctx: TContext,
|
|
356
|
+
options?: { forceFreshLeaderThread?: boolean },
|
|
357
|
+
) => Promise<void>;
|
|
358
|
+
stopPolling: () => Promise<void>;
|
|
359
|
+
registerFollowerWithOwner: (
|
|
360
|
+
ctx: TContext,
|
|
361
|
+
owner: TOwner,
|
|
362
|
+
) => Promise<boolean | undefined>;
|
|
363
|
+
stopFollowerRegistration: () => void;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export interface TelegramThreadAwarePollingDeps<
|
|
367
|
+
TContext,
|
|
368
|
+
TOwner,
|
|
369
|
+
> extends TelegramStartupThreadCapabilityProbeDeps {
|
|
370
|
+
isBusConfigured: () => boolean;
|
|
371
|
+
isBusRuntimeEnabled: () => boolean;
|
|
372
|
+
isTopicModeUnavailableError: (error: unknown) => boolean;
|
|
373
|
+
getPollingStartedWithTelegramBus: () => boolean;
|
|
374
|
+
setPollingStartedWithTelegramBus: (started: boolean) => void;
|
|
375
|
+
setForceFreshLeaderThreadOnNextStart: (forceFresh: boolean) => void;
|
|
376
|
+
startClassicPolling: (ctx: TContext) => MaybePromise<void>;
|
|
377
|
+
stopClassicPolling: () => Promise<void>;
|
|
378
|
+
startBusLeaderPolling: (ctx: TContext) => Promise<void>;
|
|
379
|
+
stopBusLeaderPolling: () => Promise<void>;
|
|
380
|
+
startLeaderHealth: () => void;
|
|
381
|
+
stopLeaderHealth: () => void;
|
|
382
|
+
registerFollowerWithLeader: (
|
|
383
|
+
ctx: TContext,
|
|
384
|
+
owner: TOwner,
|
|
385
|
+
) => Promise<boolean | undefined>;
|
|
386
|
+
stopFollowerRegistration: () => void;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export async function readTelegramThreadCapability(
|
|
390
|
+
deps: TelegramThreadCapabilityReaderDeps,
|
|
391
|
+
): Promise<boolean | undefined> {
|
|
392
|
+
const bot = await deps.callApi<{ has_topics_enabled?: boolean }>("getMe", {});
|
|
393
|
+
if (bot.has_topics_enabled === true) return true;
|
|
394
|
+
if (bot.has_topics_enabled === false) return false;
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export async function probeTelegramStartupThreadCapability(
|
|
399
|
+
deps: TelegramStartupThreadCapabilityProbeDeps,
|
|
400
|
+
): Promise<boolean | undefined> {
|
|
401
|
+
const threadModeEnabled = await readTelegramThreadCapability(deps);
|
|
402
|
+
const nowMs = (deps.getNowMs ?? Date.now)();
|
|
403
|
+
if (threadModeEnabled === false) {
|
|
404
|
+
deps.topicTargetStore.setBotState({
|
|
405
|
+
threadMode: "disabled",
|
|
406
|
+
updatedAtMs: nowMs,
|
|
407
|
+
lastReconcileAction: "startup-bot-topics-disabled",
|
|
408
|
+
});
|
|
409
|
+
await deps.topicTargetStore.persist();
|
|
410
|
+
deps.recordEvent("bus", "Telegram Threaded Mode unavailable on startup", {
|
|
411
|
+
phase: "startup-bot-topics-disabled",
|
|
412
|
+
});
|
|
413
|
+
deps.setTopicModeUnavailable(true);
|
|
414
|
+
return threadModeEnabled;
|
|
415
|
+
}
|
|
416
|
+
if (threadModeEnabled === true) {
|
|
417
|
+
deps.topicTargetStore.setBotState({
|
|
418
|
+
...deps.topicTargetStore.getBotState(),
|
|
419
|
+
threadMode: "enabled",
|
|
420
|
+
updatedAtMs: nowMs,
|
|
421
|
+
lastReconcileAction: "startup-bot-topics-enabled",
|
|
422
|
+
});
|
|
423
|
+
await deps.topicTargetStore.persist();
|
|
424
|
+
deps.setTopicModeUnavailable(false);
|
|
425
|
+
}
|
|
426
|
+
return threadModeEnabled;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function hasTelegramThreadCapabilityBindings(
|
|
430
|
+
store: TelegramThreadCapabilityStore,
|
|
431
|
+
): boolean {
|
|
432
|
+
return (
|
|
433
|
+
store.list?.().some((record) => {
|
|
434
|
+
return (
|
|
435
|
+
typeof record.target?.chatId === "number" &&
|
|
436
|
+
typeof record.target.threadId === "number" &&
|
|
437
|
+
record.status !== "deleted" &&
|
|
438
|
+
record.status !== "offline" &&
|
|
439
|
+
record.status !== "stale"
|
|
440
|
+
);
|
|
441
|
+
}) ?? false
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export async function applyTelegramThreadCapability<TContext>(
|
|
446
|
+
ctx: TContext,
|
|
447
|
+
threadModeEnabled: boolean,
|
|
448
|
+
phase: string,
|
|
449
|
+
deps: TelegramThreadCapabilityRuntimeDeps<TContext>,
|
|
450
|
+
): Promise<void> {
|
|
451
|
+
await deps.topicTargetStore.load();
|
|
452
|
+
if (!deps.isBusConfigured()) return;
|
|
453
|
+
const nowMs = (deps.getNowMs ?? Date.now)();
|
|
454
|
+
if (!threadModeEnabled) {
|
|
455
|
+
if (
|
|
456
|
+
hasTelegramThreadCapabilityBindings(deps.topicTargetStore) &&
|
|
457
|
+
!phase.endsWith("-confirmed")
|
|
458
|
+
) {
|
|
459
|
+
deps.recordEvent("bus", "Telegram Threaded Mode probe deferred", {
|
|
460
|
+
phase,
|
|
461
|
+
reason: "active-thread-bindings-present",
|
|
462
|
+
});
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
deps.topicTargetStore.setBotState({
|
|
466
|
+
threadMode: "disabled",
|
|
467
|
+
updatedAtMs: nowMs,
|
|
468
|
+
lastReconcileAction: phase,
|
|
469
|
+
});
|
|
470
|
+
await deps.topicTargetStore.persist();
|
|
471
|
+
deps.setTopicModeUnavailable(true);
|
|
472
|
+
deps.stopFollowerRegistration();
|
|
473
|
+
if (deps.getPollingStartedWithTelegramBus()) {
|
|
474
|
+
deps.stopLeaderHealth();
|
|
475
|
+
await deps.stopBusPolling();
|
|
476
|
+
deps.setPollingStartedWithTelegramBus(false);
|
|
477
|
+
await deps.startClassicPolling(ctx);
|
|
478
|
+
}
|
|
479
|
+
deps.updateStatus(ctx);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
deps.topicTargetStore.setBotState({
|
|
483
|
+
...deps.topicTargetStore.getBotState(),
|
|
484
|
+
threadMode: "enabled",
|
|
485
|
+
updatedAtMs: nowMs,
|
|
486
|
+
lastReconcileAction: phase,
|
|
487
|
+
});
|
|
488
|
+
await deps.topicTargetStore.persist();
|
|
489
|
+
deps.setTopicModeUnavailable(false);
|
|
490
|
+
if (!deps.getPollingStartedWithTelegramBus() && deps.ownsLock(ctx)) {
|
|
491
|
+
await deps.stopClassicPolling();
|
|
492
|
+
deps.setPollingStartedWithTelegramBus(true);
|
|
493
|
+
try {
|
|
494
|
+
await deps.startBusPolling(ctx);
|
|
495
|
+
deps.startLeaderHealth();
|
|
496
|
+
} catch (error) {
|
|
497
|
+
deps.setPollingStartedWithTelegramBus(false);
|
|
498
|
+
const threadModeUnavailable =
|
|
499
|
+
deps.isTopicModeUnavailableError?.(error) === true;
|
|
500
|
+
if (threadModeUnavailable) {
|
|
501
|
+
deps.topicTargetStore.setBotState({
|
|
502
|
+
threadMode: "disabled",
|
|
503
|
+
updatedAtMs: nowMs,
|
|
504
|
+
lastReconcileAction: `${phase}-unavailable`,
|
|
505
|
+
});
|
|
506
|
+
await deps.topicTargetStore.persist();
|
|
507
|
+
deps.setTopicModeUnavailable(true);
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
await deps.startClassicPolling(ctx);
|
|
511
|
+
} catch (classicError) {
|
|
512
|
+
deps.recordEvent("bus", classicError, {
|
|
513
|
+
phase: `${phase}-classic-restore`,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
deps.updateStatus(ctx);
|
|
517
|
+
if (threadModeUnavailable) return;
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
deps.updateStatus(ctx);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export function createTelegramThreadAwarePollingPorts<TContext, TOwner>(
|
|
525
|
+
deps: TelegramThreadAwarePollingDeps<TContext, TOwner>,
|
|
526
|
+
): TelegramThreadAwarePollingPorts<TContext, TOwner> {
|
|
527
|
+
const startPolling = async (
|
|
528
|
+
ctx: TContext,
|
|
529
|
+
options?: { forceFreshLeaderThread?: boolean },
|
|
530
|
+
): Promise<void> => {
|
|
531
|
+
if (deps.isBusConfigured()) {
|
|
532
|
+
await deps.topicTargetStore.load();
|
|
533
|
+
let startupThreadCapability: boolean | undefined;
|
|
534
|
+
try {
|
|
535
|
+
startupThreadCapability =
|
|
536
|
+
await probeTelegramStartupThreadCapability(deps);
|
|
537
|
+
} catch (error) {
|
|
538
|
+
deps.recordEvent("bus", error, { phase: "startup-thread-mode-probe" });
|
|
539
|
+
}
|
|
540
|
+
deps.setTopicModeUnavailable(startupThreadCapability !== true);
|
|
541
|
+
}
|
|
542
|
+
if (deps.isBusRuntimeEnabled()) {
|
|
543
|
+
deps.setTopicModeUnavailable(false);
|
|
544
|
+
try {
|
|
545
|
+
deps.setPollingStartedWithTelegramBus(true);
|
|
546
|
+
deps.setForceFreshLeaderThreadOnNextStart(
|
|
547
|
+
!!options?.forceFreshLeaderThread,
|
|
548
|
+
);
|
|
549
|
+
await deps.startBusLeaderPolling(ctx);
|
|
550
|
+
deps.startLeaderHealth();
|
|
551
|
+
return;
|
|
552
|
+
} catch (error) {
|
|
553
|
+
deps.setPollingStartedWithTelegramBus(false);
|
|
554
|
+
if (!deps.isTopicModeUnavailableError(error)) throw error;
|
|
555
|
+
deps.setTopicModeUnavailable(true);
|
|
556
|
+
await deps.topicTargetStore.load();
|
|
557
|
+
deps.topicTargetStore.setBotState({
|
|
558
|
+
threadMode: "disabled",
|
|
559
|
+
updatedAtMs: Date.now(),
|
|
560
|
+
lastReconcileAction: "thread-mode-unavailable",
|
|
561
|
+
});
|
|
562
|
+
await deps.topicTargetStore.persist();
|
|
563
|
+
deps.recordEvent("bus", error, { phase: "thread-mode-unavailable" });
|
|
564
|
+
} finally {
|
|
565
|
+
deps.setForceFreshLeaderThreadOnNextStart(false);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
deps.setPollingStartedWithTelegramBus(false);
|
|
569
|
+
await deps.startClassicPolling(ctx);
|
|
570
|
+
};
|
|
571
|
+
const stopPolling = async (): Promise<void> => {
|
|
572
|
+
if (deps.getPollingStartedWithTelegramBus()) {
|
|
573
|
+
deps.stopLeaderHealth();
|
|
574
|
+
await deps.stopBusLeaderPolling();
|
|
575
|
+
deps.setPollingStartedWithTelegramBus(false);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
await deps.stopClassicPolling();
|
|
579
|
+
};
|
|
580
|
+
const registerFollowerWithOwner = async (
|
|
581
|
+
ctx: TContext,
|
|
582
|
+
owner: TOwner,
|
|
583
|
+
): Promise<boolean | undefined> => {
|
|
584
|
+
await deps.topicTargetStore.load();
|
|
585
|
+
if (deps.topicTargetStore.getBotState().threadMode !== "enabled") {
|
|
586
|
+
return undefined;
|
|
587
|
+
}
|
|
588
|
+
if (!deps.isBusRuntimeEnabled()) return undefined;
|
|
589
|
+
return deps.registerFollowerWithLeader(ctx, owner);
|
|
590
|
+
};
|
|
591
|
+
return {
|
|
592
|
+
startPolling,
|
|
593
|
+
stopPolling,
|
|
594
|
+
registerFollowerWithOwner,
|
|
595
|
+
stopFollowerRegistration: deps.stopFollowerRegistration,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
export function createTelegramThreadTargetObservationHandler<TContext>(
|
|
600
|
+
deps: TelegramThreadCapabilityRuntimeDeps<TContext>,
|
|
601
|
+
): TelegramThreadTargetObservationHandler<TContext> {
|
|
602
|
+
let transitionPending = false;
|
|
603
|
+
return async (ctx) => {
|
|
604
|
+
if (transitionPending) return;
|
|
605
|
+
if (deps.topicTargetStore.getBotState().threadMode === "enabled") return;
|
|
606
|
+
transitionPending = true;
|
|
607
|
+
try {
|
|
608
|
+
await applyTelegramThreadCapability(
|
|
609
|
+
ctx,
|
|
610
|
+
true,
|
|
611
|
+
"thread-target-observed",
|
|
612
|
+
deps,
|
|
613
|
+
);
|
|
614
|
+
} catch (error) {
|
|
615
|
+
deps.recordEvent("bus", error, { phase: "thread-target-observed" });
|
|
616
|
+
} finally {
|
|
617
|
+
transitionPending = false;
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export function createTelegramThreadCapabilityMonitor<TContext>(
|
|
623
|
+
deps: TelegramThreadCapabilityRuntimeDeps<TContext>,
|
|
624
|
+
): TelegramThreadCapabilityMonitor<TContext> {
|
|
625
|
+
const intervalMs =
|
|
626
|
+
deps.intervalMs ?? TELEGRAM_THREAD_CAPABILITY_MONITOR_INTERVAL_MS;
|
|
627
|
+
let interval: ReturnType<typeof setInterval> | undefined;
|
|
628
|
+
let transitionPending = false;
|
|
629
|
+
let consecutiveDisabledProbes = 0;
|
|
630
|
+
const stop = (): void => {
|
|
631
|
+
if (!interval) return;
|
|
632
|
+
clearInterval(interval);
|
|
633
|
+
interval = undefined;
|
|
634
|
+
};
|
|
635
|
+
const check = (ctx: TContext): void => {
|
|
636
|
+
if (transitionPending) return;
|
|
637
|
+
transitionPending = true;
|
|
638
|
+
void readTelegramThreadCapability(deps)
|
|
639
|
+
.then(async (threadModeEnabled) => {
|
|
640
|
+
if (threadModeEnabled === undefined) {
|
|
641
|
+
if (
|
|
642
|
+
deps.topicTargetStore.getBotState().threadMode !== "enabled" &&
|
|
643
|
+
!deps.getPollingStartedWithTelegramBus() &&
|
|
644
|
+
deps.ownsLock(ctx)
|
|
645
|
+
) {
|
|
646
|
+
await applyTelegramThreadCapability(
|
|
647
|
+
ctx,
|
|
648
|
+
true,
|
|
649
|
+
"capability-monitor-retry",
|
|
650
|
+
deps,
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (threadModeEnabled) consecutiveDisabledProbes = 0;
|
|
656
|
+
const current = deps.topicTargetStore.getBotState().threadMode;
|
|
657
|
+
if (threadModeEnabled && current === "enabled") return;
|
|
658
|
+
if (!threadModeEnabled && current === "disabled") return;
|
|
659
|
+
if (
|
|
660
|
+
!threadModeEnabled &&
|
|
661
|
+
hasTelegramThreadCapabilityBindings(deps.topicTargetStore)
|
|
662
|
+
) {
|
|
663
|
+
consecutiveDisabledProbes += 1;
|
|
664
|
+
if (
|
|
665
|
+
consecutiveDisabledProbes <
|
|
666
|
+
TELEGRAM_THREAD_CAPABILITY_DISABLED_CONFIRMATION_PROBES
|
|
667
|
+
) {
|
|
668
|
+
deps.recordEvent("bus", "Telegram Threaded Mode probe deferred", {
|
|
669
|
+
phase: "capability-monitor-disabled",
|
|
670
|
+
reason: "active-thread-bindings-present",
|
|
671
|
+
consecutiveDisabledProbes,
|
|
672
|
+
});
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
await applyTelegramThreadCapability(
|
|
677
|
+
ctx,
|
|
678
|
+
threadModeEnabled,
|
|
679
|
+
threadModeEnabled
|
|
680
|
+
? "capability-monitor-enabled"
|
|
681
|
+
: consecutiveDisabledProbes >=
|
|
682
|
+
TELEGRAM_THREAD_CAPABILITY_DISABLED_CONFIRMATION_PROBES
|
|
683
|
+
? "capability-monitor-disabled-confirmed"
|
|
684
|
+
: "capability-monitor-disabled",
|
|
685
|
+
deps,
|
|
686
|
+
);
|
|
687
|
+
})
|
|
688
|
+
.catch((error) => {
|
|
689
|
+
deps.recordEvent("bus", error, { phase: "capability-monitor" });
|
|
690
|
+
})
|
|
691
|
+
.finally(() => {
|
|
692
|
+
transitionPending = false;
|
|
693
|
+
});
|
|
694
|
+
};
|
|
695
|
+
return {
|
|
696
|
+
start(ctx) {
|
|
697
|
+
stop();
|
|
698
|
+
if (!deps.isBusConfigured()) return;
|
|
699
|
+
interval = setInterval(() => {
|
|
700
|
+
check(ctx);
|
|
701
|
+
}, intervalMs);
|
|
702
|
+
interval.unref?.();
|
|
703
|
+
},
|
|
704
|
+
stop,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
261
708
|
export interface TelegramPollLoopDeps<
|
|
262
709
|
TUpdate extends TelegramUpdate,
|
|
263
710
|
TContext = unknown,
|
|
@@ -357,6 +804,12 @@ function getTelegramPollingErrorMessage(error: unknown): string {
|
|
|
357
804
|
return error instanceof Error ? error.message : String(error);
|
|
358
805
|
}
|
|
359
806
|
|
|
807
|
+
export function isTelegramGetUpdatesConflictError(error: unknown): boolean {
|
|
808
|
+
return getTelegramPollingErrorMessage(error).includes(
|
|
809
|
+
"Conflict: terminated by other getUpdates request",
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
|
|
360
813
|
export async function runTelegramPollLoop<
|
|
361
814
|
TUpdate extends TelegramUpdate,
|
|
362
815
|
TContext = unknown,
|
|
@@ -382,15 +835,20 @@ export async function runTelegramPollLoop<
|
|
|
382
835
|
// ignore
|
|
383
836
|
}
|
|
384
837
|
}
|
|
385
|
-
const maxUpdateFailures = Math.max(
|
|
838
|
+
const maxUpdateFailures = Math.max(
|
|
839
|
+
1,
|
|
840
|
+
deps.maxUpdateFailures ?? TELEGRAM_POLLING_DEFAULT_MAX_UPDATE_FAILURES,
|
|
841
|
+
);
|
|
386
842
|
const updateFailures = new Map<number, number>();
|
|
387
843
|
let handledUpdateFailureRethrown = false;
|
|
844
|
+
let consecutiveGetUpdatesConflicts = 0;
|
|
388
845
|
while (!deps.signal.aborted) {
|
|
389
846
|
try {
|
|
390
847
|
const updates = await deps.getUpdates(
|
|
391
848
|
buildTelegramLongPollRequest(deps.config.lastUpdateId),
|
|
392
849
|
deps.signal,
|
|
393
850
|
);
|
|
851
|
+
consecutiveGetUpdatesConflicts = 0;
|
|
394
852
|
for (const update of updates) {
|
|
395
853
|
try {
|
|
396
854
|
await deps.handleUpdate(update, deps.ctx);
|
|
@@ -425,8 +883,20 @@ export async function runTelegramPollLoop<
|
|
|
425
883
|
} else {
|
|
426
884
|
deps.recordRuntimeEvent?.("polling", error, { phase: "loop" });
|
|
427
885
|
}
|
|
886
|
+
if (isTelegramGetUpdatesConflictError(error)) {
|
|
887
|
+
consecutiveGetUpdatesConflicts += 1;
|
|
888
|
+
await deps.sleep(
|
|
889
|
+
consecutiveGetUpdatesConflicts <
|
|
890
|
+
TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_LIMIT
|
|
891
|
+
? TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_MS
|
|
892
|
+
: TELEGRAM_GET_UPDATES_CONFLICT_SLOW_RETRY_MS,
|
|
893
|
+
deps.signal,
|
|
894
|
+
);
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
consecutiveGetUpdatesConflicts = 0;
|
|
428
898
|
deps.onErrorStatus(getTelegramPollingErrorMessage(error));
|
|
429
|
-
await deps.sleep(
|
|
899
|
+
await deps.sleep(TELEGRAM_POLLING_RETRY_MS, deps.signal);
|
|
430
900
|
if (deps.signal.aborted) return;
|
|
431
901
|
deps.onStatusReset();
|
|
432
902
|
}
|