@llblab/pi-kit 0.19.1 → 0.20.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/CHANGELOG.md +12 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +4 -4
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-state-flow/README.md +3 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +45 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +9 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +19 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +52 -11
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +0 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +36 -15
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +5 -5
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +2 -1
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +2 -2
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +48 -5
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +9 -9
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +65 -11
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +1 -2
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +35 -15
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +1 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +9 -7
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +13 -32
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +11 -0
- package/node_modules/@llblab/pi-telegram/README.md +4 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +3 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +93 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +9 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +231 -71
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +4 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +34 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +63 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +3 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +13 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +25 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +32 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +10 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +22 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +1 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +9 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +41 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +9 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +23 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +7 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +68 -37
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +6 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +134 -61
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +9 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +49 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +77 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +384 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.d.ts +3 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.js +6 -0
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +21 -14
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +31 -10
- package/node_modules/@llblab/pi-telegram/docs/updates.md +2 -0
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +5 -7
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +69 -12
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +236 -94
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +39 -9
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +69 -2
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +14 -0
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +23 -4
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +32 -13
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -1
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +2 -3
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +45 -1
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +30 -2
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +11 -3
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +73 -36
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +145 -78
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +67 -4
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +465 -7
- package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +7 -0
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +3 -3
|
@@ -47,6 +47,9 @@ import { getTelegramBusTransportRetryPolicy } from "./bus-transport.ts";
|
|
|
47
47
|
import type { TelegramQueueHandoffPayload } from "./queue.ts";
|
|
48
48
|
import {
|
|
49
49
|
createTelegramWorkspaceOperationRuntime,
|
|
50
|
+
createTelegramWorkspaceSlotRotation,
|
|
51
|
+
type TelegramWorkspaceCapacityRunner,
|
|
52
|
+
type TelegramWorkspaceSlotRotationPorts,
|
|
50
53
|
type TelegramWorkspaceOperationRunner,
|
|
51
54
|
} from "./workspace-retirement.ts";
|
|
52
55
|
import {
|
|
@@ -54,6 +57,7 @@ import {
|
|
|
54
57
|
runWithTelegramWorkspaceAdmissionsAsync,
|
|
55
58
|
type TelegramWorkspaceAdmissionLedger,
|
|
56
59
|
} from "./workspace-admission.ts";
|
|
60
|
+
import { TELEGRAM_WORKSPACE_SLOTS, TelegramWorkspaceSlotUnavailableError } from "./workspace-slots.ts";
|
|
57
61
|
|
|
58
62
|
export const TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS = 15_000;
|
|
59
63
|
|
|
@@ -180,7 +184,6 @@ export interface TelegramBusFollowerDisconnectHandlerDeps {
|
|
|
180
184
|
Threads.TelegramTopicTargetStore,
|
|
181
185
|
| "list"
|
|
182
186
|
| "markStaleByTarget"
|
|
183
|
-
| "markWorkspaceBindingInactiveByTarget"
|
|
184
187
|
| "persist"
|
|
185
188
|
| "upsertPendingCleanup"
|
|
186
189
|
| "removePendingCleanup"
|
|
@@ -230,6 +233,8 @@ export interface TelegramBusLeaderRuntimeAssemblyDeps<TContext> {
|
|
|
230
233
|
| "callApi"
|
|
231
234
|
| "onFollowerDisconnected"
|
|
232
235
|
| "onFollowerConfirmedDead"
|
|
236
|
+
| "onFollowerConfirmedDeadPreserved"
|
|
237
|
+
| "getTelegramProfile"
|
|
233
238
|
| "provisionFollowerTarget"
|
|
234
239
|
| "provisionLeaderTarget"
|
|
235
240
|
| "recordRuntimeEvent"
|
|
@@ -265,6 +270,7 @@ export interface TelegramBusLeaderRuntimeAssemblyDeps<TContext> {
|
|
|
265
270
|
"acquireAdmission" | "releaseAdmission"
|
|
266
271
|
> | undefined;
|
|
267
272
|
runWorkspaceOperation?: TelegramBusWorkspaceAdmissionRunner;
|
|
273
|
+
workspaceRotation?: TelegramWorkspaceSlotRotationPorts;
|
|
268
274
|
}
|
|
269
275
|
|
|
270
276
|
export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
@@ -311,6 +317,13 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
311
317
|
});
|
|
312
318
|
},
|
|
313
319
|
}).run;
|
|
320
|
+
const runWithWorkspaceCapacity = deps.workspaceRotation && deps.captureWorkspaceExternalProtection &&
|
|
321
|
+
deps.getCurrentLeaderEpoch ? createTelegramWorkspaceSlotRotation({
|
|
322
|
+
...deps.workspaceRotation, store: deps.topicTargetStore,
|
|
323
|
+
getLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
324
|
+
getExternalProtection: deps.captureWorkspaceExternalProtection,
|
|
325
|
+
recordEvent(message, details) { deps.recordRuntimeEvent("bus", message, details); },
|
|
326
|
+
}) : undefined;
|
|
314
327
|
const captureLiveBindingKeys = (
|
|
315
328
|
bindings: readonly Threads.TelegramWorkspaceThreadBinding[],
|
|
316
329
|
): ReadonlySet<string> => resolveTelegramLiveWorkspaceBindingKeys(
|
|
@@ -382,12 +395,37 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
382
395
|
},
|
|
383
396
|
reconcileThreadDisplayOperation,
|
|
384
397
|
);
|
|
398
|
+
let startupDisplayTimer: ReturnType<typeof setTimeout> | undefined;
|
|
399
|
+
let startupDisplayGeneration = 0;
|
|
400
|
+
let startupDisplaySettled = true;
|
|
401
|
+
const cancelStartupDisplayStabilization = () => {
|
|
402
|
+
startupDisplayGeneration += 1;
|
|
403
|
+
startupDisplaySettled = true;
|
|
404
|
+
if (startupDisplayTimer) clearTimeout(startupDisplayTimer);
|
|
405
|
+
startupDisplayTimer = undefined;
|
|
406
|
+
};
|
|
385
407
|
const scheduleDisplay = () => {
|
|
386
|
-
if (!display) return;
|
|
408
|
+
if (!display || !startupDisplaySettled) return;
|
|
387
409
|
void reconcileThreadDisplay().catch((error) => {
|
|
388
410
|
deps.recordRuntimeEvent("bus", error, { phase: "thread-display-reconcile" });
|
|
389
411
|
});
|
|
390
412
|
};
|
|
413
|
+
const beginStartupDisplayStabilization = () => {
|
|
414
|
+
cancelStartupDisplayStabilization();
|
|
415
|
+
startupDisplaySettled = false;
|
|
416
|
+
};
|
|
417
|
+
const armStartupDisplayReconciliation = () => {
|
|
418
|
+
const generation = startupDisplayGeneration;
|
|
419
|
+
const delayMs = deps.runtime.followerStaleAfterMs ??
|
|
420
|
+
TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS;
|
|
421
|
+
startupDisplayTimer = setTimeout(() => {
|
|
422
|
+
if (startupDisplayGeneration !== generation) return;
|
|
423
|
+
startupDisplayTimer = undefined;
|
|
424
|
+
startupDisplaySettled = true;
|
|
425
|
+
scheduleDisplay();
|
|
426
|
+
}, delayMs);
|
|
427
|
+
startupDisplayTimer.unref?.();
|
|
428
|
+
};
|
|
391
429
|
let modeTail: Promise<void> = Promise.resolve();
|
|
392
430
|
const applyThreadDisplayMode = (mode: TelegramThreadDisplayMode, isCurrent: () => boolean): Promise<void> => {
|
|
393
431
|
const epoch = deps.getCurrentLeaderEpoch?.();
|
|
@@ -593,7 +631,7 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
593
631
|
onFollowerRegistered: scheduleDisplay,
|
|
594
632
|
provisionLeaderTarget: (ctx) => {
|
|
595
633
|
const chatId = deps.getAllowedUserId();
|
|
596
|
-
|
|
634
|
+
const provision = () => runWorkspaceOperation(
|
|
597
635
|
{
|
|
598
636
|
operationId: `leader-provision:${deps.instanceId}`,
|
|
599
637
|
operationKind: "workspace.provision-leader",
|
|
@@ -605,6 +643,7 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
605
643
|
},
|
|
606
644
|
() => provisionLeaderTarget(ctx),
|
|
607
645
|
);
|
|
646
|
+
return runWithWorkspaceCapacity ? runWithWorkspaceCapacity(provision) : provision();
|
|
608
647
|
},
|
|
609
648
|
getFollowerDisplayTitle(follower) {
|
|
610
649
|
const binding = deps.topicTargetStore.listWorkspaceBindings().find((binding) =>
|
|
@@ -722,6 +761,35 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
722
761
|
);
|
|
723
762
|
scheduleDisplay();
|
|
724
763
|
},
|
|
764
|
+
async onFollowerConfirmedDeadPreserved(follower, isDetached, operationId) {
|
|
765
|
+
const epoch = deps.getCurrentLeaderEpoch?.();
|
|
766
|
+
const profile = deps.getTelegramProfile?.();
|
|
767
|
+
if (epoch === undefined || !follower.registrationGeneration ||
|
|
768
|
+
typeof follower.target?.threadId !== "number") return false;
|
|
769
|
+
const isCurrent = () => isDetached() && deps.getCurrentLeaderEpoch?.() === epoch &&
|
|
770
|
+
deps.getTelegramProfile?.() === profile;
|
|
771
|
+
if (!isCurrent()) return false;
|
|
772
|
+
const settled = await runWorkspaceOperation({
|
|
773
|
+
operationId,
|
|
774
|
+
operationKind: "workspace.preserve-dead-follower",
|
|
775
|
+
scopes: [{ kind: "profile" }],
|
|
776
|
+
}, async () => {
|
|
777
|
+
if (!isCurrent()) return false;
|
|
778
|
+
await deps.topicTargetStore.load();
|
|
779
|
+
if (!isCurrent()) return false;
|
|
780
|
+
const record = deps.topicTargetStore.getActiveByInstanceId(follower.instanceId);
|
|
781
|
+
if (!record || (follower.profileKey && record.profileKey !== follower.profileKey) ||
|
|
782
|
+
record.target.chatId !== follower.target!.chatId ||
|
|
783
|
+
record.target.threadId !== follower.target!.threadId) return true;
|
|
784
|
+
const committed = await deps.topicTargetStore.detachTargetOwner(record, isCurrent);
|
|
785
|
+
if (!committed && isCurrent()) {
|
|
786
|
+
throw new Error("Telegram preserved follower detachment was not committed.");
|
|
787
|
+
}
|
|
788
|
+
return committed;
|
|
789
|
+
});
|
|
790
|
+
if (settled && isCurrent()) scheduleDisplay();
|
|
791
|
+
return settled;
|
|
792
|
+
},
|
|
725
793
|
provisionFollowerTarget: (registration, options) => runWorkspaceOperation(
|
|
726
794
|
{
|
|
727
795
|
operationId: `follower-provision:${registration.instanceId}:${registration.registrationGeneration}`,
|
|
@@ -731,7 +799,9 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
731
799
|
() => provisionFollowerTarget(registration, options),
|
|
732
800
|
),
|
|
733
801
|
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
802
|
+
getTelegramProfile: deps.getTelegramProfile,
|
|
734
803
|
runWorkspaceAdmission,
|
|
804
|
+
runWithWorkspaceCapacity,
|
|
735
805
|
callApi: createTelegramBusLeaderApiProxy({
|
|
736
806
|
call: deps.callApi,
|
|
737
807
|
callMultipart: deps.callMultipart,
|
|
@@ -749,8 +819,18 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
749
819
|
resetThreadNameAdmitted,
|
|
750
820
|
captureWorkspaceExternalProtection: deps.captureWorkspaceExternalProtection,
|
|
751
821
|
async startPolling(ctx: TContext) {
|
|
752
|
-
|
|
753
|
-
|
|
822
|
+
if (display) beginStartupDisplayStabilization();
|
|
823
|
+
try {
|
|
824
|
+
await runtime.startPolling(ctx);
|
|
825
|
+
} catch (error) {
|
|
826
|
+
cancelStartupDisplayStabilization();
|
|
827
|
+
throw error;
|
|
828
|
+
}
|
|
829
|
+
if (display) armStartupDisplayReconciliation();
|
|
830
|
+
},
|
|
831
|
+
async stopPolling() {
|
|
832
|
+
cancelStartupDisplayStabilization();
|
|
833
|
+
await runtime.stopPolling();
|
|
754
834
|
},
|
|
755
835
|
};
|
|
756
836
|
if (!display) return assembled;
|
|
@@ -824,8 +904,10 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
|
824
904
|
applyThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
|
|
825
905
|
getThreadDisplayMode?: () => TelegramThreadDisplayMode;
|
|
826
906
|
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
907
|
+
getTelegramProfile?: () => string | undefined;
|
|
827
908
|
provisionLeaderTarget?: (ctx: TContext) => Promise<void> | void;
|
|
828
909
|
runWorkspaceAdmission?: TelegramBusWorkspaceAdmissionRunner;
|
|
910
|
+
runWithWorkspaceCapacity?: TelegramWorkspaceCapacityRunner;
|
|
829
911
|
getNowMs?: () => number;
|
|
830
912
|
timeoutMs?: number;
|
|
831
913
|
followerPruneIntervalMs?: number;
|
|
@@ -838,6 +920,12 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
|
838
920
|
onFollowerConfirmedDead?: (
|
|
839
921
|
follower: TelegramBusFollowerView,
|
|
840
922
|
) => Promise<void> | void;
|
|
923
|
+
/** True settles this observation; false needs fresh proof before another attempt. */
|
|
924
|
+
onFollowerConfirmedDeadPreserved?: (
|
|
925
|
+
follower: TelegramBusFollowerView,
|
|
926
|
+
isDetached: () => boolean,
|
|
927
|
+
operationId: string,
|
|
928
|
+
) => Promise<boolean> | boolean;
|
|
841
929
|
recordRuntimeEvent?: (
|
|
842
930
|
category: string,
|
|
843
931
|
error: unknown,
|
|
@@ -943,7 +1031,7 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
943
1031
|
if (registration.cwd && !workspaceIdentity) {
|
|
944
1032
|
if (options?.existingWorkspaceBindingOnly) return undefined;
|
|
945
1033
|
if (capacityUnavailable) {
|
|
946
|
-
throw new
|
|
1034
|
+
throw new TelegramWorkspaceSlotUnavailableError();
|
|
947
1035
|
}
|
|
948
1036
|
throw new Error("Telegram Workspace identity is already claimed.");
|
|
949
1037
|
}
|
|
@@ -1071,7 +1159,7 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1071
1159
|
workspaceIdentity?.bindingKey,
|
|
1072
1160
|
);
|
|
1073
1161
|
if (!recoveredSlot) {
|
|
1074
|
-
throw new
|
|
1162
|
+
throw new TelegramWorkspaceSlotUnavailableError();
|
|
1075
1163
|
}
|
|
1076
1164
|
const recoveredRecord: Threads.TelegramTopicTargetRecord = {
|
|
1077
1165
|
profileKey: followerProfileKey,
|
|
@@ -1463,13 +1551,7 @@ function createTelegramBusFollowerCleanupHandler(
|
|
|
1463
1551
|
isCleanupTargetProtected,
|
|
1464
1552
|
callApi: deps.callApi,
|
|
1465
1553
|
markStaleByTarget(target, syncStatus, lastSyncError) {
|
|
1466
|
-
|
|
1467
|
-
target, syncStatus, lastSyncError,
|
|
1468
|
-
);
|
|
1469
|
-
const inactive = deps.topicTargetStore.markWorkspaceBindingInactiveByTarget(
|
|
1470
|
-
target, (deps.getNowMs ?? Date.now)(),
|
|
1471
|
-
);
|
|
1472
|
-
return stale || inactive;
|
|
1554
|
+
return deps.topicTargetStore.markStaleByTarget(target, syncStatus, lastSyncError);
|
|
1473
1555
|
},
|
|
1474
1556
|
removeCleanupIntentById: deps.topicTargetStore.removePendingCleanup,
|
|
1475
1557
|
persist: deps.topicTargetStore.persist,
|
|
@@ -1824,6 +1906,7 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
1824
1906
|
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
1825
1907
|
runFollowerMutation?: TelegramBusFollowerMutationRunner;
|
|
1826
1908
|
runWorkspaceAdmission?: TelegramBusWorkspaceAdmissionRunner;
|
|
1909
|
+
runWithWorkspaceCapacity?: TelegramWorkspaceCapacityRunner;
|
|
1827
1910
|
}): (
|
|
1828
1911
|
envelope: TelegramBusEnvelope,
|
|
1829
1912
|
) => Promise<TelegramBusEnvelope> | TelegramBusEnvelope {
|
|
@@ -2148,14 +2231,16 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
2148
2231
|
}.`,
|
|
2149
2232
|
};
|
|
2150
2233
|
}
|
|
2234
|
+
if (!envelope.registration.registrationGeneration) {
|
|
2235
|
+
return {
|
|
2236
|
+
kind: "bus.ack", requestId: envelope.requestId, ok: false,
|
|
2237
|
+
protocol: deps.protocolIdentity,
|
|
2238
|
+
message: "Telegram follower registration requires an exact generation.",
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2151
2241
|
const registrationOperation = () =>
|
|
2152
2242
|
runFollowerMutation(envelope.registration, async () => {
|
|
2153
2243
|
try {
|
|
2154
|
-
if (!envelope.registration.registrationGeneration) {
|
|
2155
|
-
throw new Error(
|
|
2156
|
-
"Telegram follower registration requires an exact generation.",
|
|
2157
|
-
);
|
|
2158
|
-
}
|
|
2159
2244
|
const leaderEpoch = deps.getCurrentLeaderEpoch?.();
|
|
2160
2245
|
if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
|
|
2161
2246
|
throw new Error(
|
|
@@ -2222,6 +2307,8 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
2222
2307
|
} : {}),
|
|
2223
2308
|
};
|
|
2224
2309
|
} catch (error) {
|
|
2310
|
+
if (!restoringWorkspace && deps.runWithWorkspaceCapacity &&
|
|
2311
|
+
error instanceof TelegramWorkspaceSlotUnavailableError) throw error;
|
|
2225
2312
|
return {
|
|
2226
2313
|
kind: "bus.ack" as const,
|
|
2227
2314
|
requestId: envelope.requestId,
|
|
@@ -2235,16 +2322,17 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
2235
2322
|
}
|
|
2236
2323
|
},
|
|
2237
2324
|
);
|
|
2238
|
-
|
|
2325
|
+
const register = () => deps.runWorkspaceAdmission ? deps.runWorkspaceAdmission(
|
|
2326
|
+
{
|
|
2327
|
+
operationId: `follower-registration:${envelope.requestId}`,
|
|
2328
|
+
operationKind: "workspace.register-follower",
|
|
2329
|
+
scopes: [{ kind: "profile" }],
|
|
2330
|
+
},
|
|
2331
|
+
registrationOperation,
|
|
2332
|
+
) : registrationOperation();
|
|
2239
2333
|
try {
|
|
2240
|
-
return await deps.
|
|
2241
|
-
|
|
2242
|
-
operationId: `follower-registration:${envelope.requestId}`,
|
|
2243
|
-
operationKind: "workspace.register-follower",
|
|
2244
|
-
scopes: [{ kind: "profile" }],
|
|
2245
|
-
},
|
|
2246
|
-
registrationOperation,
|
|
2247
|
-
);
|
|
2334
|
+
return await (!restoringWorkspace && deps.runWithWorkspaceCapacity
|
|
2335
|
+
? deps.runWithWorkspaceCapacity(register) : register());
|
|
2248
2336
|
} catch (error) {
|
|
2249
2337
|
return {
|
|
2250
2338
|
kind: "bus.ack" as const,
|
|
@@ -2802,8 +2890,23 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
2802
2890
|
let pruneInterval: ReturnType<typeof setInterval> | undefined;
|
|
2803
2891
|
let pruneGeneration = 0;
|
|
2804
2892
|
let prunePromise: Promise<void> | undefined;
|
|
2893
|
+
type PreservationObservation = {
|
|
2894
|
+
follower: TelegramBusFollowerView;
|
|
2895
|
+
operationId: string;
|
|
2896
|
+
registration: ReturnType<TelegramBusFollowerRegistry["observeUnregistered"]>;
|
|
2897
|
+
isCurrent: () => boolean;
|
|
2898
|
+
};
|
|
2899
|
+
const pendingPreservations = new Map<string, PreservationObservation>();
|
|
2900
|
+
const forgetPreservation = (observation: PreservationObservation | undefined) => {
|
|
2901
|
+
if (!observation) return;
|
|
2902
|
+
if (pendingPreservations.get(observation.follower.instanceId) === observation) {
|
|
2903
|
+
pendingPreservations.delete(observation.follower.instanceId);
|
|
2904
|
+
}
|
|
2905
|
+
observation.registration.release();
|
|
2906
|
+
};
|
|
2805
2907
|
const stopPruning = () => {
|
|
2806
2908
|
pruneGeneration += 1;
|
|
2909
|
+
for (const observation of pendingPreservations.values()) forgetPreservation(observation);
|
|
2807
2910
|
if (pruneInterval) clearInterval(pruneInterval);
|
|
2808
2911
|
pruneInterval = undefined;
|
|
2809
2912
|
prunePromise = undefined;
|
|
@@ -2819,99 +2922,133 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
2819
2922
|
}
|
|
2820
2923
|
};
|
|
2821
2924
|
const pruneFollowers = async (expectedGeneration: number) => {
|
|
2822
|
-
const
|
|
2925
|
+
const epoch = deps.getCurrentLeaderEpoch?.();
|
|
2926
|
+
const profile = deps.getTelegramProfile?.();
|
|
2927
|
+
const isCurrent = (): boolean => pruneGeneration === expectedGeneration &&
|
|
2928
|
+
deps.getTelegramProfile?.() === profile &&
|
|
2929
|
+
(!deps.getCurrentLeaderEpoch || (epoch !== undefined && deps.getCurrentLeaderEpoch() === epoch));
|
|
2823
2930
|
try {
|
|
2824
2931
|
await localServer.ensureEndpoint();
|
|
2825
2932
|
} catch (error) {
|
|
2826
|
-
recordPruneEvent(error, {
|
|
2827
|
-
|
|
2828
|
-
|
|
2933
|
+
recordPruneEvent(error, { phase: "leader-endpoint-recovery" });
|
|
2934
|
+
}
|
|
2935
|
+
if (pruneGeneration !== expectedGeneration) return;
|
|
2936
|
+
for (const observation of pendingPreservations.values()) {
|
|
2937
|
+
if (!observation.isCurrent()) forgetPreservation(observation);
|
|
2829
2938
|
}
|
|
2830
2939
|
if (!isCurrent()) return;
|
|
2831
|
-
const
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2940
|
+
const attempts: { follower: TelegramBusFollowerView; observation?: PreservationObservation; fresh: boolean }[] =
|
|
2941
|
+
[...pendingPreservations.values()].map((observation) => ({
|
|
2942
|
+
follower: observation.follower, observation, fresh: false,
|
|
2943
|
+
}));
|
|
2944
|
+
const removed = deps.followerRegistry.pruneStale(getNowMs(), followerStaleAfterMs);
|
|
2835
2945
|
for (const follower of removed) {
|
|
2946
|
+
forgetPreservation(pendingPreservations.get(follower.instanceId));
|
|
2947
|
+
let observation: PreservationObservation | undefined;
|
|
2948
|
+
if (deps.onFollowerConfirmedDeadPreserved && epoch !== undefined &&
|
|
2949
|
+
follower.registrationGeneration && Number.isSafeInteger(follower.pid) && follower.pid! > 0 &&
|
|
2950
|
+
Number.isSafeInteger(follower.target?.threadId) && follower.target!.threadId! > 0) {
|
|
2951
|
+
if (pendingPreservations.size < TELEGRAM_WORKSPACE_SLOTS.length) {
|
|
2952
|
+
const registration = deps.followerRegistry.observeUnregistered(follower);
|
|
2953
|
+
let current = true;
|
|
2954
|
+
observation = {
|
|
2955
|
+
follower, registration,
|
|
2956
|
+
// Reuse exact admission authority after a lost acquisition acknowledgement.
|
|
2957
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
2958
|
+
isCurrent() {
|
|
2959
|
+
current = current && isCurrent() && registration.isCurrent();
|
|
2960
|
+
return current;
|
|
2961
|
+
},
|
|
2962
|
+
};
|
|
2963
|
+
pendingPreservations.set(follower.instanceId, observation);
|
|
2964
|
+
} else {
|
|
2965
|
+
recordPruneEvent("Telegram follower preservation observation capacity exhausted; retaining binding", {
|
|
2966
|
+
phase: "follower-preservation-capacity", instanceId: follower.instanceId,
|
|
2967
|
+
});
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
attempts.push({ follower, observation, fresh: true });
|
|
2971
|
+
}
|
|
2972
|
+
for (const { follower, observation, fresh } of attempts) {
|
|
2973
|
+
if (!isCurrent()) return;
|
|
2974
|
+
if (observation && !observation.isCurrent()) {
|
|
2975
|
+
forgetPreservation(observation);
|
|
2976
|
+
continue;
|
|
2977
|
+
}
|
|
2836
2978
|
let processConfirmedDead = false;
|
|
2837
|
-
if (follower.pid !== undefined &&
|
|
2979
|
+
if (follower.pid !== undefined && Number.isSafeInteger(follower.pid) &&
|
|
2980
|
+
follower.pid > 0 && deps.isFollowerProcessAlive) {
|
|
2838
2981
|
try {
|
|
2839
|
-
processConfirmedDead =
|
|
2982
|
+
processConfirmedDead = deps.isFollowerProcessAlive(follower.pid) === false;
|
|
2840
2983
|
} catch (error) {
|
|
2841
|
-
recordPruneEvent(error, {
|
|
2842
|
-
phase: "follower-process-liveness",
|
|
2843
|
-
instanceId: follower.instanceId,
|
|
2844
|
-
pid: follower.pid,
|
|
2984
|
+
if (fresh) recordPruneEvent(error, {
|
|
2985
|
+
phase: "follower-process-liveness", instanceId: follower.instanceId, pid: follower.pid,
|
|
2845
2986
|
});
|
|
2846
2987
|
}
|
|
2847
2988
|
}
|
|
2848
2989
|
if (!processConfirmedDead) {
|
|
2849
|
-
recordPruneEvent(
|
|
2850
|
-
"
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
instanceId: follower.instanceId,
|
|
2854
|
-
processLiveness:
|
|
2855
|
-
follower.pid === undefined || !deps.isFollowerProcessAlive
|
|
2856
|
-
? "unknown"
|
|
2857
|
-
: "alive-or-unknown",
|
|
2858
|
-
},
|
|
2859
|
-
);
|
|
2990
|
+
if (fresh) recordPruneEvent("Telegram bus follower heartbeat stale; preserving thread binding", {
|
|
2991
|
+
phase: "follower-pruned", instanceId: follower.instanceId,
|
|
2992
|
+
processLiveness: follower.pid === undefined || !deps.isFollowerProcessAlive ? "unknown" : "alive-or-unknown",
|
|
2993
|
+
});
|
|
2860
2994
|
continue;
|
|
2861
2995
|
}
|
|
2862
|
-
let cleanupEnabled
|
|
2996
|
+
let cleanupEnabled: boolean | undefined;
|
|
2863
2997
|
try {
|
|
2864
|
-
cleanupEnabled =
|
|
2865
|
-
(await deps.shouldCleanupConfirmedDeadFollower?.()) ?? false;
|
|
2998
|
+
cleanupEnabled = await deps.shouldCleanupConfirmedDeadFollower?.();
|
|
2866
2999
|
} catch (error) {
|
|
2867
3000
|
recordPruneEvent(error, {
|
|
2868
|
-
phase: "follower-confirmed-dead-cleanup-policy",
|
|
2869
|
-
instanceId: follower.instanceId,
|
|
2870
|
-
pid: follower.pid,
|
|
3001
|
+
phase: "follower-confirmed-dead-cleanup-policy", instanceId: follower.instanceId, pid: follower.pid,
|
|
2871
3002
|
});
|
|
2872
3003
|
}
|
|
2873
|
-
if (!isCurrent())
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
3004
|
+
if (!isCurrent()) {
|
|
3005
|
+
forgetPreservation(observation);
|
|
3006
|
+
return;
|
|
3007
|
+
}
|
|
3008
|
+
const recordPreserved = () => recordPruneEvent(
|
|
3009
|
+
"Telegram bus follower process confirmed dead; preserving thread binding",
|
|
3010
|
+
{ phase: "follower-confirmed-dead-preserved", instanceId: follower.instanceId,
|
|
3011
|
+
pid: follower.pid, cleanupEnabled },
|
|
3012
|
+
);
|
|
3013
|
+
// Deferred observations authorize only non-destructive publication, never deletion retries.
|
|
3014
|
+
if (cleanupEnabled === true && !fresh) {
|
|
3015
|
+
forgetPreservation(observation);
|
|
3016
|
+
recordPreserved();
|
|
3017
|
+
continue;
|
|
3018
|
+
}
|
|
3019
|
+
const onConfirmedDead = cleanupEnabled === true ? deps.onFollowerConfirmedDead :
|
|
3020
|
+
cleanupEnabled === false && observation ? deps.onFollowerConfirmedDeadPreserved : undefined;
|
|
3021
|
+
if (!onConfirmedDead) {
|
|
3022
|
+
if (cleanupEnabled === true) forgetPreservation(observation);
|
|
3023
|
+
if (fresh) recordPreserved();
|
|
2884
3024
|
continue;
|
|
2885
3025
|
}
|
|
3026
|
+
const findReplacement = () => deps.followerRegistry.list().find((candidate) =>
|
|
3027
|
+
candidate.instanceId === follower.instanceId ||
|
|
3028
|
+
(!!follower.profileKey && candidate.profileKey === follower.profileKey) ||
|
|
3029
|
+
(!!follower.target && candidate.target?.chatId === follower.target.chatId &&
|
|
3030
|
+
candidate.target?.threadId === follower.target.threadId),
|
|
3031
|
+
);
|
|
3032
|
+
let settled = false;
|
|
2886
3033
|
try {
|
|
2887
3034
|
await runFollowerMutation(follower, async () => {
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
);
|
|
2894
|
-
if (replacement) {
|
|
2895
|
-
recordPruneEvent(
|
|
2896
|
-
"Telegram bus follower replaced before confirmed-dead cleanup; preserving thread binding",
|
|
2897
|
-
{
|
|
2898
|
-
phase: "follower-confirmed-dead-replaced",
|
|
2899
|
-
instanceId: follower.instanceId,
|
|
2900
|
-
replacementInstanceId: replacement.instanceId,
|
|
2901
|
-
},
|
|
2902
|
-
);
|
|
2903
|
-
return;
|
|
2904
|
-
}
|
|
2905
|
-
await deps.onFollowerConfirmedDead!(follower);
|
|
3035
|
+
const isDetached = () => isCurrent() && (!observation || observation.isCurrent()) &&
|
|
3036
|
+
!findReplacement() && deps.isFollowerProcessAlive?.(follower.pid!) === false;
|
|
3037
|
+
if (!isDetached()) return;
|
|
3038
|
+
if (cleanupEnabled === true) await deps.onFollowerConfirmedDead!(follower);
|
|
3039
|
+
else settled = await deps.onFollowerConfirmedDeadPreserved!(follower, isDetached, observation!.operationId);
|
|
2906
3040
|
});
|
|
3041
|
+
if (!cleanupEnabled) recordPreserved();
|
|
2907
3042
|
} catch (error) {
|
|
2908
3043
|
recordPruneEvent(error, {
|
|
2909
|
-
phase: "follower-confirmed-dead-cleanup",
|
|
2910
|
-
instanceId: follower.instanceId,
|
|
2911
|
-
|
|
2912
|
-
chatId: follower.target?.chatId,
|
|
2913
|
-
threadId: follower.target?.threadId,
|
|
3044
|
+
phase: cleanupEnabled ? "follower-confirmed-dead-cleanup" : "follower-confirmed-dead-preserve",
|
|
3045
|
+
instanceId: follower.instanceId, pid: follower.pid,
|
|
3046
|
+
chatId: follower.target?.chatId, threadId: follower.target?.threadId,
|
|
2914
3047
|
});
|
|
3048
|
+
} finally {
|
|
3049
|
+
if (settled || cleanupEnabled === true || (observation && !observation.isCurrent())) {
|
|
3050
|
+
forgetPreservation(observation);
|
|
3051
|
+
}
|
|
2915
3052
|
}
|
|
2916
3053
|
}
|
|
2917
3054
|
};
|
|
@@ -2949,7 +3086,11 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
2949
3086
|
resolveAgentTarget: deps.resolveAgentTarget,
|
|
2950
3087
|
routeAgentMessage: deps.routeAgentMessage,
|
|
2951
3088
|
routeQueueHandoff: deps.routeQueueHandoff,
|
|
2952
|
-
provisionFollowerTarget: deps.provisionFollowerTarget,
|
|
3089
|
+
provisionFollowerTarget: deps.provisionFollowerTarget ? (registration, options) => {
|
|
3090
|
+
// Same-instance ownership can change in the store before registry publication.
|
|
3091
|
+
forgetPreservation(pendingPreservations.get(registration.instanceId));
|
|
3092
|
+
return deps.provisionFollowerTarget!(registration, options);
|
|
3093
|
+
} : undefined,
|
|
2953
3094
|
onFollowerDisconnected: deps.onFollowerDisconnected,
|
|
2954
3095
|
renameFollowerThread: deps.renameFollowerThread,
|
|
2955
3096
|
resetFollowerThreadName: deps.resetFollowerThreadName,
|
|
@@ -2960,6 +3101,7 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
2960
3101
|
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
2961
3102
|
runFollowerMutation,
|
|
2962
3103
|
runWorkspaceAdmission: deps.runWorkspaceAdmission,
|
|
3104
|
+
runWithWorkspaceCapacity: deps.runWithWorkspaceCapacity,
|
|
2963
3105
|
});
|
|
2964
3106
|
const routeQueueHandoffEnvelope = async (
|
|
2965
3107
|
input: Parameters<TelegramBusLeaderRuntime<TContext>["routeQueueHandoff"]>[0],
|
|
@@ -2142,6 +2142,10 @@ export interface TelegramBusFollowerRegistry {
|
|
|
2142
2142
|
list: () => TelegramBusFollowerView[];
|
|
2143
2143
|
remove: (instanceId: string) => boolean;
|
|
2144
2144
|
clear: () => void;
|
|
2145
|
+
observeUnregistered: (follower: TelegramBusFollowerView) => {
|
|
2146
|
+
isCurrent: () => boolean;
|
|
2147
|
+
release: () => void;
|
|
2148
|
+
};
|
|
2145
2149
|
pruneStale: (
|
|
2146
2150
|
nowMs: number,
|
|
2147
2151
|
staleAfterMs: number,
|
|
@@ -2163,8 +2167,19 @@ export function createTelegramBusForwardOwnershipValidator(
|
|
|
2163
2167
|
};
|
|
2164
2168
|
}
|
|
2165
2169
|
|
|
2170
|
+
function hasTelegramBusFollowerIdentityOverlap(
|
|
2171
|
+
first: TelegramBusInstanceRegistration,
|
|
2172
|
+
second: TelegramBusInstanceRegistration,
|
|
2173
|
+
): boolean {
|
|
2174
|
+
return first.instanceId === second.instanceId ||
|
|
2175
|
+
(first.profileKey !== undefined && first.profileKey === second.profileKey) ||
|
|
2176
|
+
(first.target !== undefined && first.target.chatId === second.target?.chatId &&
|
|
2177
|
+
first.target.threadId === second.target.threadId);
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2166
2180
|
export function createTelegramBusFollowerRegistry(): TelegramBusFollowerRegistry {
|
|
2167
2181
|
const followers = new Map<string, TelegramBusFollowerView>();
|
|
2182
|
+
const observations = new Set<{ follower: TelegramBusFollowerView; current: boolean }>();
|
|
2168
2183
|
const clone = (
|
|
2169
2184
|
follower: TelegramBusFollowerView,
|
|
2170
2185
|
): TelegramBusFollowerView => ({
|
|
@@ -2184,14 +2199,7 @@ export function createTelegramBusFollowerRegistry(): TelegramBusFollowerRegistry
|
|
|
2184
2199
|
const existing = followers.get(registration.instanceId);
|
|
2185
2200
|
for (const [instanceId, follower] of followers.entries()) {
|
|
2186
2201
|
if (instanceId === registration.instanceId) continue;
|
|
2187
|
-
|
|
2188
|
-
registration.profileKey !== undefined &&
|
|
2189
|
-
registration.profileKey === follower.profileKey;
|
|
2190
|
-
const sameTarget =
|
|
2191
|
-
registration.target !== undefined &&
|
|
2192
|
-
follower.target?.chatId === registration.target.chatId &&
|
|
2193
|
-
follower.target.threadId === registration.target.threadId;
|
|
2194
|
-
if (sameProfile || sameTarget) followers.delete(instanceId);
|
|
2202
|
+
if (hasTelegramBusFollowerIdentityOverlap(registration, follower)) followers.delete(instanceId);
|
|
2195
2203
|
}
|
|
2196
2204
|
const next: TelegramBusFollowerView = {
|
|
2197
2205
|
...registration,
|
|
@@ -2208,6 +2216,11 @@ export function createTelegramBusFollowerRegistry(): TelegramBusFollowerRegistry
|
|
|
2208
2216
|
existing?.lastHeartbeatMs ?? registration.connectedAtMs,
|
|
2209
2217
|
};
|
|
2210
2218
|
followers.set(registration.instanceId, next);
|
|
2219
|
+
for (const observation of observations) {
|
|
2220
|
+
if (!hasTelegramBusFollowerIdentityOverlap(next, observation.follower)) continue;
|
|
2221
|
+
observation.current = false;
|
|
2222
|
+
observations.delete(observation);
|
|
2223
|
+
}
|
|
2211
2224
|
return clone(next);
|
|
2212
2225
|
},
|
|
2213
2226
|
heartbeat: (instanceId, nowMs) => {
|
|
@@ -2234,7 +2247,24 @@ export function createTelegramBusFollowerRegistry(): TelegramBusFollowerRegistry
|
|
|
2234
2247
|
},
|
|
2235
2248
|
list: () => [...followers.values()].map(clone),
|
|
2236
2249
|
remove: (instanceId) => followers.delete(instanceId),
|
|
2237
|
-
clear: () =>
|
|
2250
|
+
clear: () => {
|
|
2251
|
+
followers.clear();
|
|
2252
|
+
for (const observation of observations) observation.current = false;
|
|
2253
|
+
observations.clear();
|
|
2254
|
+
},
|
|
2255
|
+
observeUnregistered: (follower) => {
|
|
2256
|
+
// This watches replacement, not process death or delivery authority.
|
|
2257
|
+
const observation = { follower: clone(follower), current: ![...followers.values()]
|
|
2258
|
+
.some((current) => hasTelegramBusFollowerIdentityOverlap(current, follower)) };
|
|
2259
|
+
if (observation.current) observations.add(observation);
|
|
2260
|
+
return {
|
|
2261
|
+
isCurrent: () => observation.current,
|
|
2262
|
+
release() {
|
|
2263
|
+
observation.current = false;
|
|
2264
|
+
observations.delete(observation);
|
|
2265
|
+
},
|
|
2266
|
+
};
|
|
2267
|
+
},
|
|
2238
2268
|
pruneStale: (nowMs, staleAfterMs) => {
|
|
2239
2269
|
const removed: TelegramBusFollowerView[] = [];
|
|
2240
2270
|
for (const [instanceId, follower] of followers.entries()) {
|