@llblab/pi-telegram 0.51.5 → 0.52.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 +10 -8
- package/BACKLOG.md +1 -8
- package/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +16 -6
- package/dist/lib/activity-verbosity.d.ts +1 -5
- package/dist/lib/activity-verbosity.js +5 -7
- package/dist/lib/bindings.d.ts +9 -2
- package/dist/lib/bindings.js +22 -13
- package/dist/lib/bus-api.js +4 -2
- package/dist/lib/bus-follower.d.ts +55 -57
- package/dist/lib/bus-follower.js +213 -140
- package/dist/lib/bus-leader.d.ts +21 -2
- package/dist/lib/bus-leader.js +181 -25
- package/dist/lib/bus-transport.d.ts +4 -1
- package/dist/lib/bus-transport.js +28 -4
- package/dist/lib/bus.d.ts +52 -56
- package/dist/lib/bus.js +78 -246
- package/dist/lib/channel-posts.d.ts +1 -3
- package/dist/lib/channel-posts.js +23 -26
- package/dist/lib/command-templates.d.ts +0 -5
- package/dist/lib/command-templates.js +5 -5
- package/dist/lib/commands.d.ts +12 -26
- package/dist/lib/commands.js +152 -98
- package/dist/lib/config.d.ts +0 -17
- package/dist/lib/config.js +12 -14
- package/dist/lib/delivery.d.ts +0 -4
- package/dist/lib/delivery.js +2 -5
- package/dist/lib/extension.js +281 -61
- package/dist/lib/generative-apps.d.ts +0 -19
- package/dist/lib/inbound.d.ts +0 -2
- package/dist/lib/inbound.js +2 -2
- package/dist/lib/journal.d.ts +177 -16
- package/dist/lib/journal.js +1115 -249
- package/dist/lib/keyboard.d.ts +0 -2
- package/dist/lib/keyboard.js +2 -2
- package/dist/lib/lifecycle.d.ts +57 -2
- package/dist/lib/lifecycle.js +110 -5
- package/dist/lib/locks.d.ts +100 -12
- package/dist/lib/locks.js +380 -14
- package/dist/lib/logging.d.ts +30 -12
- package/dist/lib/logging.js +129 -72
- package/dist/lib/media.d.ts +28 -5
- package/dist/lib/media.js +26 -8
- package/dist/lib/menu-model.d.ts +0 -11
- package/dist/lib/menu-model.js +8 -8
- package/dist/lib/menu-settings.d.ts +0 -18
- package/dist/lib/menu-settings.js +18 -18
- package/dist/lib/menu-status.d.ts +4 -4
- package/dist/lib/menu-status.js +12 -7
- package/dist/lib/menu-thinking.d.ts +1 -1
- package/dist/lib/menu-thinking.js +1 -1
- package/dist/lib/menu.d.ts +1 -0
- package/dist/lib/menu.js +4 -3
- package/dist/lib/model.js +2 -4
- package/dist/lib/outbound-attachments.d.ts +0 -1
- package/dist/lib/outbound-attachments.js +1 -1
- package/dist/lib/outbound-buttons.js +1 -8
- package/dist/lib/outbound-markup.d.ts +1 -6
- package/dist/lib/outbound-markup.js +4 -21
- package/dist/lib/outbound.d.ts +0 -8
- package/dist/lib/outbound.js +5 -5
- package/dist/lib/paths.d.ts +50 -7
- package/dist/lib/paths.js +155 -17
- package/dist/lib/pi.d.ts +1 -0
- package/dist/lib/pi.js +2 -1
- package/dist/lib/polling.d.ts +0 -5
- package/dist/lib/polling.js +5 -5
- package/dist/lib/preview.d.ts +0 -3
- package/dist/lib/preview.js +3 -3
- package/dist/lib/prompt-templates.d.ts +0 -1
- package/dist/lib/prompt-templates.js +1 -1
- package/dist/lib/queue.d.ts +3 -15
- package/dist/lib/queue.js +69 -8
- package/dist/lib/recovery.d.ts +29 -9
- package/dist/lib/recovery.js +104 -33
- package/dist/lib/replies.d.ts +0 -4
- package/dist/lib/replies.js +1 -1
- package/dist/lib/routing.d.ts +73 -8
- package/dist/lib/routing.js +2206 -307
- package/dist/lib/sections.d.ts +0 -2
- package/dist/lib/sections.js +1 -1
- package/dist/lib/status.d.ts +46 -9
- package/dist/lib/status.js +179 -27
- package/dist/lib/sync.d.ts +0 -5
- package/dist/lib/sync.js +9 -8
- package/dist/lib/telegram-api.d.ts +11 -3
- package/dist/lib/telegram-api.js +34 -23
- package/dist/lib/thread-cleanup-manager.d.ts +1 -0
- package/dist/lib/thread-cleanup-manager.js +18 -21
- package/dist/lib/thread-naming.d.ts +32 -5
- package/dist/lib/thread-naming.js +191 -7
- package/dist/lib/thread-reconciler.d.ts +2 -0
- package/dist/lib/thread-reconciler.js +1 -1
- package/dist/lib/threads.d.ts +292 -59
- package/dist/lib/threads.js +1664 -373
- package/dist/lib/turns.d.ts +0 -1
- package/dist/lib/turns.js +1 -1
- package/dist/lib/updates.d.ts +184 -28
- package/dist/lib/updates.js +1032 -114
- package/dist/lib/wire.d.ts +12 -0
- package/dist/lib/wire.js +21 -0
- package/dist/lib/workspace-admission.d.ts +25 -17
- package/dist/lib/workspace-admission.js +95 -20
- package/dist/lib/workspace-identity.d.ts +20 -0
- package/dist/lib/workspace-identity.js +103 -0
- package/dist/lib/workspace-retirement.d.ts +45 -10
- package/dist/lib/workspace-retirement.js +196 -64
- package/dist/package.json +6 -6
- package/dist/skills/generated-control-surface/SKILL.md +3 -3
- package/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
- package/docs/README.md +1 -1
- package/docs/activity.md +1 -1
- package/docs/architecture.md +158 -39
- package/docs/callback-namespaces.md +1 -1
- package/docs/delivery.md +1 -1
- package/docs/generative-apps.md +2 -2
- package/docs/inbound.md +1 -1
- package/docs/multi-instance-bus.md +127 -19
- package/docs/public-api.md +5 -3
- package/docs/ui-style.md +26 -8
- package/docs/updates.md +1 -1
- package/lib/activity-verbosity.ts +5 -9
- package/lib/bindings.ts +23 -10
- package/lib/bus-api.ts +5 -2
- package/lib/bus-follower.ts +251 -226
- package/lib/bus-leader.ts +178 -31
- package/lib/bus-transport.ts +30 -8
- package/lib/bus.ts +110 -334
- package/lib/channel-posts.ts +29 -29
- package/lib/command-templates.ts +5 -5
- package/lib/commands.ts +150 -112
- package/lib/config.ts +12 -17
- package/lib/delivery.ts +2 -8
- package/lib/extension.ts +282 -72
- package/lib/generative-apps.ts +0 -22
- package/lib/inbound.ts +2 -2
- package/lib/journal.ts +1182 -326
- package/lib/keyboard.ts +2 -2
- package/lib/lifecycle.ts +141 -5
- package/lib/locks.ts +402 -23
- package/lib/logging.ts +151 -101
- package/lib/media.ts +51 -9
- package/lib/menu-model.ts +8 -8
- package/lib/menu-settings.ts +18 -18
- package/lib/menu-status.ts +11 -0
- package/lib/menu-thinking.ts +2 -2
- package/lib/menu.ts +5 -1
- package/lib/model.ts +2 -4
- package/lib/outbound-attachments.ts +1 -1
- package/lib/outbound-buttons.ts +1 -10
- package/lib/outbound-markup.ts +5 -24
- package/lib/outbound.ts +5 -5
- package/lib/paths.ts +177 -17
- package/lib/pi.ts +5 -2
- package/lib/polling.ts +5 -5
- package/lib/preview.ts +3 -3
- package/lib/prompt-templates.ts +1 -1
- package/lib/queue.ts +67 -9
- package/lib/recovery.ts +104 -44
- package/lib/replies.ts +1 -1
- package/lib/routing.ts +1955 -375
- package/lib/sections.ts +1 -1
- package/lib/status.ts +185 -36
- package/lib/sync.ts +8 -8
- package/lib/telegram-api.ts +48 -33
- package/lib/thread-cleanup-manager.ts +24 -21
- package/lib/thread-naming.ts +267 -9
- package/lib/thread-reconciler.ts +3 -1
- package/lib/threads.ts +1718 -491
- package/lib/turns.ts +1 -1
- package/lib/updates.ts +1072 -151
- package/lib/wire.ts +28 -0
- package/lib/workspace-admission.ts +97 -47
- package/lib/workspace-identity.ts +147 -0
- package/lib/workspace-retirement.ts +234 -102
- package/package.json +6 -6
- package/scripts/audit-exports.mjs +100 -0
- package/scripts/check-downgrade.mjs +80 -43
- package/skills/generated-control-surface/SKILL.md +3 -3
- package/skills/telegram-bridge/references/diagnosis.md +3 -3
package/lib/bus-follower.ts
CHANGED
|
@@ -7,9 +7,12 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { basename } from "node:path";
|
|
10
|
+
import { isDeepStrictEqual } from "node:util";
|
|
11
|
+
import type { TelegramWorkspaceRestoreExecutor, TelegramWorkspaceRestoreIntent } from "./threads.ts";
|
|
10
12
|
|
|
11
|
-
import * as Sync from "./sync.ts";
|
|
12
13
|
import * as Threads from "./threads.ts";
|
|
14
|
+
import * as WorkspaceIdentity from "./workspace-identity.ts";
|
|
15
|
+
import { isWireRecord as isRecord } from "./wire.ts";
|
|
13
16
|
import { parseTelegramUpdateJournalQueueOwner, type TelegramUpdateJournalStoreOptions } from "./journal.ts";
|
|
14
17
|
import {
|
|
15
18
|
TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS,
|
|
@@ -28,7 +31,6 @@ import {
|
|
|
28
31
|
} from "./telegram-api.ts";
|
|
29
32
|
import {
|
|
30
33
|
createTelegramBusFollowerDeliveryIdentity,
|
|
31
|
-
createTelegramBusFollowerTargetController,
|
|
32
34
|
createTelegramBusForeignOwnedUpdateForwarder,
|
|
33
35
|
createTelegramBusLocalServer,
|
|
34
36
|
createTelegramBusRequestIdFactory,
|
|
@@ -42,6 +44,7 @@ import {
|
|
|
42
44
|
type TelegramBusAgentMessage,
|
|
43
45
|
type TelegramBusAgentTargetSelector,
|
|
44
46
|
type TelegramBusEnvelope,
|
|
47
|
+
type TelegramBusWorkspaceRestoreObservation,
|
|
45
48
|
type TelegramBusForwardOwnership,
|
|
46
49
|
type TelegramBusProtocolIdentity,
|
|
47
50
|
type TelegramBusSocketPathSource,
|
|
@@ -49,6 +52,7 @@ import {
|
|
|
49
52
|
TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
|
|
50
53
|
TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
|
|
51
54
|
TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
55
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
|
|
52
56
|
} from "./bus.ts";
|
|
53
57
|
import type { TelegramConfigStore, TelegramThreadDisplayMode } from "./config.ts";
|
|
54
58
|
import {
|
|
@@ -61,15 +65,15 @@ import {
|
|
|
61
65
|
type TelegramWorkspaceAdmissionLedger,
|
|
62
66
|
} from "./workspace-admission.ts";
|
|
63
67
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
|
|
69
|
+
const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
|
|
70
|
+
const TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS = 30_000;
|
|
71
|
+
const TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS = 30_000;
|
|
68
72
|
export const TELEGRAM_BUS_FOLLOWER_HEARTBEAT_TIMEOUT_MS =
|
|
69
73
|
TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS;
|
|
70
|
-
|
|
74
|
+
const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS =
|
|
71
75
|
TELEGRAM_BUS_REGISTRATION_RETRY.attempts;
|
|
72
|
-
|
|
76
|
+
const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS =
|
|
73
77
|
TELEGRAM_BUS_REGISTRATION_RETRY.delayMs;
|
|
74
78
|
|
|
75
79
|
const TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY =
|
|
@@ -112,7 +116,7 @@ export function setTelegramFollowerSessionHandoff(
|
|
|
112
116
|
else store[TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY] = handoff;
|
|
113
117
|
}
|
|
114
118
|
|
|
115
|
-
|
|
119
|
+
function isTelegramFollowerSessionHandoffFresh(
|
|
116
120
|
handoff: TelegramFollowerSessionHandoff | undefined,
|
|
117
121
|
options: { pid?: number; nowMs?: number; ttlMs?: number } = {},
|
|
118
122
|
): handoff is TelegramFollowerSessionHandoff {
|
|
@@ -206,6 +210,8 @@ export interface TelegramBusFollowerRegistrationState {
|
|
|
206
210
|
getDisplayTitle: () => string | undefined;
|
|
207
211
|
setDisplayTitle: (title: string, generation: string) => boolean;
|
|
208
212
|
getGeneration: () => string | undefined;
|
|
213
|
+
/** Return the acknowledged session only when it matches the caller's current context. */
|
|
214
|
+
getSessionId: (currentSessionId: string | undefined) => string | undefined;
|
|
209
215
|
beginRecovery: () => number;
|
|
210
216
|
cancelRecovery: () => void;
|
|
211
217
|
waitForGeneration: (timeoutMs?: number) => Promise<string | undefined>;
|
|
@@ -220,6 +226,7 @@ export interface TelegramBusFollowerRegistrationState {
|
|
|
220
226
|
threadName?: string;
|
|
221
227
|
displayTitle?: string;
|
|
222
228
|
generation?: string;
|
|
229
|
+
sessionId?: string;
|
|
223
230
|
leaderProtocol?: TelegramBusProtocolIdentity;
|
|
224
231
|
},
|
|
225
232
|
) => void;
|
|
@@ -481,30 +488,6 @@ export function createTelegramBusFollowerPromotionHandler<
|
|
|
481
488
|
);
|
|
482
489
|
}
|
|
483
490
|
|
|
484
|
-
export interface TelegramBusFollowerTargetReplacementHandlerDeps<TContext> {
|
|
485
|
-
topicTargetStore: Pick<
|
|
486
|
-
Threads.TelegramTopicTargetStore,
|
|
487
|
-
"load" | "list" | "markStaleByTarget" | "upsert" | "persist"
|
|
488
|
-
>;
|
|
489
|
-
registrationState: Pick<
|
|
490
|
-
TelegramBusFollowerRegistrationState,
|
|
491
|
-
"getTarget" | "getSlot" | "setRegistered" | "getGeneration"
|
|
492
|
-
>;
|
|
493
|
-
instanceId: string;
|
|
494
|
-
getManualFollowerProfileKey: () => string;
|
|
495
|
-
manualFollowerOwnerId: string;
|
|
496
|
-
getSyncState: () => Sync.TelegramSyncState;
|
|
497
|
-
setSyncState: (state: Sync.TelegramSyncState) => void;
|
|
498
|
-
getNowMs?: () => number;
|
|
499
|
-
updateStatus: (ctx: TContext) => void;
|
|
500
|
-
recordRuntimeEvent?: (
|
|
501
|
-
category: string,
|
|
502
|
-
error: unknown,
|
|
503
|
-
details?: Record<string, unknown>,
|
|
504
|
-
) => void;
|
|
505
|
-
getWorkspaceAdmission?: TelegramBusFollowerWorkspaceAdmissionDeps["getWorkspaceAdmission"];
|
|
506
|
-
}
|
|
507
|
-
|
|
508
491
|
export type TelegramBusFollowerLeaderState =
|
|
509
492
|
| { kind: "inactive" }
|
|
510
493
|
| { kind: "active-here"; lock: TelegramBusFollowerLeaderLock }
|
|
@@ -571,15 +554,9 @@ export interface TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext> {
|
|
|
571
554
|
>,
|
|
572
555
|
ctx: TContext,
|
|
573
556
|
) => Promise<TelegramQueueHandoffStageResult> | TelegramQueueHandoffStageResult;
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
oldTarget?: TelegramTarget & { threadId: number };
|
|
578
|
-
reason: "thread-restore";
|
|
579
|
-
registrationGeneration: string;
|
|
580
|
-
},
|
|
581
|
-
ctx: TContext,
|
|
582
|
-
) => Promise<void> | void;
|
|
557
|
+
isWorkspaceRestoreEnabled?: () => boolean;
|
|
558
|
+
handleWorkspaceRestore?: (input: { operationId: string; registrationGeneration: string; mode: "apply" | "inspect" },
|
|
559
|
+
ctx: TContext) => Promise<TelegramBusWorkspaceRestoreObservation>;
|
|
583
560
|
recordRuntimeEvent?: (
|
|
584
561
|
category: string,
|
|
585
562
|
error: unknown,
|
|
@@ -590,6 +567,7 @@ export interface TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext> {
|
|
|
590
567
|
export interface TelegramBusFollowerRuntimeAssembly<TContext> {
|
|
591
568
|
receiver: TelegramBusForwardedUpdateReceiverRuntime;
|
|
592
569
|
registration: TelegramBusFollowerRegistrationRuntime<TContext>;
|
|
570
|
+
getReadySessionId: () => string | undefined;
|
|
593
571
|
}
|
|
594
572
|
|
|
595
573
|
export interface TelegramBusFollowerRuntimeAssemblyPorts<
|
|
@@ -604,15 +582,10 @@ export interface TelegramBusFollowerRuntimeAssemblyPorts<
|
|
|
604
582
|
) => void;
|
|
605
583
|
receiver: Omit<
|
|
606
584
|
TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext>,
|
|
607
|
-
| "handleReplaceTarget"
|
|
608
585
|
| "instanceId"
|
|
609
586
|
| "recordRuntimeEvent"
|
|
610
587
|
| "getRegistrationGeneration"
|
|
611
588
|
>;
|
|
612
|
-
targetReplacement: Omit<
|
|
613
|
-
TelegramBusFollowerTargetReplacementHandlerDeps<TContext>,
|
|
614
|
-
"registrationState" | "instanceId" | "recordRuntimeEvent"
|
|
615
|
-
>;
|
|
616
589
|
recovery: Omit<
|
|
617
590
|
TelegramBusFollowerHeartbeatRecoveryHandlerDeps<TContext>,
|
|
618
591
|
"getRegistrationRuntime" | "registrationState" | "recordRuntimeEvent"
|
|
@@ -637,20 +610,25 @@ export function createTelegramBusFollowerRuntimeAssembly<
|
|
|
637
610
|
ports: TelegramBusFollowerRuntimeAssemblyPorts<TContext>,
|
|
638
611
|
): TelegramBusFollowerRuntimeAssembly<TContext> {
|
|
639
612
|
// Binding startup needs registration identity before it can grant inbound authority.
|
|
640
|
-
let readyContext: { generation: string; ctx: TContext; sessionGeneration: number | undefined } | undefined;
|
|
613
|
+
let readyContext: { generation: string; ctx: TContext; sessionGeneration: number | undefined; sessionId: string | undefined } | undefined;
|
|
641
614
|
const isReadyContext = (ctx?: TContext): boolean => Boolean(readyContext &&
|
|
642
615
|
(ctx === undefined || readyContext.ctx === ctx) &&
|
|
643
616
|
ports.registrationState.getGeneration() === readyContext.generation &&
|
|
644
617
|
ports.registration.getSessionGeneration?.() === readyContext.sessionGeneration &&
|
|
618
|
+
ports.registration.getSessionId?.(readyContext.ctx) === readyContext.sessionId &&
|
|
645
619
|
ports.registration.isContextActive?.(readyContext.ctx) !== false);
|
|
646
620
|
const prepareContext = async (ctx: TContext): Promise<void> => {
|
|
647
621
|
const generation = ports.registrationState.getGeneration();
|
|
648
622
|
const sessionGeneration = ports.registration.getSessionGeneration?.();
|
|
623
|
+
const sessionId = ports.registration.getSessionId?.(ctx);
|
|
649
624
|
readyContext = undefined;
|
|
625
|
+
if (!generation || (sessionId !== undefined && ports.registrationState.getSessionId(sessionId) !== sessionId))
|
|
626
|
+
throw new Error("Telegram follower session change requires acknowledged leader registration.");
|
|
650
627
|
await ports.registration.onRegistered?.(ctx);
|
|
651
628
|
if (generation && ports.registrationState.getGeneration() === generation &&
|
|
652
629
|
ports.registration.getSessionGeneration?.() === sessionGeneration &&
|
|
653
|
-
ports.registration.
|
|
630
|
+
ports.registration.getSessionId?.(ctx) === sessionId &&
|
|
631
|
+
ports.registration.isContextActive?.(ctx) !== false) readyContext = { generation, ctx, sessionGeneration, sessionId };
|
|
654
632
|
};
|
|
655
633
|
const sharedRuntimeDeps = {
|
|
656
634
|
instanceId: ports.instanceId,
|
|
@@ -664,10 +642,6 @@ export function createTelegramBusFollowerRuntimeAssembly<
|
|
|
664
642
|
getRegistrationGeneration() {
|
|
665
643
|
return isReadyContext() ? readyContext?.generation : undefined;
|
|
666
644
|
},
|
|
667
|
-
handleReplaceTarget: createTelegramBusFollowerTargetReplacementHandler({
|
|
668
|
-
...ports.targetReplacement,
|
|
669
|
-
...sharedRuntimeDeps,
|
|
670
|
-
}),
|
|
671
645
|
});
|
|
672
646
|
let registration: TelegramBusFollowerRegistrationRuntime<TContext>;
|
|
673
647
|
const recovery = createTelegramBusFollowerHeartbeatRecoveryHandler({
|
|
@@ -696,115 +670,146 @@ export function createTelegramBusFollowerRuntimeAssembly<
|
|
|
696
670
|
if (!isReadyContext(ctx)) await prepareContext(ctx);
|
|
697
671
|
},
|
|
698
672
|
};
|
|
699
|
-
return { receiver, registration };
|
|
673
|
+
return { receiver, registration, getReadySessionId: () => isReadyContext() ? readyContext?.sessionId : undefined };
|
|
700
674
|
}
|
|
701
675
|
|
|
702
|
-
export
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
676
|
+
export interface TelegramBusFollowerRestoreContext {
|
|
677
|
+
executor: TelegramWorkspaceRestoreExecutor;
|
|
678
|
+
profileBindingKey: string;
|
|
679
|
+
operatorUserId: number;
|
|
680
|
+
cwd: string;
|
|
681
|
+
sessionId: string;
|
|
682
|
+
generation: string | number;
|
|
683
|
+
leaderProtocol?: TelegramBusProtocolIdentity;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** Binds Restore to real Pi lifetime and the current authenticated transport owner, not request fields. */
|
|
687
|
+
export function createTelegramBusFollowerRestoreContextGetter<TContext>(deps: {
|
|
688
|
+
isContextCurrent: (ctx: TContext) => boolean;
|
|
689
|
+
getSessionId: (ctx: TContext) => string | undefined;
|
|
690
|
+
getCwd: (ctx: TContext) => string | undefined;
|
|
691
|
+
getGeneration: () => number;
|
|
692
|
+
getProfileBindingKey: () => string | undefined;
|
|
693
|
+
getOperatorUserId: () => number | undefined;
|
|
694
|
+
getLeaderState: () => TelegramLockState;
|
|
695
|
+
getAuthenticatedSecret: () => string | undefined;
|
|
696
|
+
getLeaderProtocol: () => TelegramBusProtocolIdentity | undefined;
|
|
697
|
+
}) {
|
|
698
|
+
return (ctx: TContext): TelegramBusFollowerRestoreContext | undefined => {
|
|
699
|
+
if (!deps.isContextCurrent(ctx)) return undefined;
|
|
700
|
+
const leader = deps.getLeaderState();
|
|
701
|
+
const secret = deps.getAuthenticatedSecret();
|
|
702
|
+
const leaderProtocol = deps.getLeaderProtocol();
|
|
703
|
+
if (!leaderProtocol || !hasTelegramBusCapability(leaderProtocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE)) return undefined;
|
|
704
|
+
if (leader.kind !== "active-elsewhere" || !secret || leader.lock.busSecret !== secret ||
|
|
705
|
+
!leader.lock.instanceId || leader.lock.leaderEpoch === undefined) return undefined;
|
|
706
|
+
const profileBindingKey = deps.getProfileBindingKey();
|
|
707
|
+
const operatorUserId = deps.getOperatorUserId();
|
|
708
|
+
const sessionId = deps.getSessionId(ctx);
|
|
709
|
+
const cwd = deps.getCwd(ctx);
|
|
710
|
+
const generation = deps.getGeneration();
|
|
711
|
+
if (!profileBindingKey || !sessionId || !cwd || typeof operatorUserId !== "number" ||
|
|
712
|
+
!Number.isSafeInteger(operatorUserId) || operatorUserId <= 0 ||
|
|
713
|
+
!Number.isSafeInteger(generation) || generation < 0 || !deps.isContextCurrent(ctx)) return undefined;
|
|
714
|
+
return { executor: { instanceId: leader.lock.instanceId, leaderEpoch: String(leader.lock.leaderEpoch) },
|
|
715
|
+
profileBindingKey, operatorUserId, sessionId, cwd, generation, leaderProtocol: structuredClone(leaderProtocol) };
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/** Prepared receiver: its caller authenticates the envelope; no canonical writes, dispatch or cleanup. */
|
|
720
|
+
export function createTelegramBusFollowerWorkspaceRestoreHandler<TContext>(deps: {
|
|
721
|
+
instanceId: string;
|
|
722
|
+
/** Captures actual Pi lifetime and authenticated leader/profile authority, never request-derived values. */
|
|
723
|
+
getContextAuthority: (ctx: TContext) => TelegramBusFollowerRestoreContext | undefined;
|
|
724
|
+
readRestoreIntent: (operationId: string, profileBindingKey: string) => TelegramWorkspaceRestoreIntent | undefined;
|
|
725
|
+
topicTargetStore: Pick<Threads.TelegramTopicTargetStore, "load" | "withWorkspaceRestoreSnapshot">;
|
|
726
|
+
registrationState: Pick<TelegramBusFollowerRegistrationState, "getTarget" | "getSlot" | "getGeneration" | "setRegistered" | "getLeaderProtocol">;
|
|
727
|
+
getWorkspaceAdmission: NonNullable<TelegramBusFollowerWorkspaceAdmissionDeps["getWorkspaceAdmission"]>;
|
|
728
|
+
recordRuntimeEvent?: TelegramBusFollowerWorkspaceAdmissionDeps["recordRuntimeEvent"];
|
|
729
|
+
}) {
|
|
730
|
+
return async (input: { operationId: string; registrationGeneration: string; mode: "apply" | "inspect" }, ctx: TContext) => {
|
|
731
|
+
const captured = deps.getContextAuthority(ctx);
|
|
732
|
+
if (!captured) throw new Error("Stale Telegram follower Restore context.");
|
|
733
|
+
const authority = structuredClone(captured);
|
|
734
|
+
const request = structuredClone(input);
|
|
735
|
+
if (!authority.profileBindingKey || !authority.sessionId || !authority.cwd ||
|
|
736
|
+
!authority.executor?.instanceId || !authority.executor.leaderEpoch ||
|
|
737
|
+
!(typeof authority.generation === "string" ? authority.generation.length > 0 :
|
|
738
|
+
Number.isSafeInteger(authority.generation) && authority.generation >= 0) ||
|
|
739
|
+
!Number.isSafeInteger(authority.operatorUserId) || authority.operatorUserId <= 0 ||
|
|
740
|
+
!request.operationId || !request.registrationGeneration || !["apply", "inspect"].includes(request.mode))
|
|
741
|
+
throw new Error("Invalid Telegram follower Restore authority.");
|
|
742
|
+
const assertCurrent = (): void => {
|
|
743
|
+
if (!isDeepStrictEqual(deps.getContextAuthority(ctx), authority) ||
|
|
744
|
+
deps.registrationState.getGeneration() !== request.registrationGeneration)
|
|
745
|
+
throw new Error("Stale Telegram follower Restore authority.");
|
|
746
|
+
};
|
|
747
|
+
assertCurrent();
|
|
748
|
+
if (!deps.getWorkspaceAdmission) throw new Error("Telegram Workspace admission authority is unavailable.");
|
|
749
|
+
const result = await runTelegramBusFollowerWorkspaceMutation(deps, "workspace.restore-follower-target", async () => {
|
|
730
750
|
assertCurrent();
|
|
751
|
+
const intent = deps.readRestoreIntent(request.operationId, authority.profileBindingKey);
|
|
752
|
+
assertCurrent();
|
|
753
|
+
if (!intent || intent.request.operationId !== request.operationId ||
|
|
754
|
+
(intent.phase !== "recipient-issued" && intent.phase !== "ready") ||
|
|
755
|
+
!isDeepStrictEqual(intent.executor, authority.executor) ||
|
|
756
|
+
intent.operatorUserId !== authority.operatorUserId || !intent.recipient ||
|
|
757
|
+
(request.mode === "apply" && (intent.recipient.kind !== "follower" || intent.recipient.instanceId !== deps.instanceId ||
|
|
758
|
+
intent.recipient.generation !== request.registrationGeneration)) ||
|
|
759
|
+
intent.recipient.sessionId !== authority.sessionId || intent.request.binding.sessionId !== authority.sessionId ||
|
|
760
|
+
intent.request.binding.cwd !== WorkspaceIdentity.normalizeTelegramWorkspacePath(authority.cwd))
|
|
761
|
+
throw new Error("Telegram follower Restore intent does not match this recipient.");
|
|
762
|
+
const expected = structuredClone(intent);
|
|
731
763
|
await deps.topicTargetStore.load();
|
|
732
764
|
assertCurrent();
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
(
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
await deps.topicTargetStore.persist();
|
|
782
|
-
assertCurrent(input.target);
|
|
783
|
-
deps.setSyncState(
|
|
784
|
-
Sync.markTelegramSyncSliceFresh(
|
|
785
|
-
deps.getSyncState(),
|
|
786
|
-
"target-bindings",
|
|
787
|
-
{
|
|
788
|
-
nowMs,
|
|
789
|
-
action: "follower-thread-restore",
|
|
790
|
-
},
|
|
791
|
-
),
|
|
792
|
-
);
|
|
793
|
-
deps.updateStatus(ctx);
|
|
794
|
-
deps.recordRuntimeEvent?.(
|
|
795
|
-
"bus",
|
|
796
|
-
"Telegram follower thread target replaced",
|
|
797
|
-
{
|
|
798
|
-
phase: "follower-thread-restore",
|
|
799
|
-
chatId: input.target.chatId,
|
|
800
|
-
threadId: input.target.threadId,
|
|
801
|
-
oldThreadId:
|
|
802
|
-
input.oldTarget?.threadId ?? currentRecord?.target.threadId,
|
|
803
|
-
slot: currentSlot,
|
|
804
|
-
},
|
|
805
|
-
);
|
|
806
|
-
},
|
|
807
|
-
);
|
|
765
|
+
const observe = (canonical: Readonly<Pick<Threads.TelegramTopicTargetFile, "threads" | "workspaceBindings">>): TelegramBusWorkspaceRestoreObservation => {
|
|
766
|
+
if (!isDeepStrictEqual(deps.readRestoreIntent(request.operationId, authority.profileBindingKey), expected))
|
|
767
|
+
throw new Error("Telegram follower Restore intent changed.");
|
|
768
|
+
assertCurrent();
|
|
769
|
+
const { binding, target } = expected.request;
|
|
770
|
+
const bindings = (canonical.workspaceBindings ?? []).filter(value => value.bindingKey === binding.bindingKey);
|
|
771
|
+
const snapshot = canonical.threads;
|
|
772
|
+
const records = snapshot.filter(value => value.instanceId === deps.instanceId);
|
|
773
|
+
const owners = snapshot.filter(value => value.status === "active" &&
|
|
774
|
+
(value.slot === binding.slot || isDeepStrictEqual(value.target, target)));
|
|
775
|
+
if (bindings.length !== 1 || records.length !== 1 || owners.length !== 1 || bindings[0]?.cwd !== binding.cwd ||
|
|
776
|
+
bindings[0]?.sessionId !== binding.sessionId || bindings[0]?.slot !== binding.slot || bindings[0]?.inactiveSinceMs !== undefined ||
|
|
777
|
+
!isDeepStrictEqual(bindings[0]?.target, target) || records[0]?.status !== "active" ||
|
|
778
|
+
records[0]?.owner?.kind !== "manual-follower" ||
|
|
779
|
+
records[0]?.slot !== binding.slot || !isDeepStrictEqual(records[0]?.target, target) ||
|
|
780
|
+
deps.registrationState.getSlot() !== binding.slot)
|
|
781
|
+
throw new Error("Telegram follower Restore canonical binding is not committed.");
|
|
782
|
+
const localTarget = deps.registrationState.getTarget();
|
|
783
|
+
const alreadyReady = isDeepStrictEqual(localTarget, target);
|
|
784
|
+
if (!alreadyReady && !isDeepStrictEqual(localTarget, binding.target))
|
|
785
|
+
throw new Error("Telegram follower Restore local target changed.");
|
|
786
|
+
assertCurrent();
|
|
787
|
+
if (!alreadyReady && request.mode === "apply") {
|
|
788
|
+
if (expected.phase !== "recipient-issued") throw new Error("Telegram follower Restore readiness regressed.");
|
|
789
|
+
deps.registrationState.setRegistered(true, target, { slot: binding.slot,
|
|
790
|
+
threadName: records[0]?.threadName, generation: request.registrationGeneration,
|
|
791
|
+
leaderProtocol: deps.registrationState.getLeaderProtocol() });
|
|
792
|
+
}
|
|
793
|
+
assertCurrent();
|
|
794
|
+
const observedTarget = deps.registrationState.getTarget();
|
|
795
|
+
if (!observedTarget || (!isDeepStrictEqual(observedTarget, target) && !isDeepStrictEqual(observedTarget, binding.target)))
|
|
796
|
+
throw new Error("Telegram follower Restore local target changed.");
|
|
797
|
+
const ready = isDeepStrictEqual(observedTarget, target);
|
|
798
|
+
assertCurrent();
|
|
799
|
+
return { operationId: request.operationId, recipient: { kind: "follower" as const, instanceId: deps.instanceId,
|
|
800
|
+
sessionId: authority.sessionId, generation: request.registrationGeneration },
|
|
801
|
+
target: { chatId: observedTarget.chatId, threadId: observedTarget.threadId! }, slot: binding.slot!, ready };
|
|
802
|
+
};
|
|
803
|
+
let result: TelegramBusWorkspaceRestoreObservation | undefined;
|
|
804
|
+
deps.topicTargetStore.withWorkspaceRestoreSnapshot(expected, snapshot => { result = observe(snapshot); });
|
|
805
|
+
if (!result) throw new Error("Telegram follower Restore observation was not completed.");
|
|
806
|
+
return result;
|
|
807
|
+
});
|
|
808
|
+
assertCurrent();
|
|
809
|
+
if (!isDeepStrictEqual(deps.registrationState.getTarget(), result.target) || deps.registrationState.getSlot() !== result.slot)
|
|
810
|
+
throw new Error("Telegram follower Restore local registration changed after admission release.");
|
|
811
|
+
return result;
|
|
812
|
+
};
|
|
808
813
|
}
|
|
809
814
|
|
|
810
815
|
export function createTelegramBusFollowerClientRuntime<
|
|
@@ -855,10 +860,6 @@ export function createTelegramBusFollowerClientRuntime<
|
|
|
855
860
|
getAuthSecret: deps.getApiAuthSecret,
|
|
856
861
|
getRegistrationGeneration: deps.getRegistrationGeneration,
|
|
857
862
|
}),
|
|
858
|
-
targetController: createTelegramBusFollowerTargetController({
|
|
859
|
-
...sharedClientDeps,
|
|
860
|
-
getAuthSecret: deps.getForwardingAuthSecret,
|
|
861
|
-
}),
|
|
862
863
|
};
|
|
863
864
|
}
|
|
864
865
|
|
|
@@ -939,7 +940,7 @@ export function createTelegramBusFollowerQueueHandoffClient(
|
|
|
939
940
|
};
|
|
940
941
|
}
|
|
941
942
|
|
|
942
|
-
|
|
943
|
+
function createTelegramBusAgentMessageClient(
|
|
943
944
|
deps: TelegramBusFollowerApiCallerDeps,
|
|
944
945
|
): {
|
|
945
946
|
resolveTarget: (
|
|
@@ -1130,10 +1131,16 @@ function isTelegramStaleContextError(error: unknown): boolean {
|
|
|
1130
1131
|
|
|
1131
1132
|
export function createTelegramBusFollowerSessionReplacementSuspender(
|
|
1132
1133
|
deps: TelegramBusFollowerSessionReplacementSuspenderDeps,
|
|
1133
|
-
): () => Promise<void> {
|
|
1134
|
+
): (preserveTarget?: boolean) => Promise<void> {
|
|
1134
1135
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
1135
1136
|
const getPid = deps.getPid ?? (() => process.pid);
|
|
1136
|
-
return async () => {
|
|
1137
|
+
return async (preserveTarget = true) => {
|
|
1138
|
+
if (!preserveTarget) {
|
|
1139
|
+
setTelegramFollowerSessionHandoff(undefined);
|
|
1140
|
+
Threads.setTelegramLeaderSessionHandoff(undefined);
|
|
1141
|
+
await deps.suspendPolling();
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1137
1144
|
const target = deps.registrationState.getTarget();
|
|
1138
1145
|
if (deps.registrationState.isRegistered() && target) {
|
|
1139
1146
|
setTelegramFollowerSessionHandoff({
|
|
@@ -1278,6 +1285,7 @@ export function createTelegramBusFollowerRegistrationState(
|
|
|
1278
1285
|
let threadName: string | undefined;
|
|
1279
1286
|
let displayTitle: string | undefined;
|
|
1280
1287
|
let generation: string | undefined;
|
|
1288
|
+
let sessionId: string | undefined;
|
|
1281
1289
|
let leaderProtocol: TelegramBusProtocolIdentity | undefined;
|
|
1282
1290
|
let eligibleElectionSlots: string[] = [];
|
|
1283
1291
|
let recoveryEpoch = 0;
|
|
@@ -1307,6 +1315,7 @@ export function createTelegramBusFollowerRegistrationState(
|
|
|
1307
1315
|
return true;
|
|
1308
1316
|
},
|
|
1309
1317
|
getGeneration: () => generation,
|
|
1318
|
+
getSessionId: currentSessionId => registered && generation && currentSessionId === sessionId ? sessionId : undefined,
|
|
1310
1319
|
beginRecovery: () => {
|
|
1311
1320
|
if (activeRecoveryEpoch !== undefined) return activeRecoveryEpoch;
|
|
1312
1321
|
activeRecoveryEpoch = ++recoveryEpoch;
|
|
@@ -1348,6 +1357,7 @@ export function createTelegramBusFollowerRegistrationState(
|
|
|
1348
1357
|
generation === metadata?.generation &&
|
|
1349
1358
|
target?.chatId === nextTarget?.chatId && target?.threadId === nextTarget?.threadId
|
|
1350
1359
|
? displayTitle : undefined;
|
|
1360
|
+
const retainedSessionId = next && registered && generation === metadata?.generation ? sessionId : undefined;
|
|
1351
1361
|
const availabilityChanged = registered !== next;
|
|
1352
1362
|
registered = next;
|
|
1353
1363
|
target = next ? (nextTarget ? { ...nextTarget } : undefined) : undefined;
|
|
@@ -1357,6 +1367,7 @@ export function createTelegramBusFollowerRegistrationState(
|
|
|
1357
1367
|
metadata.displayTitle?.trim() && metadata.displayTitle.length <= 128
|
|
1358
1368
|
? metadata.displayTitle : retainedDisplayTitle;
|
|
1359
1369
|
generation = next ? metadata?.generation : undefined;
|
|
1370
|
+
sessionId = next ? metadata?.sessionId ?? retainedSessionId : undefined;
|
|
1360
1371
|
leaderProtocol =
|
|
1361
1372
|
next && metadata?.leaderProtocol
|
|
1362
1373
|
? {
|
|
@@ -1650,7 +1661,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1650
1661
|
let activeLeaderSocketPath: string | undefined;
|
|
1651
1662
|
let activeAuthSecret: string | undefined;
|
|
1652
1663
|
let activeRegistrationGeneration: string | undefined;
|
|
1653
|
-
let registrationAttempt: { ctx: TContext; sessionGeneration: number | undefined } | undefined;
|
|
1664
|
+
let registrationAttempt: { ctx: TContext; sessionGeneration: number | undefined; sessionId: string | undefined } | undefined;
|
|
1654
1665
|
let activeContext: TContext | undefined;
|
|
1655
1666
|
let lastKnownTarget: TelegramTarget | undefined;
|
|
1656
1667
|
let lastKnownSlot: string | undefined;
|
|
@@ -1787,10 +1798,12 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1787
1798
|
registerWithLeader: async (ctx, leader, options) => {
|
|
1788
1799
|
if (deps.isContextActive?.(ctx) === false) return false;
|
|
1789
1800
|
const sessionGeneration = deps.getSessionGeneration?.();
|
|
1790
|
-
const
|
|
1801
|
+
const sessionId = deps.getSessionId?.(ctx);
|
|
1802
|
+
const attempt = { ctx, sessionGeneration, sessionId };
|
|
1791
1803
|
registrationAttempt = attempt;
|
|
1792
1804
|
const isCurrentRequest = () => registrationAttempt === attempt &&
|
|
1793
|
-
deps.getSessionGeneration?.() === sessionGeneration && deps.
|
|
1805
|
+
deps.getSessionGeneration?.() === sessionGeneration && deps.getSessionId?.(ctx) === sessionId &&
|
|
1806
|
+
deps.isContextActive?.(ctx) !== false;
|
|
1794
1807
|
const abandonOwnedRequest = async (): Promise<void> => {
|
|
1795
1808
|
if (registrationAttempt !== attempt) return;
|
|
1796
1809
|
registrationAttempt = undefined;
|
|
@@ -1853,7 +1866,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1853
1866
|
? { slot: deps.registrationState?.getSlot() ?? lastKnownSlot }
|
|
1854
1867
|
: {}),
|
|
1855
1868
|
cwd: ctx.cwd,
|
|
1856
|
-
sessionId
|
|
1869
|
+
sessionId,
|
|
1857
1870
|
pid: getPid(),
|
|
1858
1871
|
processBirthId: deps.getProcessBirthId?.(),
|
|
1859
1872
|
sessionGeneration,
|
|
@@ -1904,7 +1917,9 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1904
1917
|
local: deps.protocolIdentity,
|
|
1905
1918
|
remote: response?.kind === "bus.ack" ? response.protocol : undefined,
|
|
1906
1919
|
});
|
|
1907
|
-
|
|
1920
|
+
// An identity-less error ACK is a rejection/transport failure, not evidence of an incompatible leader.
|
|
1921
|
+
const unidentifiedRejection = response?.kind === "bus.ack" && !response.ok && response.protocol === undefined;
|
|
1922
|
+
if (!compatibility.compatible && !unidentifiedRejection) {
|
|
1908
1923
|
stopHeartbeat();
|
|
1909
1924
|
activeLeaderSocketPath = undefined;
|
|
1910
1925
|
activeAuthSecret = undefined;
|
|
@@ -1942,6 +1957,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1942
1957
|
deps.registrationState?.setRegistered(true, registrationResult.target, {
|
|
1943
1958
|
...registrationResult,
|
|
1944
1959
|
generation: registrationGeneration,
|
|
1960
|
+
sessionId,
|
|
1945
1961
|
...(response.protocol
|
|
1946
1962
|
? { leaderProtocol: response.protocol }
|
|
1947
1963
|
: {}),
|
|
@@ -1988,7 +2004,8 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1988
2004
|
},
|
|
1989
2005
|
setContext(ctx) {
|
|
1990
2006
|
if (registrationAttempt && (registrationAttempt.ctx !== ctx ||
|
|
1991
|
-
registrationAttempt.sessionGeneration !== deps.getSessionGeneration?.()
|
|
2007
|
+
registrationAttempt.sessionGeneration !== deps.getSessionGeneration?.() ||
|
|
2008
|
+
registrationAttempt.sessionId !== deps.getSessionId?.(ctx))) registrationAttempt = undefined;
|
|
1992
2009
|
activeContext = ctx;
|
|
1993
2010
|
},
|
|
1994
2011
|
async setThreadDisplayMode(mode) {
|
|
@@ -2320,7 +2337,20 @@ export function createTelegramBusFollowerDurableAdmissionRuntime<TContext>(deps:
|
|
|
2320
2337
|
): unknown;
|
|
2321
2338
|
};
|
|
2322
2339
|
signalWorker: (ctx: TContext) => void;
|
|
2340
|
+
getNowMs?: () => number;
|
|
2341
|
+
/** Bounded process-local replay window; omit for the production 24 h / 4,096-delivery defaults. */
|
|
2342
|
+
recentDeliveryLimit?: { maxAgeMs: number; maxEntries: number };
|
|
2323
2343
|
}): TelegramBusFollowerDurableAdmissionPort<TContext> {
|
|
2344
|
+
// The journal forgets a delivery once its worker completes; this window absorbs a leader retry after a lost ACK.
|
|
2345
|
+
const recentDeliveries = new Map<string, number>();
|
|
2346
|
+
const getNowMs = deps.getNowMs ?? Date.now;
|
|
2347
|
+
const limit = deps.recentDeliveryLimit ?? { maxAgeMs: 24 * 60 * 60 * 1000, maxEntries: 4096 };
|
|
2348
|
+
const pruneRecentDeliveries = (nowMs: number): void => {
|
|
2349
|
+
for (const [deliveryId, admittedAtMs] of recentDeliveries) {
|
|
2350
|
+
if (recentDeliveries.size <= limit.maxEntries && nowMs - admittedAtMs < limit.maxAgeMs) break;
|
|
2351
|
+
recentDeliveries.delete(deliveryId);
|
|
2352
|
+
}
|
|
2353
|
+
};
|
|
2324
2354
|
return {
|
|
2325
2355
|
async admit(envelope, ctx) {
|
|
2326
2356
|
const delivery = envelope.delivery;
|
|
@@ -2367,12 +2397,17 @@ export function createTelegramBusFollowerDurableAdmissionRuntime<TContext>(deps:
|
|
|
2367
2397
|
? { message: carrier }
|
|
2368
2398
|
: { edited_message: carrier }),
|
|
2369
2399
|
};
|
|
2370
|
-
|
|
2371
|
-
deps.signalWorker(ctx);
|
|
2372
|
-
return {
|
|
2400
|
+
const result = {
|
|
2373
2401
|
deliveryId: delivery.deliveryId,
|
|
2374
2402
|
sourceUpdateId: delivery.sourceUpdateId,
|
|
2375
2403
|
};
|
|
2404
|
+
pruneRecentDeliveries(getNowMs());
|
|
2405
|
+
if (recentDeliveries.has(delivery.deliveryId)) return result;
|
|
2406
|
+
deps.journal.appendBatch([update]);
|
|
2407
|
+
recentDeliveries.set(delivery.deliveryId, getNowMs());
|
|
2408
|
+
pruneRecentDeliveries(getNowMs());
|
|
2409
|
+
deps.signalWorker(ctx);
|
|
2410
|
+
return result;
|
|
2376
2411
|
},
|
|
2377
2412
|
};
|
|
2378
2413
|
}
|
|
@@ -2491,7 +2526,7 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
|
|
|
2491
2526
|
envelope.kind !== "leader.forwardEditedMessage" &&
|
|
2492
2527
|
envelope.kind !== "leader.wakeInputCustody" &&
|
|
2493
2528
|
envelope.kind !== "leader.offerInputCustodyHandoff" &&
|
|
2494
|
-
envelope.kind !== "leader.
|
|
2529
|
+
envelope.kind !== "leader.workspaceRestore" &&
|
|
2495
2530
|
envelope.kind !== "leader.offerQueueHandoff") ||
|
|
2496
2531
|
envelope.recipientInstanceId !== deps.instanceId
|
|
2497
2532
|
) {
|
|
@@ -2514,13 +2549,29 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
|
|
|
2514
2549
|
message: "Stale Telegram bus follower registration generation.",
|
|
2515
2550
|
};
|
|
2516
2551
|
}
|
|
2552
|
+
if (envelope.kind === "leader.workspaceRestore") {
|
|
2553
|
+
const ctx = deps.getContext();
|
|
2554
|
+
if (!authSecret || !ctx || !deps.isWorkspaceRestoreEnabled?.() || !deps.handleWorkspaceRestore) return {
|
|
2555
|
+
kind: "bus.ack", requestId: envelope.requestId, ok: false, message: "Workspace Restore capability is unavailable.",
|
|
2556
|
+
};
|
|
2557
|
+
try {
|
|
2558
|
+
const result = await deps.handleWorkspaceRestore({ operationId: envelope.operationId,
|
|
2559
|
+
registrationGeneration, mode: envelope.mode }, ctx);
|
|
2560
|
+
if (deps.getAuthSecret?.() !== authSecret || deps.getRegistrationGeneration() !== registrationGeneration ||
|
|
2561
|
+
deps.getContext() !== ctx || !deps.isWorkspaceRestoreEnabled()) throw new Error("Workspace Restore receiver authority changed.");
|
|
2562
|
+
return { kind: "bus.ack", requestId: envelope.requestId, ok: true, result };
|
|
2563
|
+
} catch (error) {
|
|
2564
|
+
deps.recordRuntimeEvent?.("bus", error, { phase: "follower-workspace-restore" });
|
|
2565
|
+
return { kind: "bus.ack", requestId: envelope.requestId, ok: false,
|
|
2566
|
+
message: error instanceof Error ? error.message : "Workspace Restore failed." };
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2517
2569
|
if (envelope.kind === "leader.offerInputCustodyHandoff" &&
|
|
2518
2570
|
envelope.recipientBindingKey !== deps.getRecipientBindingKey()) return {
|
|
2519
2571
|
kind: "bus.ack", requestId: envelope.requestId, ok: false,
|
|
2520
2572
|
message: "Mismatched Telegram follower handoff recipient identity.",
|
|
2521
2573
|
};
|
|
2522
2574
|
if (
|
|
2523
|
-
envelope.kind !== "leader.replaceFollowerTarget" &&
|
|
2524
2575
|
envelope.kind !== "leader.offerQueueHandoff" &&
|
|
2525
2576
|
envelope.kind !== "leader.offerInputCustodyHandoff" &&
|
|
2526
2577
|
(!envelope.delivery ||
|
|
@@ -2557,10 +2608,7 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
|
|
|
2557
2608
|
const result = await deps.handleInputCustodyHandoff(envelope, ctx);
|
|
2558
2609
|
return { kind: "bus.ack", requestId: envelope.requestId, ok: true, result };
|
|
2559
2610
|
}
|
|
2560
|
-
if (
|
|
2561
|
-
envelope.kind !== "leader.replaceFollowerTarget" &&
|
|
2562
|
-
envelope.kind !== "leader.offerQueueHandoff"
|
|
2563
|
-
) {
|
|
2611
|
+
if (envelope.kind !== "leader.offerQueueHandoff") {
|
|
2564
2612
|
const sourceReferenceEnabled = envelope.kind === "leader.wakeInputCustody" ||
|
|
2565
2613
|
(deps.isSourceReferenceAdmissionEnabled?.() ?? false);
|
|
2566
2614
|
if (sourceReferenceEnabled &&
|
|
@@ -2580,52 +2628,33 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
|
|
|
2580
2628
|
result: receipt,
|
|
2581
2629
|
};
|
|
2582
2630
|
}
|
|
2583
|
-
if (
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
);
|
|
2588
|
-
}
|
|
2589
|
-
const result = await deps.handleQueueHandoff(envelope, ctx);
|
|
2590
|
-
const receipt = envelope.payload.admissionReceipts[0];
|
|
2591
|
-
if (
|
|
2592
|
-
envelope.payload.admissionReceipts.length !== 1 ||
|
|
2593
|
-
!receipt ||
|
|
2594
|
-
result.status !== "staged" ||
|
|
2595
|
-
result.receiptId !== receipt.receiptId ||
|
|
2596
|
-
result.sourceUpdateIds.length !== receipt.sourceUpdateIds.length ||
|
|
2597
|
-
result.sourceUpdateIds.some(
|
|
2598
|
-
(updateId, index) => updateId !== receipt.sourceUpdateIds[index],
|
|
2599
|
-
)
|
|
2600
|
-
) {
|
|
2601
|
-
throw new Error(
|
|
2602
|
-
"Telegram queue handoff staging returned a mismatched receipt.",
|
|
2603
|
-
);
|
|
2604
|
-
}
|
|
2605
|
-
return {
|
|
2606
|
-
kind: "bus.ack",
|
|
2607
|
-
requestId: envelope.requestId,
|
|
2608
|
-
ok: true,
|
|
2609
|
-
result,
|
|
2610
|
-
};
|
|
2631
|
+
if (!deps.handleQueueHandoff) {
|
|
2632
|
+
throw new Error(
|
|
2633
|
+
"Telegram bus receiver cannot accept queue handoff payloads.",
|
|
2634
|
+
);
|
|
2611
2635
|
}
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2636
|
+
const result = await deps.handleQueueHandoff(envelope, ctx);
|
|
2637
|
+
const receipt = envelope.payload.admissionReceipts[0];
|
|
2638
|
+
if (
|
|
2639
|
+
envelope.payload.admissionReceipts.length !== 1 ||
|
|
2640
|
+
!receipt ||
|
|
2641
|
+
result.status !== "staged" ||
|
|
2642
|
+
result.receiptId !== receipt.receiptId ||
|
|
2643
|
+
result.sourceUpdateIds.length !== receipt.sourceUpdateIds.length ||
|
|
2644
|
+
result.sourceUpdateIds.some(
|
|
2645
|
+
(updateId, index) => updateId !== receipt.sourceUpdateIds[index],
|
|
2646
|
+
)
|
|
2647
|
+
) {
|
|
2648
|
+
throw new Error(
|
|
2649
|
+
"Telegram queue handoff staging returned a mismatched receipt.",
|
|
2626
2650
|
);
|
|
2627
2651
|
}
|
|
2628
|
-
return {
|
|
2652
|
+
return {
|
|
2653
|
+
kind: "bus.ack",
|
|
2654
|
+
requestId: envelope.requestId,
|
|
2655
|
+
ok: true,
|
|
2656
|
+
result,
|
|
2657
|
+
};
|
|
2629
2658
|
} catch (error) {
|
|
2630
2659
|
deps.recordRuntimeEvent?.("bus", error, { phase: "follower-forward" });
|
|
2631
2660
|
return {
|
|
@@ -2670,7 +2699,3 @@ function parseTarget(value: unknown): TelegramTarget | undefined {
|
|
|
2670
2699
|
if (typeof value.threadId === "number") target.threadId = value.threadId;
|
|
2671
2700
|
return target;
|
|
2672
2701
|
}
|
|
2673
|
-
|
|
2674
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
2675
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2676
|
-
}
|