@llblab/pi-telegram 0.18.4 → 0.18.6
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 +8 -5
- package/BACKLOG.md +13 -34
- package/CHANGELOG.md +24 -1
- package/README.md +1 -1
- package/docs/multi-instance-bus.md +26 -5
- package/index.ts +56 -4
- package/lib/bindings.ts +3 -14
- package/lib/bus-follower.ts +66 -46
- package/lib/bus-leader.ts +171 -47
- package/lib/bus-transport.ts +227 -0
- package/lib/bus.ts +214 -46
- package/lib/polling.ts +58 -5
- package/lib/preview.ts +20 -39
- package/lib/routing.ts +5 -11
- package/lib/runtime-log.ts +19 -1
- package/lib/runtime.ts +23 -7
- package/lib/status.ts +52 -2
- package/lib/threads.ts +56 -2
- package/lib/updates.ts +43 -2
- package/package.json +1 -1
package/lib/polling.ts
CHANGED
|
@@ -20,7 +20,7 @@ const TELEGRAM_INITIAL_SYNC_LIMIT = 1;
|
|
|
20
20
|
const TELEGRAM_INITIAL_SYNC_TIMEOUT_SECONDS = 0;
|
|
21
21
|
const TELEGRAM_LONG_POLL_LIMIT = 10;
|
|
22
22
|
const TELEGRAM_LONG_POLL_TIMEOUT_SECONDS = 30;
|
|
23
|
-
const TELEGRAM_THREAD_CAPABILITY_MONITOR_INTERVAL_MS =
|
|
23
|
+
const TELEGRAM_THREAD_CAPABILITY_MONITOR_INTERVAL_MS = 2_500;
|
|
24
24
|
const TELEGRAM_THREAD_CAPABILITY_DISABLED_CONFIRMATION_PROBES = 2;
|
|
25
25
|
const TELEGRAM_POLLING_DEFAULT_MAX_UPDATE_FAILURES = 3;
|
|
26
26
|
const TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_LIMIT = 3;
|
|
@@ -426,6 +426,12 @@ export async function probeTelegramStartupThreadCapability(
|
|
|
426
426
|
return threadModeEnabled;
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
+
function hasTelegramClassicRestoreFailure(
|
|
430
|
+
state: TelegramThreadCapabilityState,
|
|
431
|
+
): boolean {
|
|
432
|
+
return state.lastReconcileAction?.endsWith("-classic-restore-failed") ?? false;
|
|
433
|
+
}
|
|
434
|
+
|
|
429
435
|
function hasTelegramThreadCapabilityBindings(
|
|
430
436
|
store: TelegramThreadCapabilityStore,
|
|
431
437
|
): boolean {
|
|
@@ -451,6 +457,7 @@ export async function applyTelegramThreadCapability<TContext>(
|
|
|
451
457
|
await deps.topicTargetStore.load();
|
|
452
458
|
if (!deps.isBusConfigured()) return;
|
|
453
459
|
const nowMs = (deps.getNowMs ?? Date.now)();
|
|
460
|
+
const previousBotState = deps.topicTargetStore.getBotState();
|
|
454
461
|
if (!threadModeEnabled) {
|
|
455
462
|
if (
|
|
456
463
|
hasTelegramThreadCapabilityBindings(deps.topicTargetStore) &&
|
|
@@ -470,11 +477,26 @@ export async function applyTelegramThreadCapability<TContext>(
|
|
|
470
477
|
await deps.topicTargetStore.persist();
|
|
471
478
|
deps.setTopicModeUnavailable(true);
|
|
472
479
|
deps.stopFollowerRegistration();
|
|
473
|
-
if (
|
|
480
|
+
if (
|
|
481
|
+
deps.getPollingStartedWithTelegramBus() ||
|
|
482
|
+
hasTelegramClassicRestoreFailure(previousBotState)
|
|
483
|
+
) {
|
|
474
484
|
deps.stopLeaderHealth();
|
|
475
485
|
await deps.stopBusPolling();
|
|
476
486
|
deps.setPollingStartedWithTelegramBus(false);
|
|
477
|
-
|
|
487
|
+
try {
|
|
488
|
+
await deps.startClassicPolling(ctx);
|
|
489
|
+
} catch (classicError) {
|
|
490
|
+
deps.topicTargetStore.setBotState({
|
|
491
|
+
threadMode: "disabled",
|
|
492
|
+
updatedAtMs: (deps.getNowMs ?? Date.now)(),
|
|
493
|
+
lastReconcileAction: `${phase}-classic-restore-failed`,
|
|
494
|
+
});
|
|
495
|
+
await deps.topicTargetStore.persist();
|
|
496
|
+
deps.recordEvent("bus", classicError, {
|
|
497
|
+
phase: `${phase}-classic-restore`,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
478
500
|
}
|
|
479
501
|
deps.updateStatus(ctx);
|
|
480
502
|
return;
|
|
@@ -509,6 +531,12 @@ export async function applyTelegramThreadCapability<TContext>(
|
|
|
509
531
|
try {
|
|
510
532
|
await deps.startClassicPolling(ctx);
|
|
511
533
|
} catch (classicError) {
|
|
534
|
+
deps.topicTargetStore.setBotState({
|
|
535
|
+
threadMode: "disabled",
|
|
536
|
+
updatedAtMs: (deps.getNowMs ?? Date.now)(),
|
|
537
|
+
lastReconcileAction: `${phase}-classic-restore-failed`,
|
|
538
|
+
});
|
|
539
|
+
await deps.topicTargetStore.persist();
|
|
512
540
|
deps.recordEvent("bus", classicError, {
|
|
513
541
|
phase: `${phase}-classic-restore`,
|
|
514
542
|
});
|
|
@@ -583,6 +611,19 @@ export function createTelegramThreadAwarePollingPorts<TContext, TOwner>(
|
|
|
583
611
|
): Promise<boolean | undefined> => {
|
|
584
612
|
await deps.topicTargetStore.load();
|
|
585
613
|
if (deps.topicTargetStore.getBotState().threadMode !== "enabled") {
|
|
614
|
+
if (hasTelegramThreadCapabilityBindings(deps.topicTargetStore)) {
|
|
615
|
+
deps.recordEvent(
|
|
616
|
+
"bus",
|
|
617
|
+
"Telegram Threaded Mode disabled; follower takeover blocked",
|
|
618
|
+
{
|
|
619
|
+
phase: "follower-register-thread-mode-disabled",
|
|
620
|
+
reason: "active-thread-bindings-present",
|
|
621
|
+
},
|
|
622
|
+
);
|
|
623
|
+
throw new Error(
|
|
624
|
+
"Telegram Threaded Mode is disabled; the current leader remains the classic polling owner.",
|
|
625
|
+
);
|
|
626
|
+
}
|
|
586
627
|
return undefined;
|
|
587
628
|
}
|
|
588
629
|
if (!deps.isBusRuntimeEnabled()) return undefined;
|
|
@@ -653,9 +694,21 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
|
|
|
653
694
|
return;
|
|
654
695
|
}
|
|
655
696
|
if (threadModeEnabled) consecutiveDisabledProbes = 0;
|
|
656
|
-
const
|
|
697
|
+
const botState = deps.topicTargetStore.getBotState();
|
|
698
|
+
const current = botState.threadMode;
|
|
657
699
|
if (threadModeEnabled && current === "enabled") return;
|
|
658
|
-
if (!threadModeEnabled && current === "disabled")
|
|
700
|
+
if (!threadModeEnabled && current === "disabled") {
|
|
701
|
+
if (!deps.ownsLock(ctx) || !hasTelegramClassicRestoreFailure(botState)) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
await applyTelegramThreadCapability(
|
|
705
|
+
ctx,
|
|
706
|
+
false,
|
|
707
|
+
"capability-monitor-disabled-confirmed",
|
|
708
|
+
deps,
|
|
709
|
+
);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
659
712
|
if (
|
|
660
713
|
!threadModeEnabled &&
|
|
661
714
|
hasTelegramThreadCapabilityBindings(deps.topicTargetStore)
|
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 =
|
|
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
|
|
338
|
-
|
|
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)
|
|
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 (
|
|
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/routing.ts
CHANGED
|
@@ -217,21 +217,13 @@ function formatTelegramAllTabMenuChooserText(command: string): string {
|
|
|
217
217
|
function buildTelegramUnboundRerouteChooserMarkup(
|
|
218
218
|
rerouteId: string,
|
|
219
219
|
records: readonly Threads.TelegramTopicTargetRecord[],
|
|
220
|
-
|
|
220
|
+
_options: {
|
|
221
221
|
currentLeaderProfileKey?: string;
|
|
222
222
|
currentInstanceId?: string;
|
|
223
223
|
} = {},
|
|
224
224
|
): Menu.TelegramReplyMarkup {
|
|
225
225
|
const activeRecords = records.filter((record) => record.status === "active");
|
|
226
|
-
const
|
|
227
|
-
(record) =>
|
|
228
|
-
typeof record.rerouteConfirmedAtMs === "number" &&
|
|
229
|
-
isCurrentLeaderTopicRecord(
|
|
230
|
-
record,
|
|
231
|
-
options.currentLeaderProfileKey,
|
|
232
|
-
options.currentInstanceId,
|
|
233
|
-
),
|
|
234
|
-
);
|
|
226
|
+
const canRestoreAnyLiveThread = activeRecords.length > 0;
|
|
235
227
|
const rows = activeRecords.map((record) => [
|
|
236
228
|
{
|
|
237
229
|
text: getTelegramRouteThreadButtonLabel(record),
|
|
@@ -242,7 +234,7 @@ function buildTelegramUnboundRerouteChooserMarkup(
|
|
|
242
234
|
},
|
|
243
235
|
]);
|
|
244
236
|
return {
|
|
245
|
-
inline_keyboard:
|
|
237
|
+
inline_keyboard: canRestoreAnyLiveThread
|
|
246
238
|
? [
|
|
247
239
|
...rows,
|
|
248
240
|
[
|
|
@@ -414,6 +406,7 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
414
406
|
getCurrentInstanceId?: () => string | undefined;
|
|
415
407
|
getMessageOwnership?: Updates.TelegramMessageOwnershipLookup;
|
|
416
408
|
getTargetOwnership?: Updates.TelegramTargetOwnershipLookup;
|
|
409
|
+
recordMessageOwnership?: Updates.TelegramMessageOwnershipRecorder;
|
|
417
410
|
getLiveThreadTargets?: () => Queue.TelegramQueueTarget[];
|
|
418
411
|
getLocalThreadLabelForTarget?: (
|
|
419
412
|
target: Queue.TelegramQueueTarget,
|
|
@@ -1861,6 +1854,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1861
1854
|
getCurrentInstanceId: deps.getCurrentInstanceId,
|
|
1862
1855
|
getMessageOwnership: deps.getMessageOwnership,
|
|
1863
1856
|
getTargetOwnership: deps.getTargetOwnership,
|
|
1857
|
+
recordMessageOwnership: deps.recordMessageOwnership,
|
|
1864
1858
|
handleTelegramTopicLifecycleUpdate,
|
|
1865
1859
|
foreignOwnedUpdateForwarder: deps.foreignOwnedUpdateForwarder,
|
|
1866
1860
|
setAllowedUserId: deps.configStore.setAllowedUserId,
|
package/lib/runtime-log.ts
CHANGED
|
@@ -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 {
|
|
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/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 =
|
|
7
|
+
const TELEGRAM_TYPING_ACTION_INTERVAL_MS = 2500;
|
|
8
8
|
const TELEGRAM_TYPING_IDLE_DRAIN_MAX_MS = 250;
|
|
9
9
|
|
|
10
10
|
export interface TelegramRuntimeQueueCounters {
|
|
@@ -398,13 +398,9 @@ export function createTelegramTypingLoopStarter<TContext>(
|
|
|
398
398
|
chatId: chatId ?? deps.getDefaultChatId(),
|
|
399
399
|
target: options?.target,
|
|
400
400
|
intervalMs: deps.intervalMs ?? TELEGRAM_TYPING_ACTION_INTERVAL_MS,
|
|
401
|
-
sendTypingAction: async (targetChatId) => {
|
|
401
|
+
sendTypingAction: async (targetChatId, actionOptions) => {
|
|
402
402
|
try {
|
|
403
|
-
|
|
404
|
-
await deps.sendTypingAction(targetChatId, threadParams);
|
|
405
|
-
if (threadParams?.message_thread_id !== undefined) {
|
|
406
|
-
await deps.sendAggregateTypingAction?.(targetChatId);
|
|
407
|
-
}
|
|
403
|
+
await deps.sendTypingAction(targetChatId, actionOptions);
|
|
408
404
|
} catch (error) {
|
|
409
405
|
const message =
|
|
410
406
|
error instanceof Error ? error.message : String(error);
|
|
@@ -419,6 +415,26 @@ export function createTelegramTypingLoopStarter<TContext>(
|
|
|
419
415
|
});
|
|
420
416
|
}
|
|
421
417
|
},
|
|
418
|
+
sendAggregateTypingAction: deps.sendAggregateTypingAction
|
|
419
|
+
? async (targetChatId) => {
|
|
420
|
+
try {
|
|
421
|
+
await deps.sendAggregateTypingAction?.(targetChatId);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
const message =
|
|
424
|
+
error instanceof Error ? error.message : String(error);
|
|
425
|
+
updateTelegramRuntimeStatusSafely(deps.updateStatus, ctx, {
|
|
426
|
+
error: message,
|
|
427
|
+
category: "typing",
|
|
428
|
+
phase: "status-update",
|
|
429
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
430
|
+
});
|
|
431
|
+
deps.recordRuntimeEvent?.("typing", error, {
|
|
432
|
+
chatId: targetChatId,
|
|
433
|
+
aggregate: true,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
: undefined,
|
|
422
438
|
});
|
|
423
439
|
};
|
|
424
440
|
}
|
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 ?? [],
|
|
@@ -756,8 +771,6 @@ export function buildTelegramStatusBarText(
|
|
|
756
771
|
return `${label} ${theme.fg("warning", "electing")}${queued}`;
|
|
757
772
|
if (!state.pollingActive && state.busRole !== "follower")
|
|
758
773
|
return `${label} ${theme.fg("muted", "disconnected")}${queued}`;
|
|
759
|
-
if (state.busRole === "follower")
|
|
760
|
-
return `${label} ${theme.fg("success", "follower")}${queued}`;
|
|
761
774
|
if (state.compactionInProgress) {
|
|
762
775
|
return `${label} ${theme.fg("warning", "compacting")}${queued}`;
|
|
763
776
|
}
|
|
@@ -769,6 +782,8 @@ export function buildTelegramStatusBarText(
|
|
|
769
782
|
processingStatus === "active" ? "warning" : "accent";
|
|
770
783
|
return `${label} ${theme.fg(processingToken, processingStatus)}${queued}`;
|
|
771
784
|
}
|
|
785
|
+
if (state.busRole === "follower")
|
|
786
|
+
return `${label} ${theme.fg("success", "follower")}${queued}`;
|
|
772
787
|
if (state.busRole === "leader")
|
|
773
788
|
return `${label} ${theme.fg("success", "leader")}`;
|
|
774
789
|
return `${label} ${theme.fg("success", "connected")}`;
|
|
@@ -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/lib/threads.ts
CHANGED
|
@@ -1438,7 +1438,7 @@ function getTelegramThreadNameEntropyIndex(
|
|
|
1438
1438
|
|
|
1439
1439
|
export function getTelegramTopicThreadNameValidationError(
|
|
1440
1440
|
threadName: string,
|
|
1441
|
-
|
|
1441
|
+
_slot: string | undefined,
|
|
1442
1442
|
): string | undefined {
|
|
1443
1443
|
const identity = getTelegramTopicIdentityName(threadName);
|
|
1444
1444
|
const reasons: string[] = [];
|
|
@@ -1501,6 +1501,60 @@ function asInteger(value: unknown): number | undefined {
|
|
|
1501
1501
|
return Number.isInteger(parsed) ? parsed : undefined;
|
|
1502
1502
|
}
|
|
1503
1503
|
|
|
1504
|
+
export interface TelegramPromoteFollowerBindingToLeaderDeps {
|
|
1505
|
+
store: TelegramTopicTargetStore;
|
|
1506
|
+
instanceId: string;
|
|
1507
|
+
cwd?: string;
|
|
1508
|
+
target?: TelegramTarget;
|
|
1509
|
+
slot?: string;
|
|
1510
|
+
threadName?: string;
|
|
1511
|
+
nowMs?: number;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
export async function promoteTelegramFollowerBindingToLeader(
|
|
1515
|
+
deps: TelegramPromoteFollowerBindingToLeaderDeps,
|
|
1516
|
+
): Promise<TelegramTopicTargetRecord | undefined> {
|
|
1517
|
+
const target = deps.target;
|
|
1518
|
+
if (typeof target?.threadId !== "number") return undefined;
|
|
1519
|
+
await deps.store.load();
|
|
1520
|
+
const nowMs = deps.nowMs ?? Date.now();
|
|
1521
|
+
const existing = deps.store
|
|
1522
|
+
.list()
|
|
1523
|
+
.find(
|
|
1524
|
+
(record) =>
|
|
1525
|
+
record.target.chatId === target.chatId &&
|
|
1526
|
+
record.target.threadId === target.threadId,
|
|
1527
|
+
);
|
|
1528
|
+
const owner: TelegramThreadOwner = {
|
|
1529
|
+
kind: "leader",
|
|
1530
|
+
cwd: deps.cwd,
|
|
1531
|
+
instanceId: deps.instanceId,
|
|
1532
|
+
};
|
|
1533
|
+
const record = deps.store.upsert({
|
|
1534
|
+
profileKey: getTelegramThreadOwnerKey(owner),
|
|
1535
|
+
owner,
|
|
1536
|
+
target: { chatId: target.chatId, threadId: target.threadId },
|
|
1537
|
+
status: "active",
|
|
1538
|
+
createdAtMs: existing?.createdAtMs ?? nowMs,
|
|
1539
|
+
updatedAtMs: nowMs,
|
|
1540
|
+
...(existing?.threadName ?? deps.threadName
|
|
1541
|
+
? { threadName: existing?.threadName ?? deps.threadName }
|
|
1542
|
+
: {}),
|
|
1543
|
+
instanceId: deps.instanceId,
|
|
1544
|
+
...(existing?.slot ?? deps.slot ? { slot: existing?.slot ?? deps.slot } : {}),
|
|
1545
|
+
...(existing?.syncStatus ? { syncStatus: existing.syncStatus } : {}),
|
|
1546
|
+
...(existing?.lastSyncObservedAtMs !== undefined
|
|
1547
|
+
? { lastSyncObservedAtMs: existing.lastSyncObservedAtMs }
|
|
1548
|
+
: {}),
|
|
1549
|
+
lastReconcileAction: "follower-promoted-to-leader",
|
|
1550
|
+
...(existing?.rerouteConfirmedAtMs !== undefined
|
|
1551
|
+
? { rerouteConfirmedAtMs: existing.rerouteConfirmedAtMs }
|
|
1552
|
+
: {}),
|
|
1553
|
+
});
|
|
1554
|
+
await deps.store.persist();
|
|
1555
|
+
return record;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1504
1558
|
export interface TelegramOwnTopicProvisionDeps {
|
|
1505
1559
|
getAllowedUserId: () => number | undefined;
|
|
1506
1560
|
instanceId: string;
|
|
@@ -2077,7 +2131,7 @@ export function createTelegramTopicTargetProvisioner(
|
|
|
2077
2131
|
existing.threadName ?? identityThreadName ?? bakedThreadName,
|
|
2078
2132
|
instanceId: request.instanceId,
|
|
2079
2133
|
slot,
|
|
2080
|
-
owner:
|
|
2134
|
+
owner: request.owner ?? existing.owner,
|
|
2081
2135
|
lastError: undefined,
|
|
2082
2136
|
});
|
|
2083
2137
|
return { target: record.target, reused: true, record };
|