@llblab/pi-telegram 0.27.11 → 0.28.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 +150 -258
- package/BACKLOG.md +1 -169
- package/CHANGELOG.md +396 -441
- package/README.md +9 -6
- package/api/updates.ts +5 -0
- package/docs/architecture.md +71 -26
- package/docs/multi-instance-bus.md +23 -5
- package/docs/public-api.md +7 -6
- package/docs/ui-style.md +6 -6
- package/docs/updates.md +29 -11
- package/index.ts +356 -246
- package/lib/activity-verbosity.ts +26 -0
- package/lib/bindings.ts +240 -2
- package/lib/bus-follower.ts +436 -238
- package/lib/bus-leader.ts +395 -42
- package/lib/bus.ts +994 -153
- package/lib/commands.ts +184 -30
- package/lib/config.ts +23 -2
- package/lib/journal.ts +3140 -0
- package/lib/lifecycle.ts +4 -0
- package/lib/locks.ts +21 -21
- package/lib/media.ts +71 -32
- package/lib/menu-queue.ts +31 -17
- package/lib/menu.ts +5 -3
- package/lib/model.ts +51 -24
- package/lib/ownership.ts +42 -7
- package/lib/paths.ts +35 -0
- package/lib/polling.ts +591 -106
- package/lib/prompts.ts +17 -0
- package/lib/queue.ts +732 -143
- package/lib/routing.ts +291 -64
- package/lib/runtime.ts +26 -10
- package/lib/status.ts +257 -18
- package/lib/sync.ts +131 -5
- package/lib/telegram-api.ts +41 -11
- package/lib/text-groups.ts +75 -35
- package/lib/threads.ts +112 -4
- package/lib/turns.ts +79 -14
- package/lib/updates.ts +3771 -223
- package/package.json +3 -3
- package/scripts/check-downgrade.mjs +435 -0
package/lib/sync.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Owns pure contracts for deciding when local Telegram mirror state should be refreshed without querying Telegram on every action
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type
|
|
7
|
+
import { getTelegramTargetKey, type TelegramTarget } from "./target.ts";
|
|
8
8
|
import * as ThreadReconciler from "./thread-reconciler.ts";
|
|
9
9
|
import {
|
|
10
10
|
getTelegramTargetFromApiBody,
|
|
@@ -70,6 +70,7 @@ export interface TelegramTopicLifecycleSyncDeps {
|
|
|
70
70
|
recordThreadReconciliationPlan?: (
|
|
71
71
|
plan: ThreadReconciler.ThreadReconciliationPlan,
|
|
72
72
|
) => void;
|
|
73
|
+
assertExecutionCurrent?: (message: unknown) => void;
|
|
73
74
|
recordEvent?: (
|
|
74
75
|
category: string,
|
|
75
76
|
message: unknown,
|
|
@@ -162,6 +163,52 @@ export function markTelegramConfigSyncChange<
|
|
|
162
163
|
return nextState;
|
|
163
164
|
}
|
|
164
165
|
|
|
166
|
+
export interface TelegramSessionRestartThreadCleanupDeps<
|
|
167
|
+
TSyncState extends TelegramSyncState,
|
|
168
|
+
> extends Omit<TelegramManualThreadDisconnectDeps<TSyncState>, "stopPolling"> {
|
|
169
|
+
suspendPolling: () => Promise<void>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function createTelegramSessionRestartThreadCleanupHandler<
|
|
173
|
+
TSyncState extends TelegramSyncState,
|
|
174
|
+
>(
|
|
175
|
+
deps: TelegramSessionRestartThreadCleanupDeps<TSyncState>,
|
|
176
|
+
): () => Promise<string> {
|
|
177
|
+
return createTelegramManualThreadDisconnectHandler({
|
|
178
|
+
...deps,
|
|
179
|
+
async stopPolling() {
|
|
180
|
+
await deps.suspendPolling();
|
|
181
|
+
return "Telegram bridge suspended for session restart.";
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface TelegramThreadDisconnectAssembly {
|
|
187
|
+
disconnect: () => Promise<string>;
|
|
188
|
+
cleanupForSessionRestart: () => Promise<string>;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function createTelegramThreadDisconnectAssembly<
|
|
192
|
+
TSyncState extends TelegramSyncState,
|
|
193
|
+
>(
|
|
194
|
+
deps: Omit<TelegramManualThreadDisconnectDeps<TSyncState>, "stopPolling"> & {
|
|
195
|
+
stopPolling: () => Promise<string>;
|
|
196
|
+
suspendPolling: () => Promise<void>;
|
|
197
|
+
},
|
|
198
|
+
): TelegramThreadDisconnectAssembly {
|
|
199
|
+
return {
|
|
200
|
+
disconnect: createTelegramManualThreadDisconnectHandler({
|
|
201
|
+
...deps,
|
|
202
|
+
stopPolling: deps.stopPolling,
|
|
203
|
+
}),
|
|
204
|
+
cleanupForSessionRestart:
|
|
205
|
+
createTelegramSessionRestartThreadCleanupHandler({
|
|
206
|
+
...deps,
|
|
207
|
+
suspendPolling: deps.suspendPolling,
|
|
208
|
+
}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
165
212
|
export function createTelegramManualThreadDisconnectHandler<
|
|
166
213
|
TSyncState extends TelegramSyncState,
|
|
167
214
|
>(deps: TelegramManualThreadDisconnectDeps<TSyncState>): () => Promise<string> {
|
|
@@ -260,6 +307,8 @@ export function createTelegramLeaderHealthRuntime<
|
|
|
260
307
|
const intervalMs = deps.intervalMs ?? 60_000;
|
|
261
308
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
262
309
|
let interval: ReturnType<typeof setInterval> | undefined;
|
|
310
|
+
let generation = 0;
|
|
311
|
+
let tickPromise: Promise<void> | undefined;
|
|
263
312
|
|
|
264
313
|
const markFresh = (): void => {
|
|
265
314
|
let state = markTelegramSyncSliceFresh(
|
|
@@ -282,20 +331,67 @@ export function createTelegramLeaderHealthRuntime<
|
|
|
282
331
|
action: "leader-health-tick",
|
|
283
332
|
}) as TSyncState,
|
|
284
333
|
);
|
|
285
|
-
|
|
334
|
+
try {
|
|
335
|
+
deps.recordEvent("telegram", error, { phase: "leader-health-tick" });
|
|
336
|
+
} catch {
|
|
337
|
+
// Health diagnostics cannot create an unhandled timer rejection.
|
|
338
|
+
}
|
|
286
339
|
};
|
|
287
340
|
|
|
288
341
|
const stop = (): void => {
|
|
289
|
-
|
|
290
|
-
clearInterval(interval);
|
|
342
|
+
generation += 1;
|
|
343
|
+
if (interval) clearInterval(interval);
|
|
291
344
|
interval = undefined;
|
|
345
|
+
tickPromise = undefined;
|
|
346
|
+
};
|
|
347
|
+
const requestTick = (): Promise<void> => {
|
|
348
|
+
if (tickPromise) return tickPromise;
|
|
349
|
+
const expectedGeneration = generation;
|
|
350
|
+
let tracked: Promise<void>;
|
|
351
|
+
tracked = Promise.resolve()
|
|
352
|
+
.then(deps.callGetMe)
|
|
353
|
+
.then(
|
|
354
|
+
() => {
|
|
355
|
+
if (generation !== expectedGeneration) return;
|
|
356
|
+
try {
|
|
357
|
+
markFresh();
|
|
358
|
+
} catch (stateError) {
|
|
359
|
+
try {
|
|
360
|
+
deps.recordEvent("telegram", stateError, {
|
|
361
|
+
phase: "leader-health-state",
|
|
362
|
+
});
|
|
363
|
+
} catch {
|
|
364
|
+
// State and diagnostic failure remain contained by this owner.
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
(error) => {
|
|
369
|
+
if (generation !== expectedGeneration) return;
|
|
370
|
+
try {
|
|
371
|
+
markSuspect(error);
|
|
372
|
+
} catch (stateError) {
|
|
373
|
+
try {
|
|
374
|
+
deps.recordEvent("telegram", stateError, {
|
|
375
|
+
phase: "leader-health-state",
|
|
376
|
+
});
|
|
377
|
+
} catch {
|
|
378
|
+
// State and diagnostic failure remain contained by this owner.
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
)
|
|
383
|
+
.finally(() => {
|
|
384
|
+
if (tickPromise === tracked) tickPromise = undefined;
|
|
385
|
+
});
|
|
386
|
+
tickPromise = tracked;
|
|
387
|
+
return tracked;
|
|
292
388
|
};
|
|
293
389
|
|
|
294
390
|
return {
|
|
295
391
|
start() {
|
|
296
392
|
stop();
|
|
297
393
|
interval = setInterval(() => {
|
|
298
|
-
void
|
|
394
|
+
void requestTick();
|
|
299
395
|
}, intervalMs);
|
|
300
396
|
interval.unref?.();
|
|
301
397
|
},
|
|
@@ -381,6 +477,32 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
381
477
|
assertLeaderEpoch("start");
|
|
382
478
|
await deps.topicTargetStore.load();
|
|
383
479
|
assertLeaderEpoch("after-load");
|
|
480
|
+
const unavailableTargetKeys = new Set([
|
|
481
|
+
...deps.topicTargetStore
|
|
482
|
+
.listSyncObservations()
|
|
483
|
+
.filter((observation) => observation.syncStatus === "deleted")
|
|
484
|
+
.map((observation) => getTelegramTargetKey(observation.target)),
|
|
485
|
+
...deps.topicTargetStore
|
|
486
|
+
.listPendingCleanups()
|
|
487
|
+
.map((intent) => getTelegramTargetKey(intent.target)),
|
|
488
|
+
]);
|
|
489
|
+
let invalidatedUnavailableTarget = false;
|
|
490
|
+
for (const record of deps.topicTargetStore.list()) {
|
|
491
|
+
if (
|
|
492
|
+
record.instanceId !== deps.instanceId ||
|
|
493
|
+
!unavailableTargetKeys.has(getTelegramTargetKey(record.target))
|
|
494
|
+
) {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
invalidatedUnavailableTarget =
|
|
498
|
+
deps.topicTargetStore.markStaleByTarget(record.target) ||
|
|
499
|
+
invalidatedUnavailableTarget;
|
|
500
|
+
}
|
|
501
|
+
if (invalidatedUnavailableTarget) {
|
|
502
|
+
assertLeaderEpoch("before-unavailable-persist");
|
|
503
|
+
await deps.topicTargetStore.persist();
|
|
504
|
+
assertLeaderEpoch("after-unavailable-persist");
|
|
505
|
+
}
|
|
384
506
|
const priorTargets = deps.topicTargetStore.list().filter((record) => {
|
|
385
507
|
return (
|
|
386
508
|
record.instanceId === deps.instanceId &&
|
|
@@ -659,6 +781,7 @@ export function createTelegramObservedTopicLifecycleSyncHandler<
|
|
|
659
781
|
createTelegramTopicLifecycleSyncHandler<TMessage>(deps);
|
|
660
782
|
return async (lifecycle) => {
|
|
661
783
|
const nowMs = deps.getNowMs ?? Date.now;
|
|
784
|
+
deps.assertExecutionCurrent?.(lifecycle.message);
|
|
662
785
|
deps.setSyncState(
|
|
663
786
|
markTelegramSyncSliceSuspect(deps.getSyncState(), "topic-state", {
|
|
664
787
|
nowMs: nowMs(),
|
|
@@ -667,6 +790,7 @@ export function createTelegramObservedTopicLifecycleSyncHandler<
|
|
|
667
790
|
}) as TSyncState,
|
|
668
791
|
);
|
|
669
792
|
await syncTopicLifecycle(lifecycle);
|
|
793
|
+
deps.assertExecutionCurrent?.(lifecycle.message);
|
|
670
794
|
deps.setSyncState(
|
|
671
795
|
markTelegramSyncSliceFresh(deps.getSyncState(), "topic-state", {
|
|
672
796
|
nowMs: nowMs(),
|
|
@@ -680,7 +804,9 @@ export function createTelegramTopicLifecycleSyncHandler<TMessage = unknown>(
|
|
|
680
804
|
deps: TelegramTopicLifecycleSyncDeps,
|
|
681
805
|
): TelegramTopicLifecycleSyncHandler<TMessage> {
|
|
682
806
|
return async (lifecycle) => {
|
|
807
|
+
deps.assertExecutionCurrent?.(lifecycle.message);
|
|
683
808
|
await deps.topicTargetStore.load();
|
|
809
|
+
deps.assertExecutionCurrent?.(lifecycle.message);
|
|
684
810
|
const nowMs = Date.now();
|
|
685
811
|
const plan = ThreadReconciler.planThreadReconciliation({
|
|
686
812
|
nowMs,
|
package/lib/telegram-api.ts
CHANGED
|
@@ -648,8 +648,31 @@ function getTelegramRetryDelayMs(
|
|
|
648
648
|
return Math.max(0, baseDelayMs * 2 ** attempt);
|
|
649
649
|
}
|
|
650
650
|
|
|
651
|
-
function
|
|
652
|
-
return
|
|
651
|
+
function getTelegramApiAbortReason(signal: AbortSignal): unknown {
|
|
652
|
+
return signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function throwIfTelegramApiCallAborted(signal: AbortSignal | undefined): void {
|
|
656
|
+
if (signal?.aborted) throw getTelegramApiAbortReason(signal);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function sleepTelegramRetry(
|
|
660
|
+
ms: number,
|
|
661
|
+
signal?: AbortSignal,
|
|
662
|
+
): Promise<void> {
|
|
663
|
+
if (signal?.aborted) return Promise.reject(getTelegramApiAbortReason(signal));
|
|
664
|
+
return new Promise((resolve, reject) => {
|
|
665
|
+
const timeout = setTimeout(() => {
|
|
666
|
+
signal?.removeEventListener("abort", onAbort);
|
|
667
|
+
resolve();
|
|
668
|
+
}, ms);
|
|
669
|
+
const onAbort = () => {
|
|
670
|
+
clearTimeout(timeout);
|
|
671
|
+
reject(getTelegramApiAbortReason(signal!));
|
|
672
|
+
};
|
|
673
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
674
|
+
if (signal?.aborted) onAbort();
|
|
675
|
+
});
|
|
653
676
|
}
|
|
654
677
|
|
|
655
678
|
function assertTelegramFileSizeWithinLimit(
|
|
@@ -1030,8 +1053,13 @@ async function callTelegramWithRetry<TResponse>(
|
|
|
1030
1053
|
isTelegramApiMethodRetrySafe(method));
|
|
1031
1054
|
const maxAttempts = Math.max(1, options?.maxAttempts ?? 3);
|
|
1032
1055
|
const retryBaseDelayMs = options?.retryBaseDelayMs ?? 500;
|
|
1033
|
-
const
|
|
1056
|
+
const waitBeforeRetry = async (ms: number): Promise<void> => {
|
|
1057
|
+
if (options?.sleep) await options.sleep(ms);
|
|
1058
|
+
else await sleepTelegramRetry(ms, options?.signal);
|
|
1059
|
+
throwIfTelegramApiCallAborted(options?.signal);
|
|
1060
|
+
};
|
|
1034
1061
|
for (let attempt = 0; ; attempt += 1) {
|
|
1062
|
+
throwIfTelegramApiCallAborted(options?.signal);
|
|
1035
1063
|
try {
|
|
1036
1064
|
return unwrapTelegramApiResult(
|
|
1037
1065
|
method,
|
|
@@ -1042,16 +1070,16 @@ async function callTelegramWithRetry<TResponse>(
|
|
|
1042
1070
|
);
|
|
1043
1071
|
} catch (error) {
|
|
1044
1072
|
const retryable =
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1073
|
+
isRetryableTelegramApiError(error) &&
|
|
1074
|
+
!(
|
|
1075
|
+
options?.retryRateLimit === false &&
|
|
1076
|
+
error instanceof TelegramApiHttpError &&
|
|
1077
|
+
error.status === 429
|
|
1078
|
+
);
|
|
1051
1079
|
if (!retrySafe) {
|
|
1052
1080
|
if (error instanceof TelegramApiHttpError && error.status === 429) {
|
|
1053
1081
|
if (attempt >= maxAttempts - 1) throw error;
|
|
1054
|
-
await
|
|
1082
|
+
await waitBeforeRetry(
|
|
1055
1083
|
getTelegramRetryDelayMs(error, attempt, retryBaseDelayMs),
|
|
1056
1084
|
);
|
|
1057
1085
|
continue;
|
|
@@ -1068,7 +1096,9 @@ async function callTelegramWithRetry<TResponse>(
|
|
|
1068
1096
|
throw error;
|
|
1069
1097
|
}
|
|
1070
1098
|
if (attempt >= maxAttempts - 1 || !retryable) throw error;
|
|
1071
|
-
await
|
|
1099
|
+
await waitBeforeRetry(
|
|
1100
|
+
getTelegramRetryDelayMs(error, attempt, retryBaseDelayMs),
|
|
1101
|
+
);
|
|
1072
1102
|
}
|
|
1073
1103
|
}
|
|
1074
1104
|
}
|
package/lib/text-groups.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface TelegramTextGroupState<TMessage, TContext = unknown> {
|
|
|
38
38
|
dispatching?: boolean;
|
|
39
39
|
suspended?: boolean;
|
|
40
40
|
reschedule?: (delayMs?: number) => void;
|
|
41
|
+
dispatchNow?: () => Promise<void>;
|
|
42
|
+
dispatchPromise?: Promise<void>;
|
|
41
43
|
dispatchLimit?: number;
|
|
42
44
|
forwardPairCandidate?: TelegramForwardCommentBatchPosition;
|
|
43
45
|
}
|
|
@@ -63,6 +65,7 @@ export interface TelegramTextGroupController<TMessage, TContext = unknown> {
|
|
|
63
65
|
ctx: TContext,
|
|
64
66
|
) => unknown | Promise<unknown>;
|
|
65
67
|
}) => boolean;
|
|
68
|
+
flushMessage: (messageId: number) => Promise<boolean>;
|
|
66
69
|
suspend: () => void;
|
|
67
70
|
resume: (context: TContext) => void;
|
|
68
71
|
clear: () => void;
|
|
@@ -193,9 +196,18 @@ export function queueTelegramTextGroupMessage<
|
|
|
193
196
|
const key = getTelegramTextGroupKey(options.message);
|
|
194
197
|
if (!key) return false;
|
|
195
198
|
const existing = options.groups.get(key);
|
|
196
|
-
|
|
199
|
+
const duplicateIndex = existing?.messages.findIndex(
|
|
197
200
|
(message) => message.message_id === options.message.message_id,
|
|
198
|
-
)
|
|
201
|
+
);
|
|
202
|
+
if (existing && duplicateIndex !== undefined && duplicateIndex >= 0) {
|
|
203
|
+
existing.messages[duplicateIndex] = options.message;
|
|
204
|
+
existing.context = options.context;
|
|
205
|
+
existing.forwardPairCandidate = options.forwardPairCandidate;
|
|
206
|
+
if (!existing.suspended && !existing.dispatching && !existing.flushTimer) {
|
|
207
|
+
existing.reschedule?.();
|
|
208
|
+
}
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
199
211
|
if (
|
|
200
212
|
!existing &&
|
|
201
213
|
!options.forceStart &&
|
|
@@ -208,46 +220,52 @@ export function queueTelegramTextGroupMessage<
|
|
|
208
220
|
state.messages.push(options.message);
|
|
209
221
|
state.context = options.context;
|
|
210
222
|
state.forwardPairCandidate = options.forwardPairCandidate;
|
|
211
|
-
const dispatchQueued = (): void => {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
options.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
() => {
|
|
239
|
-
if (options.groups.get(key) !== queued) return;
|
|
223
|
+
const dispatchQueued = (): Promise<void> => {
|
|
224
|
+
state.flushTimer = undefined;
|
|
225
|
+
const queued = options.groups.get(key);
|
|
226
|
+
if (!queued || queued.context === undefined) return Promise.resolve();
|
|
227
|
+
if (queued.dispatching) return queued.dispatchPromise ?? Promise.resolve();
|
|
228
|
+
const dispatchCount = queued.dispatchLimit ?? queued.messages.length;
|
|
229
|
+
queued.dispatchLimit = undefined;
|
|
230
|
+
const dispatchedMessages = queued.messages.slice(0, dispatchCount);
|
|
231
|
+
const dispatchedIds = new Set(
|
|
232
|
+
dispatchedMessages.map((message) => message.message_id),
|
|
233
|
+
);
|
|
234
|
+
queued.dispatching = true;
|
|
235
|
+
const operation = Promise.resolve(
|
|
236
|
+
options.dispatchMessages(dispatchedMessages, queued.context),
|
|
237
|
+
).then(
|
|
238
|
+
() => {
|
|
239
|
+
if (options.groups.get(key) !== queued) return;
|
|
240
|
+
queued.messages = queued.messages.filter(
|
|
241
|
+
(message) => !dispatchedIds.has(message.message_id),
|
|
242
|
+
);
|
|
243
|
+
queued.dispatching = false;
|
|
244
|
+
queued.dispatchPromise = undefined;
|
|
245
|
+
if (queued.messages.length === 0) options.groups.delete(key);
|
|
246
|
+
else if (!queued.flushTimer) scheduleDispatch();
|
|
247
|
+
},
|
|
248
|
+
(error) => {
|
|
249
|
+
if (options.groups.get(key) === queued) {
|
|
240
250
|
queued.dispatching = false;
|
|
251
|
+
queued.dispatchPromise = undefined;
|
|
241
252
|
if (!queued.flushTimer) scheduleDispatch();
|
|
242
|
-
}
|
|
243
|
-
|
|
253
|
+
}
|
|
254
|
+
throw error;
|
|
255
|
+
},
|
|
256
|
+
);
|
|
257
|
+
queued.dispatchPromise = operation;
|
|
258
|
+
return operation;
|
|
244
259
|
};
|
|
245
260
|
const scheduleDispatch = (delayMs = options.debounceMs): void => {
|
|
246
261
|
if (state.suspended) return;
|
|
247
|
-
state.flushTimer = options.setTimer(
|
|
262
|
+
state.flushTimer = options.setTimer(() => {
|
|
263
|
+
void dispatchQueued().catch(() => undefined);
|
|
264
|
+
}, delayMs);
|
|
248
265
|
state.flushTimer.unref?.();
|
|
249
266
|
};
|
|
250
267
|
state.reschedule = scheduleDispatch;
|
|
268
|
+
state.dispatchNow = dispatchQueued;
|
|
251
269
|
if (state.flushTimer) options.clearTimer(state.flushTimer);
|
|
252
270
|
scheduleDispatch(
|
|
253
271
|
options.dispatchImmediately ? 0 : (options.delayMs ?? options.debounceMs),
|
|
@@ -390,6 +408,24 @@ export function createTelegramTextGroupController<
|
|
|
390
408
|
: undefined,
|
|
391
409
|
});
|
|
392
410
|
},
|
|
411
|
+
async flushMessage(messageId) {
|
|
412
|
+
for (const state of groups.values()) {
|
|
413
|
+
if (!state.messages.some((message) => message.message_id === messageId)) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (state.flushTimer) clearTimer(state.flushTimer);
|
|
417
|
+
state.flushTimer = undefined;
|
|
418
|
+
await state.dispatchNow?.();
|
|
419
|
+
if (
|
|
420
|
+
state.messages.some((message) => message.message_id === messageId) &&
|
|
421
|
+
!state.dispatching
|
|
422
|
+
) {
|
|
423
|
+
await state.dispatchNow?.();
|
|
424
|
+
}
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
return false;
|
|
428
|
+
},
|
|
393
429
|
suspend: () => {
|
|
394
430
|
for (const state of groups.values()) {
|
|
395
431
|
state.suspended = true;
|
|
@@ -422,6 +458,7 @@ export function createTelegramTextGroupDispatchRuntime<
|
|
|
422
458
|
textGroups: TelegramTextGroupController<TMessage, TContext>;
|
|
423
459
|
dispatchMessages: (messages: TMessage[], ctx: TContext) => Promise<void>;
|
|
424
460
|
dispatchSingleMessage: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
461
|
+
onDeferredMessage?: (message: TMessage) => void;
|
|
425
462
|
}): TelegramTextGroupDispatchRuntime<TMessage, TContext> {
|
|
426
463
|
return {
|
|
427
464
|
handleMessage: async (message, ctx) => {
|
|
@@ -431,7 +468,10 @@ export function createTelegramTextGroupDispatchRuntime<
|
|
|
431
468
|
dispatchMessages: (messages, queuedCtx) =>
|
|
432
469
|
deps.dispatchMessages(messages, queuedCtx),
|
|
433
470
|
});
|
|
434
|
-
if (queuedTextGroup)
|
|
471
|
+
if (queuedTextGroup) {
|
|
472
|
+
deps.onDeferredMessage?.(message);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
435
475
|
await deps.dispatchSingleMessage(message, ctx);
|
|
436
476
|
},
|
|
437
477
|
};
|
package/lib/threads.ts
CHANGED
|
@@ -226,6 +226,8 @@ function getNextMonotonicSlot(
|
|
|
226
226
|
|
|
227
227
|
export interface TelegramTopicTargetStore {
|
|
228
228
|
load: () => Promise<void>;
|
|
229
|
+
/** Discard process-local projections and reload owner-published state. */
|
|
230
|
+
refresh?: () => Promise<void>;
|
|
229
231
|
persist: () => Promise<void>;
|
|
230
232
|
list: () => TelegramTopicTargetRecord[];
|
|
231
233
|
getFollowerRecoveryHintByTarget?: (
|
|
@@ -1296,6 +1298,11 @@ export function createTelegramTopicTargetStore(
|
|
|
1296
1298
|
if (dirty) return;
|
|
1297
1299
|
await loadFromDisk();
|
|
1298
1300
|
},
|
|
1301
|
+
refresh() {
|
|
1302
|
+
const refresh = persistQueue.then(loadFromDisk);
|
|
1303
|
+
persistQueue = refresh.catch(() => undefined);
|
|
1304
|
+
return refresh;
|
|
1305
|
+
},
|
|
1299
1306
|
persist() {
|
|
1300
1307
|
const persist = persistQueue.then(async () => {
|
|
1301
1308
|
const path = getPath();
|
|
@@ -2459,7 +2466,7 @@ export interface TelegramCurrentInstanceThreadRuntime {
|
|
|
2459
2466
|
getRestorationIdentity(): TelegramInstanceThreadIdentityCandidate;
|
|
2460
2467
|
}
|
|
2461
2468
|
|
|
2462
|
-
export
|
|
2469
|
+
export interface TelegramCurrentInstanceThreadRuntimeDeps {
|
|
2463
2470
|
instanceId: string;
|
|
2464
2471
|
listRecords(): readonly TelegramTopicTargetRecord[];
|
|
2465
2472
|
getPreferredTarget(): TelegramTarget | undefined;
|
|
@@ -2467,7 +2474,11 @@ export function createTelegramCurrentInstanceThreadRuntime(deps: {
|
|
|
2467
2474
|
| (TelegramInstanceThreadIdentityCandidate & { registered: boolean })
|
|
2468
2475
|
| undefined;
|
|
2469
2476
|
getLeader(): TelegramInstanceThreadIdentityCandidate | undefined;
|
|
2470
|
-
}
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
export function createTelegramCurrentInstanceThreadRuntime(
|
|
2480
|
+
deps: TelegramCurrentInstanceThreadRuntimeDeps,
|
|
2481
|
+
): TelegramCurrentInstanceThreadRuntime {
|
|
2471
2482
|
const findRecord = function (): TelegramTopicTargetRecord | undefined {
|
|
2472
2483
|
return findCurrentTelegramInstanceThreadRecord({
|
|
2473
2484
|
records: deps.listRecords(),
|
|
@@ -2563,6 +2574,11 @@ export interface TelegramThreadStatusProjectionRuntime {
|
|
|
2563
2574
|
followerTarget?: TelegramTarget;
|
|
2564
2575
|
followerSlot?: string;
|
|
2565
2576
|
followerThreadName?: string;
|
|
2577
|
+
leaderProtocol?: {
|
|
2578
|
+
protocolVersion: number;
|
|
2579
|
+
runtimeBuild: string;
|
|
2580
|
+
capabilities: string[];
|
|
2581
|
+
};
|
|
2566
2582
|
};
|
|
2567
2583
|
getTopicTargets(): ReturnType<typeof listTelegramThreadStatusTargets>;
|
|
2568
2584
|
getThreadReservations(): ReturnType<
|
|
@@ -2575,7 +2591,7 @@ export interface TelegramThreadStatusProjectionRuntime {
|
|
|
2575
2591
|
getInstanceThreadName(): string | undefined;
|
|
2576
2592
|
}
|
|
2577
2593
|
|
|
2578
|
-
export
|
|
2594
|
+
export interface TelegramThreadStatusProjectionRuntimeDeps {
|
|
2579
2595
|
getThreadMode(): "unknown" | "enabled" | "disabled";
|
|
2580
2596
|
isBusPollingStarted(): boolean;
|
|
2581
2597
|
isFollowerRegistered(): boolean;
|
|
@@ -2589,8 +2605,19 @@ export function createTelegramThreadStatusProjectionRuntime(deps: {
|
|
|
2589
2605
|
getFollowerTarget(): TelegramTarget | undefined;
|
|
2590
2606
|
getFollowerSlot(): string | undefined;
|
|
2591
2607
|
getFollowerThreadName(): string | undefined;
|
|
2608
|
+
getLeaderProtocol?():
|
|
2609
|
+
| {
|
|
2610
|
+
protocolVersion: number;
|
|
2611
|
+
runtimeBuild: string;
|
|
2612
|
+
capabilities: string[];
|
|
2613
|
+
}
|
|
2614
|
+
| undefined;
|
|
2592
2615
|
getCurrentIdentity(): TelegramInstanceThreadIdentityCandidate;
|
|
2593
|
-
}
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
export function createTelegramThreadStatusProjectionRuntime(
|
|
2619
|
+
deps: TelegramThreadStatusProjectionRuntimeDeps,
|
|
2620
|
+
): TelegramThreadStatusProjectionRuntime {
|
|
2594
2621
|
return {
|
|
2595
2622
|
getBusRole() {
|
|
2596
2623
|
if (deps.getThreadMode() === "disabled") return undefined;
|
|
@@ -2606,6 +2633,7 @@ export function createTelegramThreadStatusProjectionRuntime(deps: {
|
|
|
2606
2633
|
getLocalBus() {
|
|
2607
2634
|
const leaderSocketPath = deps.getLeaderSocketPath();
|
|
2608
2635
|
const followerSocketPath = deps.getFollowerSocketPath();
|
|
2636
|
+
const leaderProtocol = deps.getLeaderProtocol?.();
|
|
2609
2637
|
return {
|
|
2610
2638
|
leaderSocketPath,
|
|
2611
2639
|
leaderTransport: deps.getTransportKind(leaderSocketPath),
|
|
@@ -2615,6 +2643,7 @@ export function createTelegramThreadStatusProjectionRuntime(deps: {
|
|
|
2615
2643
|
followerTarget: deps.getFollowerTarget(),
|
|
2616
2644
|
followerSlot: deps.getFollowerSlot(),
|
|
2617
2645
|
followerThreadName: deps.getFollowerThreadName(),
|
|
2646
|
+
...(leaderProtocol ? { leaderProtocol } : {}),
|
|
2618
2647
|
};
|
|
2619
2648
|
},
|
|
2620
2649
|
getTopicTargets: () => listTelegramThreadStatusTargets(deps.listRecords()),
|
|
@@ -2633,11 +2662,84 @@ export function createTelegramThreadStatusProjectionRuntime(deps: {
|
|
|
2633
2662
|
};
|
|
2634
2663
|
}
|
|
2635
2664
|
|
|
2665
|
+
export interface TelegramCurrentThreadAssemblyDeps {
|
|
2666
|
+
instanceId: string;
|
|
2667
|
+
listRecords: TelegramCurrentInstanceThreadRuntimeDeps["listRecords"];
|
|
2668
|
+
getActiveTurnTarget(): TelegramTarget | undefined;
|
|
2669
|
+
getFollowerTarget(): TelegramTarget | undefined;
|
|
2670
|
+
isFollowerRegistered(): boolean;
|
|
2671
|
+
getFollowerSlot(): string | undefined;
|
|
2672
|
+
getFollowerThreadName(): string | undefined;
|
|
2673
|
+
getLeaderIdentity(): TelegramInstanceThreadIdentityCandidate | undefined;
|
|
2674
|
+
getLeaderTarget(): TelegramTarget | undefined;
|
|
2675
|
+
getLeaderProtocol?: TelegramThreadStatusProjectionRuntimeDeps["getLeaderProtocol"];
|
|
2676
|
+
status: Pick<
|
|
2677
|
+
TelegramThreadStatusProjectionRuntimeDeps,
|
|
2678
|
+
| "getThreadMode"
|
|
2679
|
+
| "isBusPollingStarted"
|
|
2680
|
+
| "listFollowers"
|
|
2681
|
+
| "listReservations"
|
|
2682
|
+
| "listSyncObservations"
|
|
2683
|
+
| "getLeaderSocketPath"
|
|
2684
|
+
| "getFollowerSocketPath"
|
|
2685
|
+
| "getTransportKind"
|
|
2686
|
+
>;
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
export interface TelegramCurrentThreadAssembly {
|
|
2690
|
+
current: TelegramCurrentInstanceThreadRuntime;
|
|
2691
|
+
status: TelegramThreadStatusProjectionRuntime;
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
/** Own current-thread preference and its matching status projection. */
|
|
2695
|
+
export function createTelegramCurrentThreadAssembly(
|
|
2696
|
+
deps: TelegramCurrentThreadAssemblyDeps,
|
|
2697
|
+
): TelegramCurrentThreadAssembly {
|
|
2698
|
+
const getFollower = () => {
|
|
2699
|
+
const target = deps.getFollowerTarget();
|
|
2700
|
+
if (!target) return undefined;
|
|
2701
|
+
return {
|
|
2702
|
+
registered: deps.isFollowerRegistered(),
|
|
2703
|
+
target,
|
|
2704
|
+
slot: deps.getFollowerSlot(),
|
|
2705
|
+
threadName: deps.getFollowerThreadName(),
|
|
2706
|
+
};
|
|
2707
|
+
};
|
|
2708
|
+
const current = createTelegramCurrentInstanceThreadRuntime({
|
|
2709
|
+
instanceId: deps.instanceId,
|
|
2710
|
+
listRecords: deps.listRecords,
|
|
2711
|
+
getPreferredTarget: () =>
|
|
2712
|
+
deps.getActiveTurnTarget() ??
|
|
2713
|
+
deps.getFollowerTarget() ??
|
|
2714
|
+
deps.getLeaderTarget(),
|
|
2715
|
+
getFollower,
|
|
2716
|
+
getLeader: deps.getLeaderIdentity,
|
|
2717
|
+
});
|
|
2718
|
+
return {
|
|
2719
|
+
current,
|
|
2720
|
+
status: createTelegramThreadStatusProjectionRuntime({
|
|
2721
|
+
...deps.status,
|
|
2722
|
+
isFollowerRegistered: deps.isFollowerRegistered,
|
|
2723
|
+
listRecords: deps.listRecords,
|
|
2724
|
+
getFollowerTarget: deps.getFollowerTarget,
|
|
2725
|
+
getFollowerSlot: deps.getFollowerSlot,
|
|
2726
|
+
getFollowerThreadName: deps.getFollowerThreadName,
|
|
2727
|
+
getLeaderProtocol: deps.getLeaderProtocol,
|
|
2728
|
+
getCurrentIdentity: current.getRestorationIdentity,
|
|
2729
|
+
}),
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2636
2733
|
export interface TelegramThreadStatusFollowerView {
|
|
2637
2734
|
instanceId: string;
|
|
2638
2735
|
cwd?: string;
|
|
2639
2736
|
lastHeartbeatMs: number;
|
|
2640
2737
|
target?: TelegramTarget;
|
|
2738
|
+
protocol?: {
|
|
2739
|
+
protocolVersion: number;
|
|
2740
|
+
runtimeBuild: string;
|
|
2741
|
+
capabilities: string[];
|
|
2742
|
+
};
|
|
2641
2743
|
}
|
|
2642
2744
|
|
|
2643
2745
|
function getTelegramThreadStatusName(
|
|
@@ -2660,6 +2762,11 @@ export function listTelegramThreadStatusFollowers(options: {
|
|
|
2660
2762
|
cwd?: string;
|
|
2661
2763
|
lastHeartbeatMs: number;
|
|
2662
2764
|
target?: TelegramTarget;
|
|
2765
|
+
protocol?: {
|
|
2766
|
+
protocolVersion: number;
|
|
2767
|
+
runtimeBuild: string;
|
|
2768
|
+
capabilities: string[];
|
|
2769
|
+
};
|
|
2663
2770
|
slot?: string;
|
|
2664
2771
|
threadName?: string;
|
|
2665
2772
|
status?: string;
|
|
@@ -2676,6 +2783,7 @@ export function listTelegramThreadStatusFollowers(options: {
|
|
|
2676
2783
|
cwd: follower.cwd,
|
|
2677
2784
|
lastHeartbeatMs: follower.lastHeartbeatMs,
|
|
2678
2785
|
target: follower.target,
|
|
2786
|
+
...(follower.protocol ? { protocol: follower.protocol } : {}),
|
|
2679
2787
|
slot: record?.slot,
|
|
2680
2788
|
threadName: getTelegramThreadStatusName(record),
|
|
2681
2789
|
status: record?.status,
|