@llblab/pi-kit 0.27.6 → 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/BACKLOG.md +2 -5
- package/CHANGELOG.md +5 -0
- package/README.md +2 -2
- package/node_modules/@llblab/pi-telegram/AGENTS.md +10 -8
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -9
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
- package/node_modules/@llblab/pi-telegram/LICENSE +21 -0
- package/node_modules/@llblab/pi-telegram/README.md +15 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.d.ts +1 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.js +5 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +5 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +6 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +4 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +54 -56
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +206 -139
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +21 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +181 -25
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.d.ts +4 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.js +28 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +52 -56
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +78 -246
- package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.d.ts +1 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.js +23 -26
- package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.js +5 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +0 -23
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +15 -15
- package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +0 -17
- package/node_modules/@llblab/pi-telegram/dist/lib/config.js +12 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +0 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +2 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +255 -61
- package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +0 -19
- package/node_modules/@llblab/pi-telegram/dist/lib/inbound.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/inbound.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +177 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +1115 -249
- package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +100 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +380 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/logging.d.ts +30 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +129 -72
- package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +28 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/media.js +26 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +0 -11
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +8 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +0 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +18 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.d.ts +4 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.js +12 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +4 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +0 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +1 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +1 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +4 -21
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.d.ts +0 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +5 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +50 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +155 -17
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/preview.d.ts +0 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +3 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.d.ts +0 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +3 -15
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +69 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/recovery.d.ts +29 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/recovery.js +104 -33
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +0 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +73 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +2206 -307
- package/node_modules/@llblab/pi-telegram/dist/lib/sections.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +44 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/status.js +149 -27
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +5 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +11 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +34 -23
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +18 -21
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.d.ts +32 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.js +191 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +291 -59
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +1647 -369
- package/node_modules/@llblab/pi-telegram/dist/lib/turns.d.ts +0 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +184 -28
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +1032 -114
- package/node_modules/@llblab/pi-telegram/dist/lib/wire.d.ts +12 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/wire.js +21 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +25 -17
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +95 -20
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.d.ts +20 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.js +103 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +43 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +148 -52
- package/node_modules/@llblab/pi-telegram/dist/package.json +6 -6
- package/node_modules/@llblab/pi-telegram/dist/skills/generated-control-surface/SKILL.md +3 -3
- package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
- package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/activity.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +152 -34
- package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/delivery.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +125 -19
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -1
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +22 -8
- package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +5 -9
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +6 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +243 -224
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +178 -31
- package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +30 -8
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +110 -334
- package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +29 -29
- package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +5 -5
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +15 -15
- package/node_modules/@llblab/pi-telegram/lib/config.ts +12 -17
- package/node_modules/@llblab/pi-telegram/lib/delivery.ts +2 -8
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +257 -72
- package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +0 -22
- package/node_modules/@llblab/pi-telegram/lib/inbound.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +1182 -326
- package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +402 -23
- package/node_modules/@llblab/pi-telegram/lib/logging.ts +151 -101
- package/node_modules/@llblab/pi-telegram/lib/media.ts +51 -9
- package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +8 -8
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +18 -18
- package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +11 -0
- package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/menu.ts +5 -1
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +1 -10
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +5 -24
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +5 -5
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +177 -17
- package/node_modules/@llblab/pi-telegram/lib/pi.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +5 -5
- package/node_modules/@llblab/pi-telegram/lib/preview.ts +3 -3
- package/node_modules/@llblab/pi-telegram/lib/prompt-templates.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +67 -9
- package/node_modules/@llblab/pi-telegram/lib/recovery.ts +104 -44
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +1955 -375
- package/node_modules/@llblab/pi-telegram/lib/sections.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/status.ts +156 -36
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +48 -33
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +24 -21
- package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +267 -9
- package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +3 -1
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +1697 -488
- package/node_modules/@llblab/pi-telegram/lib/turns.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +1072 -151
- package/node_modules/@llblab/pi-telegram/lib/wire.ts +28 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +97 -47
- package/node_modules/@llblab/pi-telegram/lib/workspace-identity.ts +147 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +188 -89
- package/node_modules/@llblab/pi-telegram/package.json +6 -6
- package/node_modules/@llblab/pi-telegram/scripts/audit-exports.mjs +100 -0
- package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +80 -43
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +3 -3
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +3 -3
- package/package.json +2 -2
|
@@ -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 () => {
|
|
750
|
+
assertCurrent();
|
|
751
|
+
const intent = deps.readRestoreIntent(request.operationId, authority.profileBindingKey);
|
|
730
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: (
|
|
@@ -1284,6 +1285,7 @@ export function createTelegramBusFollowerRegistrationState(
|
|
|
1284
1285
|
let threadName: string | undefined;
|
|
1285
1286
|
let displayTitle: string | undefined;
|
|
1286
1287
|
let generation: string | undefined;
|
|
1288
|
+
let sessionId: string | undefined;
|
|
1287
1289
|
let leaderProtocol: TelegramBusProtocolIdentity | undefined;
|
|
1288
1290
|
let eligibleElectionSlots: string[] = [];
|
|
1289
1291
|
let recoveryEpoch = 0;
|
|
@@ -1313,6 +1315,7 @@ export function createTelegramBusFollowerRegistrationState(
|
|
|
1313
1315
|
return true;
|
|
1314
1316
|
},
|
|
1315
1317
|
getGeneration: () => generation,
|
|
1318
|
+
getSessionId: currentSessionId => registered && generation && currentSessionId === sessionId ? sessionId : undefined,
|
|
1316
1319
|
beginRecovery: () => {
|
|
1317
1320
|
if (activeRecoveryEpoch !== undefined) return activeRecoveryEpoch;
|
|
1318
1321
|
activeRecoveryEpoch = ++recoveryEpoch;
|
|
@@ -1354,6 +1357,7 @@ export function createTelegramBusFollowerRegistrationState(
|
|
|
1354
1357
|
generation === metadata?.generation &&
|
|
1355
1358
|
target?.chatId === nextTarget?.chatId && target?.threadId === nextTarget?.threadId
|
|
1356
1359
|
? displayTitle : undefined;
|
|
1360
|
+
const retainedSessionId = next && registered && generation === metadata?.generation ? sessionId : undefined;
|
|
1357
1361
|
const availabilityChanged = registered !== next;
|
|
1358
1362
|
registered = next;
|
|
1359
1363
|
target = next ? (nextTarget ? { ...nextTarget } : undefined) : undefined;
|
|
@@ -1363,6 +1367,7 @@ export function createTelegramBusFollowerRegistrationState(
|
|
|
1363
1367
|
metadata.displayTitle?.trim() && metadata.displayTitle.length <= 128
|
|
1364
1368
|
? metadata.displayTitle : retainedDisplayTitle;
|
|
1365
1369
|
generation = next ? metadata?.generation : undefined;
|
|
1370
|
+
sessionId = next ? metadata?.sessionId ?? retainedSessionId : undefined;
|
|
1366
1371
|
leaderProtocol =
|
|
1367
1372
|
next && metadata?.leaderProtocol
|
|
1368
1373
|
? {
|
|
@@ -1656,7 +1661,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1656
1661
|
let activeLeaderSocketPath: string | undefined;
|
|
1657
1662
|
let activeAuthSecret: string | undefined;
|
|
1658
1663
|
let activeRegistrationGeneration: string | undefined;
|
|
1659
|
-
let registrationAttempt: { ctx: TContext; sessionGeneration: number | undefined } | undefined;
|
|
1664
|
+
let registrationAttempt: { ctx: TContext; sessionGeneration: number | undefined; sessionId: string | undefined } | undefined;
|
|
1660
1665
|
let activeContext: TContext | undefined;
|
|
1661
1666
|
let lastKnownTarget: TelegramTarget | undefined;
|
|
1662
1667
|
let lastKnownSlot: string | undefined;
|
|
@@ -1793,10 +1798,12 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1793
1798
|
registerWithLeader: async (ctx, leader, options) => {
|
|
1794
1799
|
if (deps.isContextActive?.(ctx) === false) return false;
|
|
1795
1800
|
const sessionGeneration = deps.getSessionGeneration?.();
|
|
1796
|
-
const
|
|
1801
|
+
const sessionId = deps.getSessionId?.(ctx);
|
|
1802
|
+
const attempt = { ctx, sessionGeneration, sessionId };
|
|
1797
1803
|
registrationAttempt = attempt;
|
|
1798
1804
|
const isCurrentRequest = () => registrationAttempt === attempt &&
|
|
1799
|
-
deps.getSessionGeneration?.() === sessionGeneration && deps.
|
|
1805
|
+
deps.getSessionGeneration?.() === sessionGeneration && deps.getSessionId?.(ctx) === sessionId &&
|
|
1806
|
+
deps.isContextActive?.(ctx) !== false;
|
|
1800
1807
|
const abandonOwnedRequest = async (): Promise<void> => {
|
|
1801
1808
|
if (registrationAttempt !== attempt) return;
|
|
1802
1809
|
registrationAttempt = undefined;
|
|
@@ -1859,7 +1866,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1859
1866
|
? { slot: deps.registrationState?.getSlot() ?? lastKnownSlot }
|
|
1860
1867
|
: {}),
|
|
1861
1868
|
cwd: ctx.cwd,
|
|
1862
|
-
sessionId
|
|
1869
|
+
sessionId,
|
|
1863
1870
|
pid: getPid(),
|
|
1864
1871
|
processBirthId: deps.getProcessBirthId?.(),
|
|
1865
1872
|
sessionGeneration,
|
|
@@ -1910,7 +1917,9 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1910
1917
|
local: deps.protocolIdentity,
|
|
1911
1918
|
remote: response?.kind === "bus.ack" ? response.protocol : undefined,
|
|
1912
1919
|
});
|
|
1913
|
-
|
|
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) {
|
|
1914
1923
|
stopHeartbeat();
|
|
1915
1924
|
activeLeaderSocketPath = undefined;
|
|
1916
1925
|
activeAuthSecret = undefined;
|
|
@@ -1948,6 +1957,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1948
1957
|
deps.registrationState?.setRegistered(true, registrationResult.target, {
|
|
1949
1958
|
...registrationResult,
|
|
1950
1959
|
generation: registrationGeneration,
|
|
1960
|
+
sessionId,
|
|
1951
1961
|
...(response.protocol
|
|
1952
1962
|
? { leaderProtocol: response.protocol }
|
|
1953
1963
|
: {}),
|
|
@@ -1994,7 +2004,8 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1994
2004
|
},
|
|
1995
2005
|
setContext(ctx) {
|
|
1996
2006
|
if (registrationAttempt && (registrationAttempt.ctx !== ctx ||
|
|
1997
|
-
registrationAttempt.sessionGeneration !== deps.getSessionGeneration?.()
|
|
2007
|
+
registrationAttempt.sessionGeneration !== deps.getSessionGeneration?.() ||
|
|
2008
|
+
registrationAttempt.sessionId !== deps.getSessionId?.(ctx))) registrationAttempt = undefined;
|
|
1998
2009
|
activeContext = ctx;
|
|
1999
2010
|
},
|
|
2000
2011
|
async setThreadDisplayMode(mode) {
|
|
@@ -2326,7 +2337,20 @@ export function createTelegramBusFollowerDurableAdmissionRuntime<TContext>(deps:
|
|
|
2326
2337
|
): unknown;
|
|
2327
2338
|
};
|
|
2328
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 };
|
|
2329
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
|
+
};
|
|
2330
2354
|
return {
|
|
2331
2355
|
async admit(envelope, ctx) {
|
|
2332
2356
|
const delivery = envelope.delivery;
|
|
@@ -2373,12 +2397,17 @@ export function createTelegramBusFollowerDurableAdmissionRuntime<TContext>(deps:
|
|
|
2373
2397
|
? { message: carrier }
|
|
2374
2398
|
: { edited_message: carrier }),
|
|
2375
2399
|
};
|
|
2376
|
-
|
|
2377
|
-
deps.signalWorker(ctx);
|
|
2378
|
-
return {
|
|
2400
|
+
const result = {
|
|
2379
2401
|
deliveryId: delivery.deliveryId,
|
|
2380
2402
|
sourceUpdateId: delivery.sourceUpdateId,
|
|
2381
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;
|
|
2382
2411
|
},
|
|
2383
2412
|
};
|
|
2384
2413
|
}
|
|
@@ -2497,7 +2526,7 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
|
|
|
2497
2526
|
envelope.kind !== "leader.forwardEditedMessage" &&
|
|
2498
2527
|
envelope.kind !== "leader.wakeInputCustody" &&
|
|
2499
2528
|
envelope.kind !== "leader.offerInputCustodyHandoff" &&
|
|
2500
|
-
envelope.kind !== "leader.
|
|
2529
|
+
envelope.kind !== "leader.workspaceRestore" &&
|
|
2501
2530
|
envelope.kind !== "leader.offerQueueHandoff") ||
|
|
2502
2531
|
envelope.recipientInstanceId !== deps.instanceId
|
|
2503
2532
|
) {
|
|
@@ -2520,13 +2549,29 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
|
|
|
2520
2549
|
message: "Stale Telegram bus follower registration generation.",
|
|
2521
2550
|
};
|
|
2522
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
|
+
}
|
|
2523
2569
|
if (envelope.kind === "leader.offerInputCustodyHandoff" &&
|
|
2524
2570
|
envelope.recipientBindingKey !== deps.getRecipientBindingKey()) return {
|
|
2525
2571
|
kind: "bus.ack", requestId: envelope.requestId, ok: false,
|
|
2526
2572
|
message: "Mismatched Telegram follower handoff recipient identity.",
|
|
2527
2573
|
};
|
|
2528
2574
|
if (
|
|
2529
|
-
envelope.kind !== "leader.replaceFollowerTarget" &&
|
|
2530
2575
|
envelope.kind !== "leader.offerQueueHandoff" &&
|
|
2531
2576
|
envelope.kind !== "leader.offerInputCustodyHandoff" &&
|
|
2532
2577
|
(!envelope.delivery ||
|
|
@@ -2563,10 +2608,7 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
|
|
|
2563
2608
|
const result = await deps.handleInputCustodyHandoff(envelope, ctx);
|
|
2564
2609
|
return { kind: "bus.ack", requestId: envelope.requestId, ok: true, result };
|
|
2565
2610
|
}
|
|
2566
|
-
if (
|
|
2567
|
-
envelope.kind !== "leader.replaceFollowerTarget" &&
|
|
2568
|
-
envelope.kind !== "leader.offerQueueHandoff"
|
|
2569
|
-
) {
|
|
2611
|
+
if (envelope.kind !== "leader.offerQueueHandoff") {
|
|
2570
2612
|
const sourceReferenceEnabled = envelope.kind === "leader.wakeInputCustody" ||
|
|
2571
2613
|
(deps.isSourceReferenceAdmissionEnabled?.() ?? false);
|
|
2572
2614
|
if (sourceReferenceEnabled &&
|
|
@@ -2586,52 +2628,33 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
|
|
|
2586
2628
|
result: receipt,
|
|
2587
2629
|
};
|
|
2588
2630
|
}
|
|
2589
|
-
if (
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
);
|
|
2594
|
-
}
|
|
2595
|
-
const result = await deps.handleQueueHandoff(envelope, ctx);
|
|
2596
|
-
const receipt = envelope.payload.admissionReceipts[0];
|
|
2597
|
-
if (
|
|
2598
|
-
envelope.payload.admissionReceipts.length !== 1 ||
|
|
2599
|
-
!receipt ||
|
|
2600
|
-
result.status !== "staged" ||
|
|
2601
|
-
result.receiptId !== receipt.receiptId ||
|
|
2602
|
-
result.sourceUpdateIds.length !== receipt.sourceUpdateIds.length ||
|
|
2603
|
-
result.sourceUpdateIds.some(
|
|
2604
|
-
(updateId, index) => updateId !== receipt.sourceUpdateIds[index],
|
|
2605
|
-
)
|
|
2606
|
-
) {
|
|
2607
|
-
throw new Error(
|
|
2608
|
-
"Telegram queue handoff staging returned a mismatched receipt.",
|
|
2609
|
-
);
|
|
2610
|
-
}
|
|
2611
|
-
return {
|
|
2612
|
-
kind: "bus.ack",
|
|
2613
|
-
requestId: envelope.requestId,
|
|
2614
|
-
ok: true,
|
|
2615
|
-
result,
|
|
2616
|
-
};
|
|
2631
|
+
if (!deps.handleQueueHandoff) {
|
|
2632
|
+
throw new Error(
|
|
2633
|
+
"Telegram bus receiver cannot accept queue handoff payloads.",
|
|
2634
|
+
);
|
|
2617
2635
|
}
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
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.",
|
|
2632
2650
|
);
|
|
2633
2651
|
}
|
|
2634
|
-
return {
|
|
2652
|
+
return {
|
|
2653
|
+
kind: "bus.ack",
|
|
2654
|
+
requestId: envelope.requestId,
|
|
2655
|
+
ok: true,
|
|
2656
|
+
result,
|
|
2657
|
+
};
|
|
2635
2658
|
} catch (error) {
|
|
2636
2659
|
deps.recordRuntimeEvent?.("bus", error, { phase: "follower-forward" });
|
|
2637
2660
|
return {
|
|
@@ -2676,7 +2699,3 @@ function parseTarget(value: unknown): TelegramTarget | undefined {
|
|
|
2676
2699
|
if (typeof value.threadId === "number") target.threadId = value.threadId;
|
|
2677
2700
|
return target;
|
|
2678
2701
|
}
|
|
2679
|
-
|
|
2680
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
2681
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2682
|
-
}
|