@llblab/pi-telegram 0.52.2 → 0.53.1
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 +9 -7
- package/BACKLOG.md +1 -18
- package/CHANGELOG.md +15 -0
- package/README.md +7 -11
- package/api/commands.ts +3 -0
- package/dist/api/commands.d.ts +1 -1
- package/dist/lib/activity-verbosity.d.ts +1 -5
- package/dist/lib/activity-verbosity.js +15 -42
- package/dist/lib/activity.d.ts +9 -0
- package/dist/lib/activity.js +39 -10
- package/dist/lib/agent-messages.js +7 -3
- package/dist/lib/bindings.d.ts +43 -5
- package/dist/lib/bindings.js +163 -20
- package/dist/lib/bus-api.d.ts +12 -0
- package/dist/lib/bus-api.js +182 -79
- package/dist/lib/bus-follower.d.ts +111 -31
- package/dist/lib/bus-follower.js +1324 -359
- package/dist/lib/bus-leader.d.ts +41 -91
- package/dist/lib/bus-leader.js +969 -637
- package/dist/lib/bus-transport.js +16 -9
- package/dist/lib/bus.d.ts +215 -18
- package/dist/lib/bus.js +1048 -327
- package/dist/lib/channel-posts.d.ts +5 -0
- package/dist/lib/channel-posts.js +434 -261
- package/dist/lib/command-templates.d.ts +26 -5
- package/dist/lib/command-templates.js +92 -176
- package/dist/lib/commands.d.ts +168 -82
- package/dist/lib/commands.js +1409 -291
- package/dist/lib/config.d.ts +1 -8
- package/dist/lib/config.js +34 -76
- package/dist/lib/delivery.d.ts +2 -2
- package/dist/lib/delivery.js +16 -16
- package/dist/lib/extension.js +639 -171
- package/dist/lib/generative-app-worker.mjs +19 -7
- package/dist/lib/generative-apps.d.ts +0 -5
- package/dist/lib/generative-apps.js +61 -49
- package/dist/lib/inbound.d.ts +5 -0
- package/dist/lib/inbound.js +35 -59
- package/dist/lib/journal.d.ts +33 -3
- package/dist/lib/journal.js +2063 -782
- package/dist/lib/lifecycle.d.ts +2 -6
- package/dist/lib/lifecycle.js +75 -58
- package/dist/lib/locks.d.ts +29 -3
- package/dist/lib/locks.js +289 -108
- package/dist/lib/logging.js +50 -14
- package/dist/lib/media.d.ts +22 -1
- package/dist/lib/media.js +46 -28
- package/dist/lib/menu-model.d.ts +19 -5
- package/dist/lib/menu-model.js +53 -11
- package/dist/lib/menu-queue.d.ts +5 -4
- package/dist/lib/menu-queue.js +45 -63
- package/dist/lib/menu-settings.d.ts +5 -4
- package/dist/lib/menu-settings.js +114 -78
- package/dist/lib/menu-status.d.ts +1 -1
- package/dist/lib/menu-status.js +8 -25
- package/dist/lib/menu-thinking.d.ts +6 -1
- package/dist/lib/menu-thinking.js +14 -21
- package/dist/lib/menu.d.ts +8 -61
- package/dist/lib/menu.js +92 -164
- package/dist/lib/model.d.ts +1 -1
- package/dist/lib/model.js +2 -16
- package/dist/lib/outbound-attachments.d.ts +6 -27
- package/dist/lib/outbound-attachments.js +45 -38
- package/dist/lib/outbound-buttons.js +22 -16
- package/dist/lib/outbound-markup.d.ts +0 -2
- package/dist/lib/outbound-markup.js +51 -64
- package/dist/lib/outbound-voice.js +6 -3
- package/dist/lib/outbound.d.ts +6 -6
- package/dist/lib/outbound.js +65 -51
- package/dist/lib/ownership.js +6 -2
- package/dist/lib/paths.d.ts +5 -0
- package/dist/lib/paths.js +37 -21
- package/dist/lib/pi.d.ts +2 -0
- package/dist/lib/pi.js +8 -1
- package/dist/lib/polling.d.ts +21 -37
- package/dist/lib/polling.js +59 -50
- package/dist/lib/preview.js +39 -19
- package/dist/lib/process-identity.d.ts +24 -0
- package/dist/lib/process-identity.js +133 -0
- package/dist/lib/prompts.js +2 -7
- package/dist/lib/queue.d.ts +14 -16
- package/dist/lib/queue.js +252 -127
- package/dist/lib/recovery.d.ts +2 -83
- package/dist/lib/recovery.js +20 -274
- package/dist/lib/replies.d.ts +16 -24
- package/dist/lib/replies.js +99 -116
- package/dist/lib/routing.d.ts +120 -7
- package/dist/lib/routing.js +4667 -1152
- package/dist/lib/runtime.d.ts +1 -11
- package/dist/lib/sections.d.ts +4 -29
- package/dist/lib/sections.js +7 -20
- package/dist/lib/skills.js +4 -3
- package/dist/lib/status.js +86 -57
- package/dist/lib/sync.d.ts +1 -3
- package/dist/lib/sync.js +67 -43
- package/dist/lib/target.d.ts +4 -0
- package/dist/lib/target.js +18 -0
- package/dist/lib/telegram-api.d.ts +18 -13
- package/dist/lib/telegram-api.js +148 -53
- package/dist/lib/text-groups.js +4 -39
- package/dist/lib/thread-cleanup-manager.d.ts +6 -27
- package/dist/lib/thread-cleanup-manager.js +369 -186
- package/dist/lib/thread-display.d.ts +1 -2
- package/dist/lib/thread-display.js +58 -38
- package/dist/lib/thread-naming.d.ts +28 -8
- package/dist/lib/thread-naming.js +175 -66
- package/dist/lib/thread-reconciler.d.ts +42 -16
- package/dist/lib/thread-reconciler.js +90 -50
- package/dist/lib/threads.d.ts +82 -20
- package/dist/lib/threads.js +2247 -855
- package/dist/lib/time-injection.js +1 -2
- package/dist/lib/turns.js +1 -19
- package/dist/lib/updates.d.ts +169 -80
- package/dist/lib/updates.js +2418 -641
- package/dist/lib/voice.d.ts +7 -22
- package/dist/lib/voice.js +7 -22
- package/dist/lib/wire.js +1 -1
- package/dist/lib/workspace-admission.d.ts +3 -1
- package/dist/lib/workspace-admission.js +425 -211
- package/dist/lib/workspace-identity.js +8 -13
- package/dist/lib/workspace-retirement.d.ts +1 -1
- package/dist/lib/workspace-retirement.js +328 -140
- package/dist/lib/workspace-slots.js +8 -4
- package/dist/package.json +1 -1
- package/docs/architecture.md +31 -29
- package/docs/inbound.md +1 -1
- package/docs/multi-instance-bus.md +15 -3
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +29 -1
- package/docs/ui-style.md +25 -8
- package/lib/activity-verbosity.ts +59 -82
- package/lib/activity.ts +146 -102
- package/lib/agent-messages.ts +12 -5
- package/lib/bindings.ts +389 -82
- package/lib/bus-api.ts +263 -102
- package/lib/bus-follower.ts +2440 -601
- package/lib/bus-leader.ts +2209 -1290
- package/lib/bus-transport.ts +46 -12
- package/lib/bus.ts +1673 -444
- package/lib/channel-posts.ts +848 -327
- package/lib/command-templates.ts +152 -265
- package/lib/commands.ts +2398 -555
- package/lib/config.ts +177 -170
- package/lib/delivery.ts +41 -29
- package/lib/extension.ts +1206 -433
- package/lib/generative-app-worker.mjs +19 -7
- package/lib/generative-apps.ts +324 -211
- package/lib/inbound.ts +95 -97
- package/lib/journal.ts +5311 -1763
- package/lib/keyboard.ts +1 -3
- package/lib/lifecycle.ts +128 -78
- package/lib/locks.ts +677 -203
- package/lib/logging.ts +147 -58
- package/lib/media.ts +82 -43
- package/lib/menu-model.ts +85 -18
- package/lib/menu-queue.ts +88 -129
- package/lib/menu-settings.ts +260 -155
- package/lib/menu-status.ts +20 -60
- package/lib/menu-thinking.ts +41 -56
- package/lib/menu.ts +112 -285
- package/lib/model.ts +8 -22
- package/lib/outbound-attachments.ts +156 -125
- package/lib/outbound-buttons.ts +76 -49
- package/lib/outbound-markup.ts +96 -94
- package/lib/outbound-voice.ts +33 -27
- package/lib/outbound.ts +128 -125
- package/lib/ownership.ts +19 -10
- package/lib/paths.ts +148 -49
- package/lib/pi.ts +35 -15
- package/lib/polling.ts +144 -147
- package/lib/preview.ts +93 -40
- package/lib/process-identity.ts +164 -0
- package/lib/prompts.ts +4 -11
- package/lib/queue.ts +516 -301
- package/lib/recovery.ts +50 -454
- package/lib/rendering.ts +1 -3
- package/lib/replies.ts +248 -209
- package/lib/routing.ts +8509 -2186
- package/lib/runtime.ts +1 -12
- package/lib/sections.ts +24 -89
- package/lib/setup.ts +1 -3
- package/lib/skills.ts +9 -4
- package/lib/status.ts +215 -110
- package/lib/sync.ts +448 -361
- package/lib/target.ts +22 -3
- package/lib/telegram-api.ts +302 -127
- package/lib/text-groups.ts +5 -39
- package/lib/thread-cleanup-manager.ts +833 -331
- package/lib/thread-display.ts +213 -97
- package/lib/thread-naming.ts +232 -87
- package/lib/thread-reconciler.ts +155 -72
- package/lib/threads.ts +5318 -1845
- package/lib/time-injection.ts +1 -4
- package/lib/turns.ts +15 -28
- package/lib/updates.ts +5184 -1612
- package/lib/voice.ts +7 -35
- package/lib/wire.ts +1 -1
- package/lib/workspace-admission.ts +996 -416
- package/lib/workspace-identity.ts +14 -17
- package/lib/workspace-retirement.ts +878 -376
- package/lib/workspace-slots.ts +22 -13
- package/package.json +1 -1
- package/scripts/check-downgrade.mjs +0 -472
- package/scripts/measure-workspace.mjs +0 -108
package/lib/bus-leader.ts
CHANGED
|
@@ -6,64 +6,78 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { isDeepStrictEqual } from "node:util";
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
createTelegramThreadDisplayReconciler,
|
|
12
|
-
resolveTelegramInitialWorkspaceDisplayName,
|
|
13
|
-
resolveTelegramLiveWorkspaceBindingKeys,
|
|
14
|
-
} from "./thread-display.ts";
|
|
15
|
-
import type { TelegramThreadDisplayMode } from "./config.ts";
|
|
16
|
-
import * as ThreadNaming from "./thread-naming.ts";
|
|
17
|
-
import * as ThreadReconciler from "./thread-reconciler.ts";
|
|
18
|
-
import {
|
|
19
|
-
getTelegramApiErrorRequestTarget,
|
|
20
|
-
isTelegramApiCommitUnknownError,
|
|
21
|
-
type TelegramApiCallOptions,
|
|
22
|
-
} from "./telegram-api.ts";
|
|
23
|
-
import type { TelegramTarget } from "./target.ts";
|
|
24
|
-
import type { TelegramAttachmentSource } from "./media.ts";
|
|
25
|
-
import * as Threads from "./threads.ts";
|
|
26
|
-
import * as WorkspaceIdentity from "./workspace-identity.ts";
|
|
9
|
+
|
|
10
|
+
import { getTelegramBusTransportRetryPolicy } from "./bus-transport.ts";
|
|
27
11
|
import {
|
|
28
12
|
createTelegramBusLocalServer,
|
|
29
13
|
createUnauthorizedBusAck,
|
|
14
|
+
rejectTelegramBusRequest,
|
|
30
15
|
getTelegramBusEnvelopeTrafficClass,
|
|
16
|
+
getTelegramBusFollowerSocketPath,
|
|
31
17
|
getTelegramBusProtocolCompatibility,
|
|
32
18
|
hasTelegramBusCapability,
|
|
33
19
|
isTelegramBusEnvelopeAuthorized,
|
|
34
|
-
|
|
20
|
+
parseTelegramBusEnvelope,
|
|
35
21
|
sendTelegramBusLocalEnvelope,
|
|
36
22
|
stripTelegramBusApiMetadata,
|
|
23
|
+
TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
|
|
24
|
+
TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION,
|
|
25
|
+
TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF,
|
|
26
|
+
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
27
|
+
TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
28
|
+
TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
|
|
29
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT,
|
|
30
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
|
|
31
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
37
32
|
type TelegramBusEnvelope,
|
|
33
|
+
type TelegramBusLeaderQueueHandoffOffer,
|
|
38
34
|
type TelegramBusFollowerRegistry,
|
|
39
35
|
type TelegramBusFollowerView,
|
|
40
36
|
type TelegramBusInstanceRegistration,
|
|
41
37
|
type TelegramBusProtocolIdentity,
|
|
38
|
+
type TelegramBusSelectedMenuDelivery,
|
|
42
39
|
type TelegramBusSocketPathSource,
|
|
43
|
-
TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION,
|
|
44
|
-
TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF,
|
|
45
|
-
TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT,
|
|
46
|
-
TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
|
|
47
|
-
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
48
|
-
TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
|
|
49
|
-
TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
|
|
50
|
-
TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
51
40
|
} from "./bus.ts";
|
|
52
|
-
import {
|
|
53
|
-
import type {
|
|
41
|
+
import type { TelegramThreadDisplayMode } from "./config.ts";
|
|
42
|
+
import type { TelegramAttachmentSource } from "./media.ts";
|
|
43
|
+
import { escapeHtml } from "./rendering.ts";
|
|
44
|
+
import * as Sync from "./sync.ts";
|
|
45
|
+
import {
|
|
46
|
+
parseTelegramIntegerId as asInteger,
|
|
47
|
+
type TelegramTarget,
|
|
48
|
+
} from "./target.ts";
|
|
49
|
+
import {
|
|
50
|
+
getTelegramApiErrorRequestTarget,
|
|
51
|
+
isTelegramApiCommitUnknownError,
|
|
52
|
+
TelegramApiAuthorityError,
|
|
53
|
+
type TelegramApiCallOptions,
|
|
54
|
+
type TelegramBridgeApiRuntime,
|
|
55
|
+
} from "./telegram-api.ts";
|
|
56
|
+
import {
|
|
57
|
+
createTelegramThreadDisplayReconciler,
|
|
58
|
+
resolveTelegramInitialWorkspaceDisplayName,
|
|
59
|
+
resolveTelegramLiveWorkspaceBindingKeys,
|
|
60
|
+
} from "./thread-display.ts";
|
|
61
|
+
import * as ThreadNaming from "./thread-naming.ts";
|
|
62
|
+
import * as ThreadReconciler from "./thread-reconciler.ts";
|
|
63
|
+
import * as Threads from "./threads.ts";
|
|
64
|
+
import {
|
|
65
|
+
createTelegramWorkspaceAdmissionOperationId,
|
|
66
|
+
runWithTelegramWorkspaceAdmissionsAsync,
|
|
67
|
+
type TelegramWorkspaceAdmissionLedger,
|
|
68
|
+
} from "./workspace-admission.ts";
|
|
69
|
+
import * as WorkspaceIdentity from "./workspace-identity.ts";
|
|
54
70
|
import {
|
|
55
71
|
createTelegramWorkspaceOperationRuntime,
|
|
56
72
|
createTelegramWorkspaceSlotRotation,
|
|
57
73
|
type TelegramWorkspaceCapacityRunner,
|
|
58
|
-
type TelegramWorkspaceSlotRotationPorts,
|
|
59
74
|
type TelegramWorkspaceOperationRunner,
|
|
75
|
+
type TelegramWorkspaceSlotRotationPorts,
|
|
60
76
|
} from "./workspace-retirement.ts";
|
|
61
77
|
import {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
} from "./workspace-admission.ts";
|
|
66
|
-
import { TELEGRAM_WORKSPACE_SLOTS, TelegramWorkspaceSlotUnavailableError } from "./workspace-slots.ts";
|
|
78
|
+
TELEGRAM_WORKSPACE_SLOTS,
|
|
79
|
+
TelegramWorkspaceSlotUnavailableError,
|
|
80
|
+
} from "./workspace-slots.ts";
|
|
67
81
|
|
|
68
82
|
export const TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS = 15_000;
|
|
69
83
|
|
|
@@ -82,21 +96,9 @@ export interface TelegramBusLeaderRuntime<TContext> {
|
|
|
82
96
|
) => Promise<Threads.TelegramTopicTargetRecord>;
|
|
83
97
|
startPolling: (ctx: TContext) => Promise<void>;
|
|
84
98
|
stopPolling: () => Promise<void>;
|
|
85
|
-
routeQueueHandoff: (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
recipientInstanceId: string;
|
|
89
|
-
recipientRegistrationGeneration: string;
|
|
90
|
-
donorInstanceId: string;
|
|
91
|
-
donorProcessId: number;
|
|
92
|
-
donorProcessBirthId: string;
|
|
93
|
-
donorSessionGeneration: number;
|
|
94
|
-
donorAcquisitionId: string;
|
|
95
|
-
donorAcquiredAtMs: number;
|
|
96
|
-
handoffToken: string;
|
|
97
|
-
payload: TelegramQueueHandoffPayload;
|
|
98
|
-
sentAtMs: number;
|
|
99
|
-
}) => Promise<TelegramBusEnvelope>;
|
|
99
|
+
routeQueueHandoff: (
|
|
100
|
+
input: TelegramBusLeaderQueueHandoffOffer,
|
|
101
|
+
) => Promise<TelegramBusEnvelope>;
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
export interface TelegramBusFollowerLifecycleAnnouncement {
|
|
@@ -105,13 +107,6 @@ export interface TelegramBusFollowerLifecycleAnnouncement {
|
|
|
105
107
|
parseMode: "HTML";
|
|
106
108
|
}
|
|
107
109
|
|
|
108
|
-
function escapeHtml(text: string): string {
|
|
109
|
-
return text
|
|
110
|
-
.replace(/&/g, "&")
|
|
111
|
-
.replace(/</g, "<")
|
|
112
|
-
.replace(/>/g, ">");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
110
|
function formatTelegramBusInstanceLabel(input: {
|
|
116
111
|
threadName?: string;
|
|
117
112
|
slot?: string;
|
|
@@ -223,7 +218,11 @@ export interface TelegramBusLeaderApiProxyDeps {
|
|
|
223
218
|
fileName: string,
|
|
224
219
|
options?: TelegramApiCallOptions,
|
|
225
220
|
) => Promise<unknown>;
|
|
226
|
-
downloadFile: (
|
|
221
|
+
downloadFile: (
|
|
222
|
+
fileId: string,
|
|
223
|
+
suggestedName: string,
|
|
224
|
+
source?: TelegramAttachmentSource,
|
|
225
|
+
) => Promise<unknown>;
|
|
227
226
|
recoverStaleTargetError?: (
|
|
228
227
|
apiBody: unknown,
|
|
229
228
|
error: unknown,
|
|
@@ -232,7 +231,10 @@ export interface TelegramBusLeaderApiProxyDeps {
|
|
|
232
231
|
|
|
233
232
|
export interface TelegramBusLeaderRuntimeAssemblyDeps<TContext> {
|
|
234
233
|
getThreadDisplayMode?: () => TelegramThreadDisplayMode;
|
|
235
|
-
persistThreadDisplayMode?: (
|
|
234
|
+
persistThreadDisplayMode?: (
|
|
235
|
+
mode: TelegramThreadDisplayMode,
|
|
236
|
+
isCurrent: () => boolean,
|
|
237
|
+
) => Promise<void>;
|
|
236
238
|
onThreadDisplayChanged?: () => void;
|
|
237
239
|
runtime: Omit<
|
|
238
240
|
TelegramBusLeaderRuntimeDeps<TContext>,
|
|
@@ -273,10 +275,12 @@ export interface TelegramBusLeaderRuntimeAssemblyDeps<TContext> {
|
|
|
273
275
|
captureWorkspaceExternalProtection?: (
|
|
274
276
|
binding: Threads.TelegramWorkspaceThreadBinding,
|
|
275
277
|
) => Threads.TelegramWorkspaceExternalProtectionEvidence;
|
|
276
|
-
getWorkspaceAdmission?: () =>
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
278
|
+
getWorkspaceAdmission?: () =>
|
|
279
|
+
| Pick<
|
|
280
|
+
TelegramWorkspaceAdmissionLedger,
|
|
281
|
+
"acquireAdmission" | "releaseAdmission"
|
|
282
|
+
>
|
|
283
|
+
| undefined;
|
|
280
284
|
runWorkspaceOperation?: TelegramBusWorkspaceAdmissionRunner;
|
|
281
285
|
workspaceRotation?: TelegramWorkspaceSlotRotationPorts;
|
|
282
286
|
}
|
|
@@ -328,7 +332,9 @@ export function createTelegramBusFollowerSessionReplacementAuthority(deps: {
|
|
|
328
332
|
typeof intent.target.threadId !== "number" ||
|
|
329
333
|
follower.target?.chatId !== intent.target.chatId ||
|
|
330
334
|
follower.target.threadId !== intent.target.threadId ||
|
|
331
|
-
!cwd ||
|
|
335
|
+
!cwd ||
|
|
336
|
+
cwd !== intent.cwd ||
|
|
337
|
+
!sessionId ||
|
|
332
338
|
intent.profileName !== (deps.getTelegramProfile?.() ?? "default")
|
|
333
339
|
) {
|
|
334
340
|
throw new Error(
|
|
@@ -336,15 +342,19 @@ export function createTelegramBusFollowerSessionReplacementAuthority(deps: {
|
|
|
336
342
|
);
|
|
337
343
|
}
|
|
338
344
|
if (intent.expiresAtMs <= nowMs || intent.expiresAtMs > nowMs + ttlMs) {
|
|
339
|
-
throw new Error(
|
|
345
|
+
throw new Error(
|
|
346
|
+
"Telegram session replacement intent is expired or unbounded.",
|
|
347
|
+
);
|
|
340
348
|
}
|
|
341
349
|
return { cwd, sessionId };
|
|
342
350
|
};
|
|
343
351
|
return {
|
|
344
352
|
async publish(follower, intent, isCurrent) {
|
|
345
353
|
const { sessionId } = assertCommon(follower, intent);
|
|
346
|
-
if (
|
|
347
|
-
|
|
354
|
+
if (
|
|
355
|
+
intent.sourceInstanceId !== follower.instanceId ||
|
|
356
|
+
intent.sourceSessionId !== sessionId
|
|
357
|
+
) {
|
|
348
358
|
throw new Error(
|
|
349
359
|
"Telegram session replacement source does not match the current follower registration.",
|
|
350
360
|
);
|
|
@@ -353,7 +363,8 @@ export function createTelegramBusFollowerSessionReplacementAuthority(deps: {
|
|
|
353
363
|
if (!isCurrent()) return false;
|
|
354
364
|
const binding = deps.store.getWorkspaceBindingByTarget(intent.target);
|
|
355
365
|
if (
|
|
356
|
-
!binding ||
|
|
366
|
+
!binding ||
|
|
367
|
+
binding.cwd !== intent.cwd ||
|
|
357
368
|
binding.sessionId !== intent.sourceSessionId ||
|
|
358
369
|
binding.slot !== intent.slot ||
|
|
359
370
|
(binding.manualThreadName ?? binding.threadName) !== intent.threadName
|
|
@@ -376,9 +387,13 @@ export function createTelegramBusFollowerSessionReplacementAuthority(deps: {
|
|
|
376
387
|
}
|
|
377
388
|
await deps.store.load();
|
|
378
389
|
if (!isCurrent()) return false;
|
|
379
|
-
if (
|
|
380
|
-
|
|
381
|
-
|
|
390
|
+
if (
|
|
391
|
+
deps.store.getWorkspaceBindingByTarget(intent.target, sessionId)
|
|
392
|
+
?.cwd !== intent.cwd
|
|
393
|
+
) {
|
|
394
|
+
throw new Error(
|
|
395
|
+
"Telegram session replacement successor binding is unavailable.",
|
|
396
|
+
);
|
|
382
397
|
}
|
|
383
398
|
return deps.store.removeSessionReplacementIntent(intent, isCurrent);
|
|
384
399
|
},
|
|
@@ -391,9 +406,11 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
391
406
|
renameLeaderThreadAdmitted: (
|
|
392
407
|
threadName: string,
|
|
393
408
|
expectedTarget?: Threads.TelegramTopicTargetRecord["target"],
|
|
409
|
+
assertRecipientAuthority?: () => void,
|
|
394
410
|
) => Promise<Threads.TelegramTopicTargetRecord>;
|
|
395
411
|
resetLeaderThreadName: (
|
|
396
412
|
expectedTarget: Threads.TelegramTopicTargetRecord["target"],
|
|
413
|
+
assertRecipientAuthority?: () => void,
|
|
397
414
|
) => Promise<{ threadName: string }>;
|
|
398
415
|
resetThreadNameAdmitted: (
|
|
399
416
|
target: Threads.TelegramTopicTargetRecord["target"],
|
|
@@ -404,7 +421,9 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
404
421
|
? (input, operation) => {
|
|
405
422
|
const admission = deps.getWorkspaceAdmission?.();
|
|
406
423
|
if (!admission) {
|
|
407
|
-
throw new Error(
|
|
424
|
+
throw new Error(
|
|
425
|
+
"Telegram Workspace admission authority is unavailable.",
|
|
426
|
+
);
|
|
408
427
|
}
|
|
409
428
|
return runWithTelegramWorkspaceAdmissionsAsync({
|
|
410
429
|
ledger: admission,
|
|
@@ -419,7 +438,8 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
419
438
|
});
|
|
420
439
|
}
|
|
421
440
|
: undefined;
|
|
422
|
-
const runWorkspaceOperation =
|
|
441
|
+
const runWorkspaceOperation =
|
|
442
|
+
deps.runWorkspaceOperation ??
|
|
423
443
|
createTelegramWorkspaceOperationRuntime({
|
|
424
444
|
getWorkspaceAdmission: deps.getWorkspaceAdmission,
|
|
425
445
|
onReleaseError(error, operationKind) {
|
|
@@ -429,20 +449,28 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
429
449
|
});
|
|
430
450
|
},
|
|
431
451
|
}).run;
|
|
432
|
-
const runWithWorkspaceCapacity =
|
|
433
|
-
deps.
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
452
|
+
const runWithWorkspaceCapacity =
|
|
453
|
+
deps.workspaceRotation &&
|
|
454
|
+
deps.captureWorkspaceExternalProtection &&
|
|
455
|
+
deps.getCurrentLeaderEpoch
|
|
456
|
+
? createTelegramWorkspaceSlotRotation({
|
|
457
|
+
...deps.workspaceRotation,
|
|
458
|
+
store: deps.topicTargetStore,
|
|
459
|
+
getLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
460
|
+
getExternalProtection: deps.captureWorkspaceExternalProtection,
|
|
461
|
+
recordEvent(message, details) {
|
|
462
|
+
deps.recordRuntimeEvent("bus", message, details);
|
|
463
|
+
},
|
|
464
|
+
})
|
|
465
|
+
: undefined;
|
|
439
466
|
const captureLiveBindingKeys = (
|
|
440
467
|
bindings: readonly Threads.TelegramWorkspaceThreadBinding[],
|
|
441
|
-
): ReadonlySet<string> =>
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
468
|
+
): ReadonlySet<string> =>
|
|
469
|
+
resolveTelegramLiveWorkspaceBindingKeys(
|
|
470
|
+
bindings,
|
|
471
|
+
deps.topicTargetStore.getActiveByInstanceId(deps.instanceId)?.target,
|
|
472
|
+
deps.runtime.followerRegistry.list(),
|
|
473
|
+
);
|
|
446
474
|
const provisionerPorts = {
|
|
447
475
|
getAllowedUserId: deps.getAllowedUserId,
|
|
448
476
|
topicTargetStore: deps.topicTargetStore,
|
|
@@ -471,42 +499,59 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
471
499
|
runWorkspaceOperation,
|
|
472
500
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
473
501
|
};
|
|
474
|
-
const display = deps.getThreadDisplayMode
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
502
|
+
const display = deps.getThreadDisplayMode
|
|
503
|
+
? createTelegramThreadDisplayReconciler({
|
|
504
|
+
store: deps.topicTargetStore,
|
|
505
|
+
getMode: deps.getThreadDisplayMode,
|
|
506
|
+
getProfileKey: () => deps.getTelegramProfile?.() ?? "default",
|
|
507
|
+
getLeaderEpoch: () => deps.getCurrentLeaderEpoch?.(),
|
|
508
|
+
captureLiveBindingKeys,
|
|
509
|
+
captureBindingAuthority(binding) {
|
|
510
|
+
const matches = (target: TelegramTarget | undefined) =>
|
|
511
|
+
target?.chatId === binding.target.chatId &&
|
|
512
|
+
target?.threadId === binding.target.threadId;
|
|
513
|
+
const leader = deps.topicTargetStore.getActiveByInstanceId(
|
|
514
|
+
deps.instanceId,
|
|
515
|
+
);
|
|
516
|
+
if (leader && matches(leader.target)) {
|
|
517
|
+
return () =>
|
|
518
|
+
matches(
|
|
519
|
+
deps.topicTargetStore.getActiveByInstanceId(deps.instanceId)
|
|
520
|
+
?.target,
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
const follower = deps.runtime.followerRegistry.getByTarget(
|
|
524
|
+
binding.target,
|
|
525
|
+
);
|
|
526
|
+
if (!follower?.registrationGeneration) return undefined;
|
|
527
|
+
const generation = follower.registrationGeneration;
|
|
528
|
+
return () => {
|
|
529
|
+
const current = deps.runtime.followerRegistry.get(
|
|
530
|
+
follower.instanceId,
|
|
531
|
+
);
|
|
532
|
+
return (
|
|
533
|
+
current?.registrationGeneration === generation &&
|
|
534
|
+
matches(current.target)
|
|
535
|
+
);
|
|
536
|
+
};
|
|
537
|
+
},
|
|
538
|
+
callApi: deps.callApi,
|
|
539
|
+
})
|
|
540
|
+
: undefined;
|
|
497
541
|
const reconcileThreadDisplayOperation = async () => {
|
|
498
542
|
const result = await display!.reconcile();
|
|
499
543
|
deps.onThreadDisplayChanged?.();
|
|
500
544
|
return result;
|
|
501
545
|
};
|
|
502
|
-
const reconcileThreadDisplay = () =>
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
546
|
+
const reconcileThreadDisplay = () =>
|
|
547
|
+
runWorkspaceOperation(
|
|
548
|
+
{
|
|
549
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
550
|
+
operationKind: "workspace.reconcile-display",
|
|
551
|
+
scopes: [{ kind: "profile" }],
|
|
552
|
+
},
|
|
553
|
+
reconcileThreadDisplayOperation,
|
|
554
|
+
);
|
|
510
555
|
let startupDisplayTimer: ReturnType<typeof setTimeout> | undefined;
|
|
511
556
|
let startupDisplayGeneration = 0;
|
|
512
557
|
let startupDisplaySettled = true;
|
|
@@ -519,7 +564,9 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
519
564
|
const scheduleDisplay = () => {
|
|
520
565
|
if (!display || !startupDisplaySettled) return;
|
|
521
566
|
void reconcileThreadDisplay().catch((error) => {
|
|
522
|
-
deps.recordRuntimeEvent("bus", error, {
|
|
567
|
+
deps.recordRuntimeEvent("bus", error, {
|
|
568
|
+
phase: "thread-display-reconcile",
|
|
569
|
+
});
|
|
523
570
|
});
|
|
524
571
|
};
|
|
525
572
|
const beginStartupDisplayStabilization = () => {
|
|
@@ -528,8 +575,8 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
528
575
|
};
|
|
529
576
|
const armStartupDisplayReconciliation = () => {
|
|
530
577
|
const generation = startupDisplayGeneration;
|
|
531
|
-
const delayMs =
|
|
532
|
-
TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS;
|
|
578
|
+
const delayMs =
|
|
579
|
+
deps.runtime.followerStaleAfterMs ?? TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS;
|
|
533
580
|
startupDisplayTimer = setTimeout(() => {
|
|
534
581
|
if (startupDisplayGeneration !== generation) return;
|
|
535
582
|
startupDisplayTimer = undefined;
|
|
@@ -539,41 +586,68 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
539
586
|
startupDisplayTimer.unref?.();
|
|
540
587
|
};
|
|
541
588
|
let modeTail: Promise<void> = Promise.resolve();
|
|
542
|
-
const applyThreadDisplayMode = (
|
|
589
|
+
const applyThreadDisplayMode = (
|
|
590
|
+
mode: TelegramThreadDisplayMode,
|
|
591
|
+
isCurrent: () => boolean,
|
|
592
|
+
): Promise<void> => {
|
|
543
593
|
const epoch = deps.getCurrentLeaderEpoch?.();
|
|
544
594
|
const profile = deps.getTelegramProfile?.();
|
|
545
|
-
const current = () =>
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
595
|
+
const current = () =>
|
|
596
|
+
epoch !== undefined &&
|
|
597
|
+
deps.getCurrentLeaderEpoch?.() === epoch &&
|
|
598
|
+
deps.getTelegramProfile?.() === profile &&
|
|
599
|
+
isCurrent();
|
|
600
|
+
const run = modeTail.then(() =>
|
|
601
|
+
runWorkspaceOperation(
|
|
602
|
+
{
|
|
603
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
604
|
+
operationKind: "workspace.set-display-mode",
|
|
605
|
+
scopes: [{ kind: "profile" }],
|
|
606
|
+
},
|
|
607
|
+
async () => {
|
|
608
|
+
if (!display || !deps.persistThreadDisplayMode || !current()) {
|
|
609
|
+
throw new Error(
|
|
610
|
+
"Telegram Thread display setting requires current leader authority.",
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
const assertDisplayPeers = () => {
|
|
614
|
+
const requiredCapability =
|
|
615
|
+
mode === "directory-snake" || mode === "directory-title"
|
|
616
|
+
? TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT
|
|
617
|
+
: TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE;
|
|
618
|
+
if (
|
|
619
|
+
mode !== "names" &&
|
|
620
|
+
deps.runtime.followerRegistry
|
|
621
|
+
.list()
|
|
622
|
+
.some(
|
|
623
|
+
(follower) =>
|
|
624
|
+
!hasTelegramBusCapability(
|
|
625
|
+
follower.protocol,
|
|
626
|
+
requiredCapability,
|
|
627
|
+
),
|
|
628
|
+
)
|
|
629
|
+
)
|
|
630
|
+
throw new Error(
|
|
631
|
+
"Update or restart all connected followers before changing Thread display mode.",
|
|
632
|
+
);
|
|
633
|
+
};
|
|
634
|
+
assertDisplayPeers();
|
|
635
|
+
await deps.persistThreadDisplayMode(mode, current);
|
|
636
|
+
assertDisplayPeers();
|
|
637
|
+
if (!current() || deps.getThreadDisplayMode?.() !== mode) {
|
|
638
|
+
throw new Error(
|
|
639
|
+
"Telegram Thread display preference changed before application.",
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
await reconcileThreadDisplayOperation();
|
|
643
|
+
if (!current() || deps.getThreadDisplayMode?.() !== mode) {
|
|
644
|
+
throw new Error(
|
|
645
|
+
"Telegram Thread display application lost its originating authority.",
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
),
|
|
650
|
+
);
|
|
577
651
|
modeTail = run.catch(() => undefined);
|
|
578
652
|
return run;
|
|
579
653
|
};
|
|
@@ -593,149 +667,414 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
593
667
|
const disconnectFollower = createTelegramBusFollowerDisconnectHandler({
|
|
594
668
|
...provisionerPorts,
|
|
595
669
|
});
|
|
596
|
-
const cleanupConfirmedDeadFollower =
|
|
597
|
-
|
|
598
|
-
|
|
670
|
+
const cleanupConfirmedDeadFollower =
|
|
671
|
+
createTelegramBusFollowerConfirmedDeadHandler({
|
|
672
|
+
...provisionerPorts,
|
|
673
|
+
});
|
|
599
674
|
const provisionFollowerTarget = createTelegramBusFollowerTargetProvisioner({
|
|
600
675
|
...provisionerPorts,
|
|
601
676
|
});
|
|
602
677
|
const renameLeaderThreadAdmitted = async (
|
|
603
678
|
threadName: string,
|
|
604
679
|
expectedTarget?: Threads.TelegramTopicTargetRecord["target"],
|
|
680
|
+
assertRecipientAuthority?: () => void,
|
|
605
681
|
) => {
|
|
606
|
-
const
|
|
607
|
-
|
|
682
|
+
const store = deps.topicTargetStore,
|
|
683
|
+
callApi = deps.callApi;
|
|
684
|
+
const publishName = store.renameByTargetAndPersist.bind(store);
|
|
685
|
+
const persist = store.persist.bind(store);
|
|
686
|
+
const instanceId = deps.instanceId;
|
|
687
|
+
const getProfile = deps.getTelegramProfile,
|
|
688
|
+
getUser = deps.getAllowedUserId;
|
|
689
|
+
const getEpoch = deps.getCurrentLeaderEpoch;
|
|
690
|
+
const profile = getProfile?.(),
|
|
691
|
+
user = getUser();
|
|
692
|
+
const recipientAuthority = assertRecipientAuthority;
|
|
693
|
+
recipientAuthority?.();
|
|
694
|
+
const leaderEpoch = getEpoch?.();
|
|
695
|
+
if (getEpoch && leaderEpoch === undefined) {
|
|
608
696
|
throw new Error(
|
|
609
697
|
"Telegram Workspace Thread rename requires leader ownership.",
|
|
610
698
|
);
|
|
611
699
|
}
|
|
612
700
|
const record = Threads.findCurrentTelegramInstanceThreadRecord({
|
|
613
|
-
records:
|
|
614
|
-
instanceId
|
|
701
|
+
records: store.list(),
|
|
702
|
+
instanceId,
|
|
615
703
|
});
|
|
616
704
|
if (typeof record?.target.threadId !== "number") {
|
|
617
705
|
throw new Error("No Workspace Thread is bound to this Pi instance.");
|
|
618
706
|
}
|
|
619
|
-
if (
|
|
620
|
-
|
|
707
|
+
if (
|
|
708
|
+
expectedTarget &&
|
|
709
|
+
(record.target.chatId !== expectedTarget.chatId ||
|
|
710
|
+
record.target.threadId !== expectedTarget.threadId)
|
|
711
|
+
) {
|
|
621
712
|
throw new Error("Telegram Workspace Thread rename target changed.");
|
|
622
713
|
}
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
instanceId: deps.instanceId,
|
|
638
|
-
});
|
|
639
|
-
if (current?.target.chatId !== record.target.chatId ||
|
|
640
|
-
current.target.threadId !== record.target.threadId) {
|
|
641
|
-
throw new Error("Telegram Workspace Thread rename target changed.");
|
|
642
|
-
}
|
|
643
|
-
},
|
|
714
|
+
const target = { ...record.target },
|
|
715
|
+
slot = record.slot,
|
|
716
|
+
owner = structuredClone(record.owner);
|
|
717
|
+
const bindingIdentity = (
|
|
718
|
+
binding: Threads.TelegramWorkspaceThreadBinding,
|
|
719
|
+
) => ({
|
|
720
|
+
bindingKey: binding.bindingKey,
|
|
721
|
+
cwd: binding.cwd,
|
|
722
|
+
workspaceKey: binding.workspaceKey,
|
|
723
|
+
sessionId: binding.sessionId,
|
|
724
|
+
sessionKey: binding.sessionKey,
|
|
725
|
+
instanceSlot: binding.instanceSlot,
|
|
726
|
+
slot: binding.slot,
|
|
727
|
+
target: { ...binding.target },
|
|
644
728
|
});
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
729
|
+
const bindings = store
|
|
730
|
+
.listWorkspaceBindings()
|
|
731
|
+
.filter(
|
|
732
|
+
(binding) =>
|
|
733
|
+
binding.target.chatId === target.chatId &&
|
|
734
|
+
binding.target.threadId === target.threadId,
|
|
735
|
+
);
|
|
736
|
+
if (
|
|
737
|
+
recipientAuthority &&
|
|
738
|
+
(bindings.length !== 1 ||
|
|
739
|
+
!bindings[0]?.sessionId ||
|
|
740
|
+
bindings[0].slot !== slot)
|
|
741
|
+
)
|
|
742
|
+
throw new Error(
|
|
743
|
+
"Telegram Workspace Thread rename recipient binding authority is unavailable.",
|
|
744
|
+
);
|
|
745
|
+
const capturedBindings = bindings.map(bindingIdentity);
|
|
746
|
+
const assertAuthority = () => {
|
|
747
|
+
recipientAuthority?.();
|
|
748
|
+
if (
|
|
749
|
+
(getEpoch && getEpoch() !== leaderEpoch) ||
|
|
750
|
+
getProfile?.() !== profile ||
|
|
751
|
+
getUser() !== user ||
|
|
752
|
+
deps.topicTargetStore !== store ||
|
|
753
|
+
deps.instanceId !== instanceId
|
|
754
|
+
)
|
|
755
|
+
throw new Error(
|
|
756
|
+
"Telegram Workspace Thread rename lost leader ownership.",
|
|
757
|
+
);
|
|
758
|
+
const current = Threads.findCurrentTelegramInstanceThreadRecord({
|
|
759
|
+
records: store.list(),
|
|
760
|
+
instanceId,
|
|
761
|
+
});
|
|
762
|
+
const currentBindings = store
|
|
763
|
+
.listWorkspaceBindings()
|
|
764
|
+
.filter(
|
|
765
|
+
(binding) =>
|
|
766
|
+
binding.target.chatId === target.chatId &&
|
|
767
|
+
binding.target.threadId === target.threadId,
|
|
768
|
+
)
|
|
769
|
+
.map(bindingIdentity);
|
|
770
|
+
if (
|
|
771
|
+
current?.target.chatId !== target.chatId ||
|
|
772
|
+
current.target.threadId !== target.threadId ||
|
|
773
|
+
current.slot !== slot ||
|
|
774
|
+
current.status !== record.status ||
|
|
775
|
+
!isDeepStrictEqual(current.owner, owner) ||
|
|
776
|
+
!isDeepStrictEqual(currentBindings, capturedBindings)
|
|
777
|
+
)
|
|
778
|
+
throw new Error(
|
|
779
|
+
"Telegram Workspace Thread rename target changed or binding authority was lost.",
|
|
780
|
+
);
|
|
781
|
+
};
|
|
782
|
+
assertAuthority();
|
|
783
|
+
const current = () => {
|
|
784
|
+
try {
|
|
785
|
+
assertAuthority();
|
|
786
|
+
return true;
|
|
787
|
+
} catch {
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
const rename = Threads.createTelegramTopicTargetRenamer({
|
|
792
|
+
store: recipientAuthority
|
|
793
|
+
? {
|
|
794
|
+
list: store.list.bind(store),
|
|
795
|
+
listWorkspaceBindings: store.listWorkspaceBindings.bind(store),
|
|
796
|
+
listPendingProvisions: store.listPendingProvisions.bind(store),
|
|
797
|
+
renameByTarget: (target, name, options) =>
|
|
798
|
+
publishName(
|
|
799
|
+
target,
|
|
800
|
+
name,
|
|
801
|
+
{ updateDisplayTitle: options?.updateDisplayTitle ?? true },
|
|
802
|
+
current,
|
|
803
|
+
),
|
|
804
|
+
}
|
|
805
|
+
: store,
|
|
806
|
+
callApi,
|
|
807
|
+
assertAuthority,
|
|
652
808
|
});
|
|
809
|
+
const renamed = await rename({ target, threadName, slot });
|
|
810
|
+
assertAuthority();
|
|
653
811
|
if (!renamed) {
|
|
654
812
|
throw new Error(
|
|
655
813
|
ThreadNaming.getTelegramTopicThreadNameValidationError(
|
|
656
814
|
threadName,
|
|
657
|
-
|
|
815
|
+
slot,
|
|
658
816
|
) ?? "Telegram Workspace Thread name is already reserved.",
|
|
659
817
|
);
|
|
660
818
|
}
|
|
661
|
-
await
|
|
819
|
+
if (!recipientAuthority) await persist(current);
|
|
820
|
+
assertAuthority();
|
|
662
821
|
return renamed;
|
|
663
822
|
};
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
assertTargetCurrent: () => void
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
823
|
+
const prepareThreadNameReset = (
|
|
824
|
+
expectedTarget: Threads.TelegramTopicTargetRecord["target"],
|
|
825
|
+
assertTargetCurrent: () => void,
|
|
826
|
+
assertRecipientAuthority?: () => void,
|
|
827
|
+
): (() => Promise<{ threadName: string }>) => {
|
|
828
|
+
const target = { ...expectedTarget },
|
|
829
|
+
store = deps.topicTargetStore,
|
|
830
|
+
callApi = deps.callApi;
|
|
831
|
+
const publishReset = store.clearManualNameByTargetAndPersist.bind(store);
|
|
832
|
+
const clearManualName = store.clearManualNameByTarget.bind(store),
|
|
833
|
+
persist = store.persist.bind(store);
|
|
834
|
+
const getEpoch = deps.getCurrentLeaderEpoch,
|
|
835
|
+
getProfile = deps.getTelegramProfile;
|
|
836
|
+
const getUser = deps.getAllowedUserId,
|
|
837
|
+
getMode = deps.getThreadDisplayMode;
|
|
838
|
+
const recipientAuthority = assertRecipientAuthority,
|
|
839
|
+
targetAuthority = assertTargetCurrent;
|
|
840
|
+
recipientAuthority?.();
|
|
841
|
+
const leaderEpoch = getEpoch?.(),
|
|
842
|
+
profile = getProfile?.(),
|
|
843
|
+
user = getUser(),
|
|
844
|
+
instanceId = deps.instanceId;
|
|
845
|
+
const mode = getMode?.() ?? "letters";
|
|
846
|
+
if (getEpoch && leaderEpoch === undefined) {
|
|
847
|
+
throw new Error(
|
|
848
|
+
"Telegram Workspace Thread reset requires leader ownership.",
|
|
849
|
+
);
|
|
671
850
|
}
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
851
|
+
const matchingBindings = () =>
|
|
852
|
+
store
|
|
853
|
+
.listWorkspaceBindings()
|
|
854
|
+
.filter(
|
|
855
|
+
(candidate) =>
|
|
856
|
+
candidate.target.chatId === target.chatId &&
|
|
857
|
+
candidate.target.threadId === target.threadId,
|
|
858
|
+
);
|
|
859
|
+
const bindings = matchingBindings(),
|
|
860
|
+
binding = bindings[0];
|
|
861
|
+
if (!binding)
|
|
862
|
+
throw new Error("No Workspace Thread binding is available to reset.");
|
|
863
|
+
const record = Threads.findCurrentTelegramInstanceThreadRecord({
|
|
864
|
+
records: store.list(),
|
|
865
|
+
instanceId,
|
|
682
866
|
});
|
|
683
|
-
if (
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
867
|
+
if (
|
|
868
|
+
recipientAuthority &&
|
|
869
|
+
(bindings.length !== 1 ||
|
|
870
|
+
!binding.sessionId ||
|
|
871
|
+
record?.status !== "active" ||
|
|
872
|
+
record.owner?.kind !== "leader" ||
|
|
873
|
+
record.owner.cwd !== binding.cwd ||
|
|
874
|
+
record.target.chatId !== target.chatId ||
|
|
875
|
+
record.target.threadId !== target.threadId ||
|
|
876
|
+
record.slot !== binding.slot)
|
|
877
|
+
)
|
|
878
|
+
throw new Error(
|
|
879
|
+
"Telegram Workspace Thread reset recipient binding authority is unavailable.",
|
|
880
|
+
);
|
|
881
|
+
const bindingIdentity = (
|
|
882
|
+
value: Threads.TelegramWorkspaceThreadBinding,
|
|
883
|
+
) => ({
|
|
884
|
+
bindingKey: value.bindingKey,
|
|
885
|
+
cwd: value.cwd,
|
|
886
|
+
workspaceKey: value.workspaceKey,
|
|
887
|
+
sessionId: value.sessionId,
|
|
888
|
+
sessionKey: value.sessionKey,
|
|
889
|
+
instanceSlot: value.instanceSlot,
|
|
890
|
+
slot: value.slot,
|
|
891
|
+
target: { ...value.target },
|
|
691
892
|
});
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
893
|
+
const capturedBinding = bindingIdentity(binding),
|
|
894
|
+
owner = record && structuredClone(record.owner);
|
|
895
|
+
const resolveTitle = (value: Threads.TelegramWorkspaceThreadBinding) =>
|
|
896
|
+
resolveTelegramInitialWorkspaceDisplayName({
|
|
897
|
+
bindings: store.listWorkspaceBindings(),
|
|
898
|
+
binding: { ...value, manualThreadName: undefined },
|
|
899
|
+
mode,
|
|
900
|
+
preserveRetainedManualName: false,
|
|
901
|
+
});
|
|
902
|
+
const automaticTitle = resolveTitle(binding);
|
|
903
|
+
if (!automaticTitle)
|
|
904
|
+
throw new Error("Telegram Workspace automatic title is unavailable.");
|
|
905
|
+
const assertAuthority = () => {
|
|
906
|
+
recipientAuthority?.();
|
|
907
|
+
if (getEpoch && getEpoch() !== leaderEpoch)
|
|
908
|
+
throw new Error(
|
|
909
|
+
"Telegram Workspace Thread reset lost leader ownership.",
|
|
910
|
+
);
|
|
911
|
+
targetAuthority();
|
|
912
|
+
if (!recipientAuthority) return;
|
|
913
|
+
if (
|
|
914
|
+
getProfile?.() !== profile ||
|
|
915
|
+
getUser() !== user ||
|
|
916
|
+
deps.topicTargetStore !== store ||
|
|
917
|
+
deps.instanceId !== instanceId
|
|
918
|
+
)
|
|
919
|
+
throw new Error(
|
|
920
|
+
"Telegram Workspace Thread reset lost recipient authority.",
|
|
921
|
+
);
|
|
922
|
+
const currentRecord = Threads.findCurrentTelegramInstanceThreadRecord({
|
|
923
|
+
records: store.list(),
|
|
924
|
+
instanceId,
|
|
925
|
+
});
|
|
926
|
+
const currentBindings = matchingBindings();
|
|
927
|
+
if (
|
|
928
|
+
currentRecord?.target.chatId !== target.chatId ||
|
|
929
|
+
currentRecord.target.threadId !== target.threadId ||
|
|
930
|
+
currentRecord.slot !== record!.slot ||
|
|
931
|
+
currentRecord.status !== record!.status ||
|
|
932
|
+
!isDeepStrictEqual(currentRecord.owner, owner) ||
|
|
933
|
+
currentBindings.length !== 1 ||
|
|
934
|
+
!isDeepStrictEqual(
|
|
935
|
+
bindingIdentity(currentBindings[0]!),
|
|
936
|
+
capturedBinding,
|
|
937
|
+
)
|
|
938
|
+
)
|
|
939
|
+
throw new Error(
|
|
940
|
+
"Telegram Workspace Thread reset target changed or binding authority was lost.",
|
|
941
|
+
);
|
|
942
|
+
if (
|
|
943
|
+
(getMode?.() ?? "letters") !== mode ||
|
|
944
|
+
resolveTitle(currentBindings[0]!) !== automaticTitle
|
|
945
|
+
)
|
|
946
|
+
throw new Error("Telegram Workspace automatic-title policy changed.");
|
|
947
|
+
};
|
|
948
|
+
assertAuthority();
|
|
949
|
+
const current = () => {
|
|
950
|
+
try {
|
|
951
|
+
assertAuthority();
|
|
952
|
+
return true;
|
|
953
|
+
} catch {
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
return async () => {
|
|
958
|
+
assertAuthority();
|
|
959
|
+
const body = {
|
|
960
|
+
chat_id: target.chatId,
|
|
961
|
+
message_thread_id: target.threadId,
|
|
962
|
+
name: automaticTitle,
|
|
963
|
+
};
|
|
964
|
+
if (recipientAuthority)
|
|
965
|
+
await callApi("editForumTopic", body, { assertAuthority });
|
|
966
|
+
else await callApi("editForumTopic", body);
|
|
967
|
+
assertAuthority();
|
|
968
|
+
if (
|
|
969
|
+
recipientAuthority &&
|
|
970
|
+
(!isDeepStrictEqual(matchingBindings()[0], binding) ||
|
|
971
|
+
!isDeepStrictEqual(
|
|
972
|
+
Threads.findCurrentTelegramInstanceThreadRecord({
|
|
973
|
+
records: store.list(),
|
|
974
|
+
instanceId,
|
|
975
|
+
}),
|
|
976
|
+
record,
|
|
977
|
+
))
|
|
978
|
+
)
|
|
979
|
+
throw new Error(
|
|
980
|
+
"Telegram Workspace Thread reset metadata changed before publication.",
|
|
981
|
+
);
|
|
982
|
+
const reset = recipientAuthority
|
|
983
|
+
? await publishReset(target, automaticTitle, current)
|
|
984
|
+
: clearManualName(target, automaticTitle);
|
|
985
|
+
if (!reset)
|
|
986
|
+
throw new Error("Telegram Workspace Thread reset changed binding.");
|
|
987
|
+
if (!recipientAuthority) await persist();
|
|
988
|
+
assertAuthority();
|
|
989
|
+
if (
|
|
990
|
+
recipientAuthority &&
|
|
991
|
+
(reset.manualThreadName !== undefined ||
|
|
992
|
+
matchingBindings()[0]?.manualThreadName !== undefined ||
|
|
993
|
+
matchingBindings()[0]?.displayTitle !== automaticTitle ||
|
|
994
|
+
store.getActiveByInstanceId(instanceId)?.manualThreadName !==
|
|
995
|
+
undefined)
|
|
996
|
+
)
|
|
997
|
+
throw new Error(
|
|
998
|
+
"Telegram Workspace Thread reset result metadata changed.",
|
|
999
|
+
);
|
|
1000
|
+
return { threadName: automaticTitle };
|
|
1001
|
+
};
|
|
1002
|
+
};
|
|
1003
|
+
const resetThreadNameAdmitted = async (
|
|
1004
|
+
target: Threads.TelegramTopicTargetRecord["target"],
|
|
1005
|
+
assertTargetCurrent: () => void = () => undefined,
|
|
1006
|
+
): Promise<{ threadName: string }> =>
|
|
1007
|
+
prepareThreadNameReset(target, assertTargetCurrent)();
|
|
1008
|
+
const renameLeaderThread = async (threadName: string) => {
|
|
1009
|
+
const getEpoch = deps.getCurrentLeaderEpoch,
|
|
1010
|
+
getProfile = deps.getTelegramProfile;
|
|
1011
|
+
const getUser = deps.getAllowedUserId,
|
|
1012
|
+
store = deps.topicTargetStore;
|
|
1013
|
+
const epoch = getEpoch?.(),
|
|
1014
|
+
profile = getProfile?.(),
|
|
1015
|
+
user = getUser();
|
|
1016
|
+
return runWorkspaceOperation(
|
|
1017
|
+
{
|
|
1018
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
1019
|
+
operationKind: "workspace.rename-leader",
|
|
1020
|
+
scopes: [{ kind: "profile" }],
|
|
1021
|
+
},
|
|
1022
|
+
() => {
|
|
1023
|
+
if (
|
|
1024
|
+
getEpoch?.() !== epoch ||
|
|
1025
|
+
getProfile?.() !== profile ||
|
|
1026
|
+
getUser() !== user ||
|
|
1027
|
+
deps.topicTargetStore !== store
|
|
1028
|
+
)
|
|
1029
|
+
throw new Error(
|
|
1030
|
+
"Telegram Workspace Thread rename lost leader ownership during admission.",
|
|
1031
|
+
);
|
|
1032
|
+
return renameLeaderThreadAdmitted(threadName);
|
|
1033
|
+
},
|
|
700
1034
|
);
|
|
701
|
-
if (!reset) throw new Error("Telegram Workspace Thread reset changed binding.");
|
|
702
|
-
await deps.topicTargetStore.persist();
|
|
703
|
-
return { threadName: automaticTitle };
|
|
704
1035
|
};
|
|
705
|
-
const
|
|
706
|
-
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
707
|
-
operationKind: "workspace.rename-leader",
|
|
708
|
-
scopes: [{ kind: "profile" }],
|
|
709
|
-
}, () => renameLeaderThreadAdmitted(threadName));
|
|
710
|
-
const resetLeaderThreadName = (
|
|
1036
|
+
const resetLeaderThreadName = async (
|
|
711
1037
|
expectedTarget: Threads.TelegramTopicTargetRecord["target"],
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
record.target.
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
1038
|
+
assertRecipientAuthority?: () => void,
|
|
1039
|
+
): Promise<{ threadName: string }> => {
|
|
1040
|
+
const target = { ...expectedTarget },
|
|
1041
|
+
store = deps.topicTargetStore,
|
|
1042
|
+
instanceId = deps.instanceId;
|
|
1043
|
+
const assertTargetCurrent = () => {
|
|
1044
|
+
const record = Threads.findCurrentTelegramInstanceThreadRecord({
|
|
1045
|
+
records: store.list(),
|
|
1046
|
+
instanceId,
|
|
1047
|
+
});
|
|
1048
|
+
if (!record || typeof record.target.threadId !== "number")
|
|
1049
|
+
throw new Error("No Workspace Thread is bound to this Pi instance.");
|
|
1050
|
+
if (
|
|
1051
|
+
record.target.chatId !== target.chatId ||
|
|
1052
|
+
record.target.threadId !== target.threadId
|
|
1053
|
+
)
|
|
1054
|
+
throw new Error("Telegram Workspace Thread reset target changed.");
|
|
1055
|
+
};
|
|
1056
|
+
// Selected lifetimes are captured before admission; ordinary reset keeps its post-admission preparation.
|
|
1057
|
+
const prepared = assertRecipientAuthority
|
|
1058
|
+
? prepareThreadNameReset(
|
|
1059
|
+
target,
|
|
1060
|
+
assertTargetCurrent,
|
|
1061
|
+
assertRecipientAuthority,
|
|
1062
|
+
)
|
|
1063
|
+
: undefined;
|
|
1064
|
+
return runWorkspaceOperation(
|
|
1065
|
+
{
|
|
1066
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
1067
|
+
operationKind: "workspace.reset-leader-name",
|
|
1068
|
+
scopes: [{ kind: "profile" }],
|
|
1069
|
+
},
|
|
1070
|
+
() => {
|
|
1071
|
+
assertTargetCurrent();
|
|
1072
|
+
return prepared
|
|
1073
|
+
? prepared()
|
|
1074
|
+
: resetThreadNameAdmitted(target, assertTargetCurrent);
|
|
1075
|
+
},
|
|
1076
|
+
);
|
|
1077
|
+
};
|
|
739
1078
|
const followerSessionReplacement =
|
|
740
1079
|
createTelegramBusFollowerSessionReplacementAuthority({
|
|
741
1080
|
store: deps.topicTargetStore,
|
|
@@ -749,25 +1088,31 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
749
1088
|
onFollowerRegistered: scheduleDisplay,
|
|
750
1089
|
provisionLeaderTarget: (ctx) => {
|
|
751
1090
|
const chatId = deps.getAllowedUserId();
|
|
752
|
-
const provision = () =>
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
1091
|
+
const provision = () =>
|
|
1092
|
+
runWorkspaceOperation(
|
|
1093
|
+
{
|
|
1094
|
+
operationId: `leader-provision:${deps.instanceId}`,
|
|
1095
|
+
operationKind: "workspace.provision-leader",
|
|
1096
|
+
scopes: [
|
|
1097
|
+
typeof chatId === "number"
|
|
1098
|
+
? { kind: "chat", chatId }
|
|
1099
|
+
: { kind: "profile" },
|
|
1100
|
+
],
|
|
1101
|
+
},
|
|
1102
|
+
() => provisionLeaderTarget(ctx),
|
|
1103
|
+
);
|
|
1104
|
+
return runWithWorkspaceCapacity
|
|
1105
|
+
? runWithWorkspaceCapacity(provision)
|
|
1106
|
+
: provision();
|
|
765
1107
|
},
|
|
766
1108
|
getFollowerDisplayTitle(follower) {
|
|
767
|
-
const binding = deps.topicTargetStore
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1109
|
+
const binding = deps.topicTargetStore
|
|
1110
|
+
.listWorkspaceBindings()
|
|
1111
|
+
.find(
|
|
1112
|
+
(binding) =>
|
|
1113
|
+
binding.target.chatId === follower.target?.chatId &&
|
|
1114
|
+
binding.target.threadId === follower.target?.threadId,
|
|
1115
|
+
);
|
|
771
1116
|
return binding?.displayTitle ?? binding?.threadName;
|
|
772
1117
|
},
|
|
773
1118
|
onFollowerDisconnected: async (follower) => {
|
|
@@ -782,24 +1127,57 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
782
1127
|
scheduleDisplay();
|
|
783
1128
|
},
|
|
784
1129
|
async renameFollowerThread(follower, threadName) {
|
|
785
|
-
return runWorkspaceOperation(
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1130
|
+
return runWorkspaceOperation(
|
|
1131
|
+
{
|
|
1132
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
1133
|
+
operationKind: "workspace.rename-follower",
|
|
1134
|
+
scopes: [{ kind: "profile" }],
|
|
1135
|
+
},
|
|
1136
|
+
async () => {
|
|
1137
|
+
const leaderEpoch = deps.getCurrentLeaderEpoch?.();
|
|
1138
|
+
if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
|
|
1139
|
+
throw new Error(
|
|
1140
|
+
"Telegram Workspace Thread rename requires leader ownership.",
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
if (typeof follower.target?.threadId !== "number") {
|
|
1144
|
+
throw new Error("Telegram follower has no bound Workspace Thread.");
|
|
1145
|
+
}
|
|
1146
|
+
const rename = Threads.createTelegramTopicTargetRenamer({
|
|
1147
|
+
store: deps.topicTargetStore,
|
|
1148
|
+
callApi: deps.callApi,
|
|
1149
|
+
assertAuthority() {
|
|
1150
|
+
if (
|
|
1151
|
+
deps.getCurrentLeaderEpoch &&
|
|
1152
|
+
deps.getCurrentLeaderEpoch() !== leaderEpoch
|
|
1153
|
+
) {
|
|
1154
|
+
throw new Error(
|
|
1155
|
+
"Telegram Workspace Thread rename lost leader ownership.",
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
const current = deps.runtime.followerRegistry.get(
|
|
1159
|
+
follower.instanceId,
|
|
1160
|
+
);
|
|
1161
|
+
if (
|
|
1162
|
+
current?.registrationGeneration !==
|
|
1163
|
+
follower.registrationGeneration ||
|
|
1164
|
+
current?.target?.chatId !== follower.target?.chatId ||
|
|
1165
|
+
current?.target?.threadId !== follower.target?.threadId
|
|
1166
|
+
) {
|
|
1167
|
+
throw new Error(
|
|
1168
|
+
"Telegram follower Workspace Thread target changed.",
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
},
|
|
1172
|
+
});
|
|
1173
|
+
const renamed = await rename({
|
|
1174
|
+
target: {
|
|
1175
|
+
chatId: follower.target.chatId,
|
|
1176
|
+
threadId: follower.target.threadId,
|
|
1177
|
+
},
|
|
1178
|
+
threadName,
|
|
1179
|
+
slot: follower.slot,
|
|
1180
|
+
});
|
|
803
1181
|
if (
|
|
804
1182
|
deps.getCurrentLeaderEpoch &&
|
|
805
1183
|
deps.getCurrentLeaderEpoch() !== leaderEpoch
|
|
@@ -808,78 +1186,72 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
808
1186
|
"Telegram Workspace Thread rename lost leader ownership.",
|
|
809
1187
|
);
|
|
810
1188
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1189
|
+
if (!renamed) {
|
|
1190
|
+
throw new Error(
|
|
1191
|
+
ThreadNaming.getTelegramTopicThreadNameValidationError(
|
|
1192
|
+
threadName,
|
|
1193
|
+
follower.slot,
|
|
1194
|
+
) ?? "Telegram Workspace Thread name is already reserved.",
|
|
1195
|
+
);
|
|
816
1196
|
}
|
|
1197
|
+
await deps.topicTargetStore.persist();
|
|
1198
|
+
return { threadName: renamed.manualThreadName ?? threadName };
|
|
817
1199
|
},
|
|
818
|
-
|
|
819
|
-
const renamed = await rename({
|
|
820
|
-
target: {
|
|
821
|
-
chatId: follower.target.chatId,
|
|
822
|
-
threadId: follower.target.threadId,
|
|
823
|
-
},
|
|
824
|
-
threadName,
|
|
825
|
-
slot: follower.slot,
|
|
826
|
-
});
|
|
827
|
-
if (
|
|
828
|
-
deps.getCurrentLeaderEpoch &&
|
|
829
|
-
deps.getCurrentLeaderEpoch() !== leaderEpoch
|
|
830
|
-
) {
|
|
831
|
-
throw new Error(
|
|
832
|
-
"Telegram Workspace Thread rename lost leader ownership.",
|
|
833
|
-
);
|
|
834
|
-
}
|
|
835
|
-
if (!renamed) {
|
|
836
|
-
throw new Error(
|
|
837
|
-
ThreadNaming.getTelegramTopicThreadNameValidationError(
|
|
838
|
-
threadName,
|
|
839
|
-
follower.slot,
|
|
840
|
-
) ?? "Telegram Workspace Thread name is already reserved.",
|
|
841
|
-
);
|
|
842
|
-
}
|
|
843
|
-
await deps.topicTargetStore.persist();
|
|
844
|
-
return { threadName: renamed.manualThreadName ?? threadName };
|
|
845
|
-
});
|
|
1200
|
+
);
|
|
846
1201
|
},
|
|
847
|
-
resetFollowerThreadName: (follower) =>
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
};
|
|
861
|
-
return resetThreadNameAdmitted(target, () => {
|
|
862
|
-
const current = deps.runtime.followerRegistry.get(follower.instanceId);
|
|
863
|
-
if (current?.registrationGeneration !== follower.registrationGeneration ||
|
|
864
|
-
current?.target?.chatId !== target.chatId ||
|
|
865
|
-
current?.target?.threadId !== target.threadId) {
|
|
866
|
-
throw new Error("Telegram follower Workspace Thread target changed.");
|
|
1202
|
+
resetFollowerThreadName: (follower) =>
|
|
1203
|
+
runWorkspaceOperation(
|
|
1204
|
+
{
|
|
1205
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
1206
|
+
operationKind: "workspace.reset-follower-name",
|
|
1207
|
+
scopes: [{ kind: "profile" }],
|
|
1208
|
+
},
|
|
1209
|
+
() => {
|
|
1210
|
+
if (
|
|
1211
|
+
!follower.target ||
|
|
1212
|
+
typeof follower.target.threadId !== "number"
|
|
1213
|
+
) {
|
|
1214
|
+
throw new Error("Telegram follower has no bound Workspace Thread.");
|
|
867
1215
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1216
|
+
const target = {
|
|
1217
|
+
chatId: follower.target.chatId,
|
|
1218
|
+
threadId: follower.target.threadId,
|
|
1219
|
+
};
|
|
1220
|
+
return resetThreadNameAdmitted(target, () => {
|
|
1221
|
+
const current = deps.runtime.followerRegistry.get(
|
|
1222
|
+
follower.instanceId,
|
|
1223
|
+
);
|
|
1224
|
+
if (
|
|
1225
|
+
current?.registrationGeneration !==
|
|
1226
|
+
follower.registrationGeneration ||
|
|
1227
|
+
current?.target?.chatId !== target.chatId ||
|
|
1228
|
+
current?.target?.threadId !== target.threadId
|
|
1229
|
+
) {
|
|
1230
|
+
throw new Error(
|
|
1231
|
+
"Telegram follower Workspace Thread target changed.",
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
});
|
|
1235
|
+
},
|
|
1236
|
+
),
|
|
871
1237
|
publishFollowerSessionReplacement: (follower, intent, isCurrent) =>
|
|
872
|
-
runWorkspaceOperation(
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1238
|
+
runWorkspaceOperation(
|
|
1239
|
+
{
|
|
1240
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
1241
|
+
operationKind: "workspace.publish-follower-session-replacement",
|
|
1242
|
+
scopes: [{ kind: "profile" }],
|
|
1243
|
+
},
|
|
1244
|
+
() => followerSessionReplacement.publish(follower, intent, isCurrent),
|
|
1245
|
+
),
|
|
877
1246
|
settleFollowerSessionReplacement: (follower, intent, isCurrent) =>
|
|
878
|
-
runWorkspaceOperation(
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1247
|
+
runWorkspaceOperation(
|
|
1248
|
+
{
|
|
1249
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
1250
|
+
operationKind: "workspace.settle-follower-session-replacement",
|
|
1251
|
+
scopes: [{ kind: "profile" }],
|
|
1252
|
+
},
|
|
1253
|
+
() => followerSessionReplacement.settle(follower, intent, isCurrent),
|
|
1254
|
+
),
|
|
883
1255
|
onFollowerConfirmedDead: async (follower) => {
|
|
884
1256
|
await runWorkspaceOperation(
|
|
885
1257
|
{
|
|
@@ -894,54 +1266,106 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
894
1266
|
async onFollowerConfirmedDeadPreserved(follower, isDetached, operationId) {
|
|
895
1267
|
const epoch = deps.getCurrentLeaderEpoch?.();
|
|
896
1268
|
const profile = deps.getTelegramProfile?.();
|
|
897
|
-
if (
|
|
898
|
-
|
|
899
|
-
|
|
1269
|
+
if (
|
|
1270
|
+
epoch === undefined ||
|
|
1271
|
+
!follower.registrationGeneration ||
|
|
1272
|
+
typeof follower.target?.threadId !== "number"
|
|
1273
|
+
)
|
|
1274
|
+
return false;
|
|
1275
|
+
const isCurrent = () =>
|
|
1276
|
+
isDetached() &&
|
|
1277
|
+
deps.getCurrentLeaderEpoch?.() === epoch &&
|
|
900
1278
|
deps.getTelegramProfile?.() === profile;
|
|
901
1279
|
if (!isCurrent()) return false;
|
|
902
|
-
const settled = await runWorkspaceOperation(
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1280
|
+
const settled = await runWorkspaceOperation(
|
|
1281
|
+
{
|
|
1282
|
+
operationId,
|
|
1283
|
+
operationKind: "workspace.preserve-dead-follower",
|
|
1284
|
+
scopes: [{ kind: "profile" }],
|
|
1285
|
+
},
|
|
1286
|
+
async () => {
|
|
1287
|
+
if (!isCurrent()) return false;
|
|
1288
|
+
await deps.topicTargetStore.load();
|
|
1289
|
+
if (!isCurrent()) return false;
|
|
1290
|
+
const record = deps.topicTargetStore.getActiveByInstanceId(
|
|
1291
|
+
follower.instanceId,
|
|
1292
|
+
);
|
|
1293
|
+
if (
|
|
1294
|
+
!record ||
|
|
1295
|
+
(follower.profileKey &&
|
|
1296
|
+
record.profileKey !== follower.profileKey) ||
|
|
912
1297
|
record.target.chatId !== follower.target!.chatId ||
|
|
913
|
-
record.target.threadId !== follower.target!.threadId
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1298
|
+
record.target.threadId !== follower.target!.threadId
|
|
1299
|
+
)
|
|
1300
|
+
return true;
|
|
1301
|
+
const committed = await deps.topicTargetStore.detachTargetOwner(
|
|
1302
|
+
record,
|
|
1303
|
+
isCurrent,
|
|
1304
|
+
);
|
|
1305
|
+
if (!committed && isCurrent()) {
|
|
1306
|
+
throw new Error(
|
|
1307
|
+
"Telegram preserved follower detachment was not committed.",
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
return committed;
|
|
1311
|
+
},
|
|
1312
|
+
);
|
|
920
1313
|
if (settled && isCurrent()) scheduleDisplay();
|
|
921
1314
|
return settled;
|
|
922
1315
|
},
|
|
923
|
-
provisionFollowerTarget: (registration, options) =>
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1316
|
+
provisionFollowerTarget: (registration, options) =>
|
|
1317
|
+
runWorkspaceOperation(
|
|
1318
|
+
{
|
|
1319
|
+
operationId: `follower-provision:${registration.instanceId}:${registration.registrationGeneration}`,
|
|
1320
|
+
operationKind: "workspace.provision-follower",
|
|
1321
|
+
scopes: [{ kind: "profile" }],
|
|
1322
|
+
},
|
|
1323
|
+
() => provisionFollowerTarget(registration, options),
|
|
1324
|
+
),
|
|
1325
|
+
commitFollowerRegistration: (input, publish) =>
|
|
1326
|
+
runWorkspaceOperation(
|
|
1327
|
+
{
|
|
1328
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
1329
|
+
operationKind: "workspace.publish-follower-registration",
|
|
1330
|
+
scopes: [{ kind: "profile" }],
|
|
1331
|
+
},
|
|
1332
|
+
async () => {
|
|
1333
|
+
if (!input.target) {
|
|
1334
|
+
publish();
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
await deps.topicTargetStore.load();
|
|
1338
|
+
const binding =
|
|
1339
|
+
input.registration.cwd && input.registration.sessionId
|
|
1340
|
+
? deps.topicTargetStore.getWorkspaceBindingByTarget(
|
|
1341
|
+
input.target,
|
|
1342
|
+
input.registration.sessionId,
|
|
1343
|
+
)
|
|
1344
|
+
: undefined;
|
|
1345
|
+
const bindingKey =
|
|
1346
|
+
binding &&
|
|
1347
|
+
input.registration.cwd &&
|
|
1348
|
+
binding.cwd ===
|
|
1349
|
+
WorkspaceIdentity.normalizeTelegramWorkspacePath(
|
|
1350
|
+
input.registration.cwd,
|
|
1351
|
+
)
|
|
1352
|
+
? binding.bindingKey
|
|
1353
|
+
: undefined;
|
|
1354
|
+
if (
|
|
1355
|
+
input.target.threadId !== undefined &&
|
|
1356
|
+
input.registration.cwd &&
|
|
1357
|
+
input.registration.sessionId &&
|
|
1358
|
+
!bindingKey
|
|
1359
|
+
)
|
|
1360
|
+
throw new Error(
|
|
1361
|
+
"Telegram follower Workspace binding changed before registration publication.",
|
|
1362
|
+
);
|
|
1363
|
+
deps.topicTargetStore.commitWorkspaceRestoreRegistration(
|
|
1364
|
+
{ target: input.target, slot: input.slot, bindingKey },
|
|
1365
|
+
publish,
|
|
1366
|
+
);
|
|
1367
|
+
},
|
|
1368
|
+
),
|
|
945
1369
|
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
946
1370
|
getTelegramProfile: deps.getTelegramProfile,
|
|
947
1371
|
getAllowedUserId: deps.getAllowedUserId,
|
|
@@ -997,86 +1421,30 @@ export type TelegramBusFollowerMessageOwnershipRecorder = (
|
|
|
997
1421
|
record: TelegramBusFollowerMessageOwnershipRecord,
|
|
998
1422
|
) => void;
|
|
999
1423
|
|
|
1000
|
-
export type TelegramBusFollowerRegistrationCommitter = (
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1424
|
+
export type TelegramBusFollowerRegistrationCommitter = (
|
|
1425
|
+
input: {
|
|
1426
|
+
registration: TelegramBusInstanceRegistration;
|
|
1427
|
+
target?: TelegramTarget;
|
|
1428
|
+
slot?: string;
|
|
1429
|
+
},
|
|
1430
|
+
publish: () => void,
|
|
1431
|
+
) => Promise<void> | void;
|
|
1005
1432
|
|
|
1006
|
-
export interface TelegramBusLeaderRuntimeDeps<TContext>
|
|
1433
|
+
export interface TelegramBusLeaderRuntimeDeps<TContext> extends Omit<
|
|
1434
|
+
TelegramBusLeaderEnvelopeHandlerDeps,
|
|
1435
|
+
"runFollowerMutation" | "getWorkspaceRestoreObservationGeneration"
|
|
1436
|
+
> {
|
|
1007
1437
|
socketPath: TelegramBusSocketPathSource;
|
|
1008
1438
|
commitEndpointPublication?: (commit: () => void) => boolean;
|
|
1009
|
-
followerRegistry: TelegramBusFollowerRegistry;
|
|
1010
|
-
authSecret?: string;
|
|
1011
|
-
protocolIdentity: TelegramBusProtocolIdentity;
|
|
1012
1439
|
startPolling: (ctx: TContext) => void | Promise<void>;
|
|
1013
1440
|
stopPolling: () => void | Promise<void>;
|
|
1014
|
-
callApi?: (method: string, args: unknown[]) => Promise<unknown> | unknown;
|
|
1015
|
-
authorizeFollowerApiCall?: (input: {
|
|
1016
|
-
follower: TelegramBusFollowerView;
|
|
1017
|
-
method: string;
|
|
1018
|
-
args: unknown[];
|
|
1019
|
-
}) => boolean;
|
|
1020
|
-
recordFollowerMessageOwnership?: TelegramBusFollowerMessageOwnershipRecorder;
|
|
1021
|
-
resolveAgentTarget?: (
|
|
1022
|
-
follower: TelegramBusFollowerView,
|
|
1023
|
-
selector: Extract<
|
|
1024
|
-
TelegramBusEnvelope,
|
|
1025
|
-
{ kind: "follower.resolveAgentTarget" }
|
|
1026
|
-
>["selector"],
|
|
1027
|
-
) => Promise<TelegramTarget | undefined> | TelegramTarget | undefined;
|
|
1028
|
-
routeAgentMessage?: (
|
|
1029
|
-
follower: TelegramBusFollowerView,
|
|
1030
|
-
message: Extract<
|
|
1031
|
-
TelegramBusEnvelope,
|
|
1032
|
-
{ kind: "follower.routeAgentMessage" }
|
|
1033
|
-
>["message"],
|
|
1034
|
-
) => Promise<void> | void;
|
|
1035
|
-
routeQueueHandoff?: (
|
|
1036
|
-
follower: TelegramBusFollowerView,
|
|
1037
|
-
envelope: Extract<
|
|
1038
|
-
TelegramBusEnvelope,
|
|
1039
|
-
{ kind: "follower.offerQueueHandoff" }
|
|
1040
|
-
>,
|
|
1041
|
-
) => Promise<unknown> | unknown;
|
|
1042
|
-
provisionFollowerTarget?: (
|
|
1043
|
-
registration: TelegramBusInstanceRegistration,
|
|
1044
|
-
options?: { existingWorkspaceBindingOnly?: boolean },
|
|
1045
|
-
) => Promise<TelegramTarget | undefined> | TelegramTarget | undefined;
|
|
1046
|
-
commitFollowerRegistration?: TelegramBusFollowerRegistrationCommitter;
|
|
1047
|
-
renameFollowerThread?: (
|
|
1048
|
-
follower: TelegramBusFollowerView,
|
|
1049
|
-
threadName: string,
|
|
1050
|
-
) => Promise<{ threadName: string }> | { threadName: string };
|
|
1051
|
-
resetFollowerThreadName?: (
|
|
1052
|
-
follower: TelegramBusFollowerView,
|
|
1053
|
-
) => Promise<{ threadName: string }> | { threadName: string };
|
|
1054
|
-
publishFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
|
|
1055
|
-
settleFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
|
|
1056
|
-
getFollowerDisplayTitle?: (follower: TelegramBusFollowerView) => string | undefined;
|
|
1057
|
-
onFollowerRegistered?: () => void;
|
|
1058
|
-
/** Observation only: reacquire admission and inspect work, never infer completion.
|
|
1059
|
-
* Listener settlement ends its currentness fence; return asynchronous work to retain that fence. */
|
|
1060
|
-
onWorkspaceRestoreRecipientObserved?: (follower: TelegramBusFollowerView, isCurrent: () => boolean) => Promise<void> | void;
|
|
1061
|
-
applyThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
|
|
1062
|
-
getThreadDisplayMode?: () => TelegramThreadDisplayMode;
|
|
1063
|
-
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
1064
|
-
getTelegramProfile?: () => string | undefined;
|
|
1065
|
-
getAllowedUserId?: () => number | undefined;
|
|
1066
1441
|
provisionLeaderTarget?: (ctx: TContext) => Promise<void> | void;
|
|
1067
|
-
runWorkspaceAdmission?: TelegramBusWorkspaceAdmissionRunner;
|
|
1068
|
-
runWithWorkspaceCapacity?: TelegramWorkspaceCapacityRunner;
|
|
1069
|
-
getNowMs?: () => number;
|
|
1070
|
-
timeoutMs?: number;
|
|
1071
1442
|
followerPruneIntervalMs?: number;
|
|
1072
1443
|
/** Leader housekeeping after each current prune pass; throttling is the callee's concern. */
|
|
1073
1444
|
afterFollowerPrune?: () => void;
|
|
1074
1445
|
followerStaleAfterMs?: number;
|
|
1075
1446
|
isFollowerProcessAlive?: (pid: number) => boolean;
|
|
1076
1447
|
shouldCleanupConfirmedDeadFollower?: () => Promise<boolean> | boolean;
|
|
1077
|
-
onFollowerDisconnected?: (
|
|
1078
|
-
follower: TelegramBusFollowerView,
|
|
1079
|
-
) => Promise<void> | void;
|
|
1080
1448
|
onFollowerConfirmedDead?: (
|
|
1081
1449
|
follower: TelegramBusFollowerView,
|
|
1082
1450
|
) => Promise<void> | void;
|
|
@@ -1086,11 +1454,6 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
|
1086
1454
|
isDetached: () => boolean,
|
|
1087
1455
|
operationId: string,
|
|
1088
1456
|
) => Promise<boolean> | boolean;
|
|
1089
|
-
recordRuntimeEvent?: (
|
|
1090
|
-
category: string,
|
|
1091
|
-
error: unknown,
|
|
1092
|
-
details?: Record<string, unknown>,
|
|
1093
|
-
) => void;
|
|
1094
1457
|
}
|
|
1095
1458
|
|
|
1096
1459
|
export function createTelegramBusInstanceLifecycleAnnouncement(input: {
|
|
@@ -1179,8 +1542,7 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1179
1542
|
registration.instanceId,
|
|
1180
1543
|
registration.previousInstanceId,
|
|
1181
1544
|
{
|
|
1182
|
-
existingBindingOnly:
|
|
1183
|
-
options?.existingWorkspaceBindingOnly === true,
|
|
1545
|
+
existingBindingOnly: options?.existingWorkspaceBindingOnly === true,
|
|
1184
1546
|
sessionId: registration.sessionId,
|
|
1185
1547
|
onCapacityUnavailable() {
|
|
1186
1548
|
capacityUnavailable = true;
|
|
@@ -1240,7 +1602,8 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1240
1602
|
return (
|
|
1241
1603
|
record.owner?.kind === "manual-follower" &&
|
|
1242
1604
|
((matchesCurrentIdentity && matchesRequestedTarget) ||
|
|
1243
|
-
matchesSessionHandoff ||
|
|
1605
|
+
matchesSessionHandoff ||
|
|
1606
|
+
matchesWorkspaceRecovery)
|
|
1244
1607
|
);
|
|
1245
1608
|
});
|
|
1246
1609
|
const followerOwner =
|
|
@@ -1281,7 +1644,10 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1281
1644
|
},
|
|
1282
1645
|
profileKey: followerProfileKey,
|
|
1283
1646
|
threadName: workspaceBinding?.threadName ?? registration.threadName,
|
|
1284
|
-
preferredSlot:
|
|
1647
|
+
preferredSlot:
|
|
1648
|
+
workspaceIdentity?.slot ??
|
|
1649
|
+
workspaceBinding?.slot ??
|
|
1650
|
+
registration.slot,
|
|
1285
1651
|
...(workspaceIdentity
|
|
1286
1652
|
? {
|
|
1287
1653
|
workspaceBindingKey: workspaceIdentity.bindingKey,
|
|
@@ -1312,7 +1678,10 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1312
1678
|
? recoveryHint.threadName
|
|
1313
1679
|
: undefined;
|
|
1314
1680
|
const requestedSlot =
|
|
1315
|
-
workspaceIdentity?.slot ??
|
|
1681
|
+
workspaceIdentity?.slot ??
|
|
1682
|
+
workspaceBinding?.slot ??
|
|
1683
|
+
registration.slot ??
|
|
1684
|
+
recoveryHint?.slot;
|
|
1316
1685
|
const recoveredSlot = deps.topicTargetStore.allocateSlot(
|
|
1317
1686
|
followerProfileKey,
|
|
1318
1687
|
requestedSlot,
|
|
@@ -1352,19 +1721,33 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1352
1721
|
const runRegistration = async (): Promise<
|
|
1353
1722
|
(TelegramTarget & { slot?: string; threadName?: string }) | undefined
|
|
1354
1723
|
> => {
|
|
1355
|
-
if (requestedTarget)
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1724
|
+
if (requestedTarget)
|
|
1725
|
+
deps.topicTargetStore.assertWorkspaceRestoreRegistration({
|
|
1726
|
+
target: requestedTarget,
|
|
1727
|
+
bindingKey: workspaceIdentity?.bindingKey,
|
|
1728
|
+
slot:
|
|
1729
|
+
workspaceIdentity?.slot ??
|
|
1730
|
+
workspaceBinding?.slot ??
|
|
1731
|
+
registration.slot,
|
|
1732
|
+
});
|
|
1359
1733
|
const recoveryTarget = reconnectRecord?.target ?? recoverableTarget;
|
|
1360
1734
|
if (recoveryTarget) {
|
|
1361
|
-
Threads.assertTelegramPendingTopicRecoveryAllowed(
|
|
1735
|
+
Threads.assertTelegramPendingTopicRecoveryAllowed(
|
|
1736
|
+
deps.topicTargetStore,
|
|
1737
|
+
recoveryTarget,
|
|
1738
|
+
);
|
|
1362
1739
|
}
|
|
1363
|
-
const pendingTargetRecovery =
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
(
|
|
1367
|
-
|
|
1740
|
+
const pendingTargetRecovery =
|
|
1741
|
+
recoverableTarget &&
|
|
1742
|
+
deps.topicTargetStore
|
|
1743
|
+
.listPendingProvisions()
|
|
1744
|
+
.some(
|
|
1745
|
+
(entry) =>
|
|
1746
|
+
entry.target?.chatId === recoverableTarget.chatId &&
|
|
1747
|
+
entry.target?.threadId === recoverableTarget.threadId &&
|
|
1748
|
+
(entry.instanceId === registration.instanceId ||
|
|
1749
|
+
entry.profileKey === followerProfileKey),
|
|
1750
|
+
);
|
|
1368
1751
|
let result: Threads.TelegramTopicTargetProvisionResult = reconnectRecord
|
|
1369
1752
|
? {
|
|
1370
1753
|
target: reconnectRecord.target,
|
|
@@ -1375,10 +1758,8 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1375
1758
|
? await recoverRequestedTarget()
|
|
1376
1759
|
: await provisionTarget();
|
|
1377
1760
|
const alignResultWithWorkspaceSlot = (): void => {
|
|
1378
|
-
if (
|
|
1379
|
-
|
|
1380
|
-
result.record.slot === workspaceIdentity.slot
|
|
1381
|
-
) return;
|
|
1761
|
+
if (!workspaceIdentity || result.record.slot === workspaceIdentity.slot)
|
|
1762
|
+
return;
|
|
1382
1763
|
deps.recordRuntimeEvent(
|
|
1383
1764
|
"bus",
|
|
1384
1765
|
"Telegram follower record slot reconciled to its Workspace claim",
|
|
@@ -1397,9 +1778,12 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1397
1778
|
};
|
|
1398
1779
|
};
|
|
1399
1780
|
alignResultWithWorkspaceSlot();
|
|
1400
|
-
const assertRestoreRegistration = (): void =>
|
|
1401
|
-
|
|
1402
|
-
|
|
1781
|
+
const assertRestoreRegistration = (): void =>
|
|
1782
|
+
deps.topicTargetStore.assertWorkspaceRestoreRegistration({
|
|
1783
|
+
target: result.target,
|
|
1784
|
+
slot: result.record.slot,
|
|
1785
|
+
bindingKey: workspaceIdentity?.bindingKey,
|
|
1786
|
+
});
|
|
1403
1787
|
assertRestoreRegistration();
|
|
1404
1788
|
const crossSessionReuse =
|
|
1405
1789
|
!!reconnectRecord &&
|
|
@@ -1420,8 +1804,7 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1420
1804
|
record: refreshedRecord,
|
|
1421
1805
|
};
|
|
1422
1806
|
}
|
|
1423
|
-
const probeRequiredRecord =
|
|
1424
|
-
reconnectRecord?.status === "probe-required";
|
|
1807
|
+
const probeRequiredRecord = reconnectRecord?.status === "probe-required";
|
|
1425
1808
|
const exactSessionHandoff =
|
|
1426
1809
|
crossSessionReuse &&
|
|
1427
1810
|
!!requestedTarget &&
|
|
@@ -1436,10 +1819,13 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1436
1819
|
!result.reused || requiresVisibilityProbe
|
|
1437
1820
|
? createTelegramBusInstanceLifecycleAnnouncement({
|
|
1438
1821
|
target: result.target,
|
|
1439
|
-
threadName:
|
|
1822
|
+
threadName:
|
|
1823
|
+
result.displayTitle ??
|
|
1440
1824
|
(workspaceBinding?.target.chatId === result.target.chatId &&
|
|
1441
|
-
|
|
1442
|
-
? workspaceBinding.displayTitle
|
|
1825
|
+
workspaceBinding.target.threadId === result.target.threadId
|
|
1826
|
+
? workspaceBinding.displayTitle
|
|
1827
|
+
: undefined) ??
|
|
1828
|
+
result.record.threadName,
|
|
1443
1829
|
slot: result.record.slot,
|
|
1444
1830
|
state: "connected",
|
|
1445
1831
|
})
|
|
@@ -1557,8 +1943,8 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1557
1943
|
}
|
|
1558
1944
|
if (workspaceIdentity) {
|
|
1559
1945
|
alignResultWithWorkspaceSlot();
|
|
1560
|
-
const workspaceCommit =
|
|
1561
|
-
|
|
1946
|
+
const workspaceCommit = Threads.commitTelegramWorkspaceProvisionBinding(
|
|
1947
|
+
{
|
|
1562
1948
|
store: deps.topicTargetStore,
|
|
1563
1949
|
instanceId: registration.instanceId,
|
|
1564
1950
|
profileKey: followerProfileKey,
|
|
@@ -1571,18 +1957,29 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1571
1957
|
: {}),
|
|
1572
1958
|
slot: workspaceIdentity.slot,
|
|
1573
1959
|
...(workspaceIdentity.sessionId
|
|
1574
|
-
? {
|
|
1575
|
-
|
|
1960
|
+
? {
|
|
1961
|
+
journalSources: [
|
|
1962
|
+
{
|
|
1963
|
+
sessionId: workspaceIdentity.sessionId,
|
|
1964
|
+
recipientBindingKey: followerProfileKey,
|
|
1965
|
+
},
|
|
1966
|
+
],
|
|
1967
|
+
journalBindingKeys: [],
|
|
1968
|
+
}
|
|
1576
1969
|
: { journalBindingKeys: [followerProfileKey] }),
|
|
1577
1970
|
journalBindingsComplete: true,
|
|
1578
1971
|
updatedAtMs: getNowMs(),
|
|
1579
1972
|
},
|
|
1580
|
-
}
|
|
1973
|
+
},
|
|
1974
|
+
);
|
|
1581
1975
|
if (connectedAnnouncement && workspaceCommit.displayTitle) {
|
|
1582
|
-
connectedAnnouncement =
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1976
|
+
connectedAnnouncement =
|
|
1977
|
+
createTelegramBusInstanceLifecycleAnnouncement({
|
|
1978
|
+
target: result.target,
|
|
1979
|
+
threadName: workspaceCommit.displayTitle,
|
|
1980
|
+
slot: result.record.slot,
|
|
1981
|
+
state: "connected",
|
|
1982
|
+
});
|
|
1586
1983
|
}
|
|
1587
1984
|
deps.topicTargetStore.markWorkspaceBindingActiveByTarget(result.target);
|
|
1588
1985
|
await deps.topicTargetStore.persist();
|
|
@@ -1605,95 +2002,102 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1605
2002
|
target: result.target,
|
|
1606
2003
|
reused: result.reused,
|
|
1607
2004
|
});
|
|
1608
|
-
scheduleTelegramBusLeaderBackgroundTask(
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
2005
|
+
scheduleTelegramBusLeaderBackgroundTask(
|
|
2006
|
+
async () => {
|
|
2007
|
+
const backgroundStartedAtMs = Date.now();
|
|
2008
|
+
if (connectedAnnouncement) {
|
|
2009
|
+
try {
|
|
2010
|
+
await deps.callApi("sendMessage", {
|
|
2011
|
+
chat_id: connectedAnnouncement.target.chatId,
|
|
2012
|
+
message_thread_id: connectedAnnouncement.target.threadId,
|
|
2013
|
+
text: connectedAnnouncement.text,
|
|
2014
|
+
parse_mode: connectedAnnouncement.parseMode,
|
|
2015
|
+
});
|
|
2016
|
+
} catch (error) {
|
|
2017
|
+
deps.recordRuntimeEvent("telegram", error, {
|
|
2018
|
+
phase: "follower-topic-announce",
|
|
2019
|
+
instanceId: registration.instanceId,
|
|
2020
|
+
chatId: result.target.chatId,
|
|
2021
|
+
threadId: result.target.threadId,
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
const reconcile = async (): Promise<void> => {
|
|
2026
|
+
await ThreadReconciler.applyThreadReconciliationPlan(
|
|
2027
|
+
ThreadReconciler.planThreadReconciliation({
|
|
2028
|
+
nowMs: getNowMs(),
|
|
2029
|
+
currentLeaderEpoch: deps.getCurrentLeaderEpoch?.(),
|
|
2030
|
+
records: recordsBeforeProvision,
|
|
2031
|
+
pendingProvisions:
|
|
2032
|
+
deps.topicTargetStore.listPendingProvisions(),
|
|
2033
|
+
replacedBindings: [
|
|
2034
|
+
{
|
|
2035
|
+
instanceId: registration.instanceId,
|
|
2036
|
+
replacementTarget: result.target,
|
|
2037
|
+
},
|
|
2038
|
+
],
|
|
2039
|
+
}),
|
|
2040
|
+
{
|
|
2041
|
+
isCleanupTargetProtected:
|
|
2042
|
+
Threads.createTelegramCleanupTargetProtection(
|
|
2043
|
+
deps.topicTargetStore,
|
|
2044
|
+
),
|
|
2045
|
+
callApi: deps.callApi,
|
|
2046
|
+
markStaleByTarget(target, syncStatus, lastSyncError) {
|
|
2047
|
+
return deps.topicTargetStore.markStaleByTarget(
|
|
2048
|
+
target,
|
|
2049
|
+
syncStatus,
|
|
2050
|
+
lastSyncError,
|
|
2051
|
+
);
|
|
2052
|
+
},
|
|
2053
|
+
persist() {
|
|
2054
|
+
return deps.topicTargetStore.persist();
|
|
2055
|
+
},
|
|
2056
|
+
removePendingProvisionById(id) {
|
|
2057
|
+
return deps.topicTargetStore.removePendingProvision(id);
|
|
2058
|
+
},
|
|
2059
|
+
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
2060
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
2061
|
+
},
|
|
2062
|
+
);
|
|
2063
|
+
await deps.topicTargetStore.persist();
|
|
2064
|
+
};
|
|
2065
|
+
try {
|
|
2066
|
+
if (deps.runWorkspaceOperation) {
|
|
2067
|
+
await deps.runWorkspaceOperation(
|
|
2068
|
+
{
|
|
2069
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
2070
|
+
operationKind: "workspace.reconcile-follower-provision",
|
|
2071
|
+
scopes: [{ kind: "profile" }],
|
|
2072
|
+
},
|
|
2073
|
+
reconcile,
|
|
2074
|
+
);
|
|
2075
|
+
} else {
|
|
2076
|
+
await reconcile();
|
|
2077
|
+
}
|
|
2078
|
+
} catch (error) {
|
|
1619
2079
|
deps.recordRuntimeEvent("telegram", error, {
|
|
1620
|
-
phase: "follower-
|
|
2080
|
+
phase: "follower-register-background-reconcile",
|
|
1621
2081
|
instanceId: registration.instanceId,
|
|
1622
2082
|
chatId: result.target.chatId,
|
|
1623
2083
|
threadId: result.target.threadId,
|
|
1624
2084
|
});
|
|
1625
2085
|
}
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
ThreadReconciler.planThreadReconciliation({
|
|
1630
|
-
nowMs: getNowMs(),
|
|
1631
|
-
currentLeaderEpoch: deps.getCurrentLeaderEpoch?.(),
|
|
1632
|
-
records: recordsBeforeProvision,
|
|
1633
|
-
pendingProvisions: deps.topicTargetStore.listPendingProvisions(),
|
|
1634
|
-
replacedBindings: [
|
|
1635
|
-
{
|
|
1636
|
-
instanceId: registration.instanceId,
|
|
1637
|
-
replacementTarget: result.target,
|
|
1638
|
-
},
|
|
1639
|
-
],
|
|
1640
|
-
}),
|
|
1641
|
-
{
|
|
1642
|
-
isCleanupTargetProtected: Threads.createTelegramCleanupTargetProtection(deps.topicTargetStore),
|
|
1643
|
-
callApi: deps.callApi,
|
|
1644
|
-
markStaleByTarget(target, syncStatus, lastSyncError) {
|
|
1645
|
-
return deps.topicTargetStore.markStaleByTarget(
|
|
1646
|
-
target,
|
|
1647
|
-
syncStatus,
|
|
1648
|
-
lastSyncError,
|
|
1649
|
-
);
|
|
1650
|
-
},
|
|
1651
|
-
persist() {
|
|
1652
|
-
return deps.topicTargetStore.persist();
|
|
1653
|
-
},
|
|
1654
|
-
removePendingProvisionById(id) {
|
|
1655
|
-
return deps.topicTargetStore.removePendingProvision(id);
|
|
1656
|
-
},
|
|
1657
|
-
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
1658
|
-
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1659
|
-
},
|
|
1660
|
-
);
|
|
1661
|
-
await deps.topicTargetStore.persist();
|
|
1662
|
-
};
|
|
1663
|
-
try {
|
|
1664
|
-
if (deps.runWorkspaceOperation) {
|
|
1665
|
-
await deps.runWorkspaceOperation(
|
|
1666
|
-
{
|
|
1667
|
-
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
1668
|
-
operationKind: "workspace.reconcile-follower-provision",
|
|
1669
|
-
scopes: [{ kind: "profile" }],
|
|
1670
|
-
},
|
|
1671
|
-
reconcile,
|
|
1672
|
-
);
|
|
1673
|
-
} else {
|
|
1674
|
-
await reconcile();
|
|
1675
|
-
}
|
|
1676
|
-
} catch (error) {
|
|
1677
|
-
deps.recordRuntimeEvent("telegram", error, {
|
|
1678
|
-
phase: "follower-register-background-reconcile",
|
|
2086
|
+
recordSlowTelegramBusFollowerRegistrationStep(deps, {
|
|
2087
|
+
phase: "follower-register-background",
|
|
2088
|
+
elapsedMs: Date.now() - backgroundStartedAtMs,
|
|
1679
2089
|
instanceId: registration.instanceId,
|
|
1680
|
-
|
|
1681
|
-
|
|
2090
|
+
target: result.target,
|
|
2091
|
+
reused: result.reused,
|
|
1682
2092
|
});
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
}, (error) => {
|
|
1692
|
-
deps.recordRuntimeEvent("bus", error, {
|
|
1693
|
-
phase: "follower-register-background-owner",
|
|
1694
|
-
instanceId: registration.instanceId,
|
|
1695
|
-
});
|
|
1696
|
-
});
|
|
2093
|
+
},
|
|
2094
|
+
(error) => {
|
|
2095
|
+
deps.recordRuntimeEvent("bus", error, {
|
|
2096
|
+
phase: "follower-register-background-owner",
|
|
2097
|
+
instanceId: registration.instanceId,
|
|
2098
|
+
});
|
|
2099
|
+
},
|
|
2100
|
+
);
|
|
1697
2101
|
return {
|
|
1698
2102
|
...result.target,
|
|
1699
2103
|
slot: result.record.slot,
|
|
@@ -1734,9 +2138,19 @@ function createTelegramBusFollowerCleanupHandler(
|
|
|
1734
2138
|
target: { chatId: target.chatId, threadId: target.threadId },
|
|
1735
2139
|
requestedAtMs: (deps.getNowMs ?? Date.now)(),
|
|
1736
2140
|
};
|
|
1737
|
-
const departingRecord = deps.topicTargetStore
|
|
1738
|
-
|
|
1739
|
-
|
|
2141
|
+
const departingRecord = deps.topicTargetStore
|
|
2142
|
+
.list()
|
|
2143
|
+
.find(
|
|
2144
|
+
(record) =>
|
|
2145
|
+
record.instanceId === follower.instanceId &&
|
|
2146
|
+
record.target.chatId === target.chatId &&
|
|
2147
|
+
record.target.threadId === target.threadId,
|
|
2148
|
+
);
|
|
2149
|
+
const isCleanupTargetProtected =
|
|
2150
|
+
Threads.createTelegramCleanupTargetProtection(
|
|
2151
|
+
deps.topicTargetStore,
|
|
2152
|
+
departingRecord,
|
|
2153
|
+
);
|
|
1740
2154
|
deps.topicTargetStore.upsertPendingCleanup(intent);
|
|
1741
2155
|
await deps.topicTargetStore.persist();
|
|
1742
2156
|
const cleanupPlan = ThreadReconciler.planThreadReconciliation({
|
|
@@ -1751,7 +2165,11 @@ function createTelegramBusFollowerCleanupHandler(
|
|
|
1751
2165
|
isCleanupTargetProtected,
|
|
1752
2166
|
callApi: deps.callApi,
|
|
1753
2167
|
markStaleByTarget(target, syncStatus, lastSyncError) {
|
|
1754
|
-
return deps.topicTargetStore.markStaleByTarget(
|
|
2168
|
+
return deps.topicTargetStore.markStaleByTarget(
|
|
2169
|
+
target,
|
|
2170
|
+
syncStatus,
|
|
2171
|
+
lastSyncError,
|
|
2172
|
+
);
|
|
1755
2173
|
},
|
|
1756
2174
|
removeCleanupIntentById: deps.topicTargetStore.removePendingCleanup,
|
|
1757
2175
|
persist: deps.topicTargetStore.persist,
|
|
@@ -1759,7 +2177,12 @@ function createTelegramBusFollowerCleanupHandler(
|
|
|
1759
2177
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1760
2178
|
},
|
|
1761
2179
|
);
|
|
1762
|
-
if (
|
|
2180
|
+
if (
|
|
2181
|
+
cleanupPlan.actions.some((action) =>
|
|
2182
|
+
isCleanupTargetProtected(action.target, action),
|
|
2183
|
+
)
|
|
2184
|
+
)
|
|
2185
|
+
return;
|
|
1763
2186
|
if (cleanup.incompleteActions?.length) {
|
|
1764
2187
|
throw new Error(
|
|
1765
2188
|
"Telegram follower thread deletion was not confirmed; reconnect the leader to retry cleanup.",
|
|
@@ -1857,7 +2280,9 @@ export function createTelegramBusLeaderTargetProvisioner<TContext>(
|
|
|
1857
2280
|
});
|
|
1858
2281
|
deps.recordThreadReconciliationPlan?.(pendingCleanupPlan);
|
|
1859
2282
|
const cleanupPorts = {
|
|
1860
|
-
isCleanupTargetProtected: Threads.createTelegramCleanupTargetProtection(
|
|
2283
|
+
isCleanupTargetProtected: Threads.createTelegramCleanupTargetProtection(
|
|
2284
|
+
deps.topicTargetStore,
|
|
2285
|
+
),
|
|
1861
2286
|
callApi: deps.callApi,
|
|
1862
2287
|
markStaleByTarget: deps.topicTargetStore.markStaleByTarget,
|
|
1863
2288
|
removeCleanupIntentById: deps.topicTargetStore.removePendingCleanup,
|
|
@@ -1977,22 +2402,50 @@ export function createTelegramBusLeaderTargetProvisioner<TContext>(
|
|
|
1977
2402
|
}
|
|
1978
2403
|
|
|
1979
2404
|
/** Strict copy of a follower-supplied attachment source; anything malformed falls back to the plain generated name. */
|
|
1980
|
-
function parseTelegramBusAttachmentSource(
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
2405
|
+
function parseTelegramBusAttachmentSource(
|
|
2406
|
+
value: unknown,
|
|
2407
|
+
): TelegramAttachmentSource | undefined {
|
|
2408
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
2409
|
+
return undefined;
|
|
2410
|
+
const raw = value as Record<string, unknown>,
|
|
2411
|
+
chat = raw.chat as Record<string, unknown> | undefined;
|
|
2412
|
+
const text = (field: unknown) =>
|
|
2413
|
+
typeof field === "string" && field.length > 0 && field.length <= 256;
|
|
2414
|
+
if (
|
|
2415
|
+
!text(raw.kind) ||
|
|
2416
|
+
!Number.isSafeInteger(raw.messageId) ||
|
|
2417
|
+
(raw.index !== undefined && !Number.isSafeInteger(raw.index)) ||
|
|
2418
|
+
(raw.userFileName !== undefined && !text(raw.userFileName)) ||
|
|
2419
|
+
(raw.scope !== undefined && !text(raw.scope)) ||
|
|
2420
|
+
(chat !== undefined &&
|
|
2421
|
+
(!chat ||
|
|
2422
|
+
typeof chat !== "object" ||
|
|
2423
|
+
Array.isArray(chat) ||
|
|
2424
|
+
!Number.isSafeInteger(chat.id) ||
|
|
2425
|
+
(chat.type !== undefined && !text(chat.type)) ||
|
|
2426
|
+
(chat.username !== undefined && !text(chat.username))))
|
|
2427
|
+
)
|
|
2428
|
+
return undefined;
|
|
2429
|
+
return {
|
|
2430
|
+
kind: raw.kind as string,
|
|
2431
|
+
messageId: raw.messageId as number,
|
|
1990
2432
|
...(raw.index !== undefined ? { index: raw.index as number } : {}),
|
|
1991
|
-
...(raw.userFileName !== undefined
|
|
2433
|
+
...(raw.userFileName !== undefined
|
|
2434
|
+
? { userFileName: raw.userFileName as string }
|
|
2435
|
+
: {}),
|
|
1992
2436
|
...(raw.scope !== undefined ? { scope: raw.scope as string } : {}),
|
|
1993
|
-
...(chat
|
|
1994
|
-
|
|
1995
|
-
|
|
2437
|
+
...(chat
|
|
2438
|
+
? {
|
|
2439
|
+
chat: {
|
|
2440
|
+
id: chat.id as number,
|
|
2441
|
+
...(chat.type !== undefined ? { type: chat.type as string } : {}),
|
|
2442
|
+
...(chat.username !== undefined
|
|
2443
|
+
? { username: chat.username as string }
|
|
2444
|
+
: {}),
|
|
2445
|
+
},
|
|
2446
|
+
}
|
|
2447
|
+
: {}),
|
|
2448
|
+
};
|
|
1996
2449
|
}
|
|
1997
2450
|
|
|
1998
2451
|
export function createTelegramBusLeaderApiProxy(
|
|
@@ -2033,7 +2486,11 @@ export function createTelegramBusLeaderApiProxy(
|
|
|
2033
2486
|
if (method === "downloadFile") {
|
|
2034
2487
|
// Follower IPC is a trust boundary: only an exact attachment-source shape reaches the file-name builder.
|
|
2035
2488
|
const source = parseTelegramBusAttachmentSource(args[2]);
|
|
2036
|
-
return deps.downloadFile(
|
|
2489
|
+
return deps.downloadFile(
|
|
2490
|
+
args[0] as string,
|
|
2491
|
+
args[1] as string,
|
|
2492
|
+
...(source ? [source] : []),
|
|
2493
|
+
);
|
|
2037
2494
|
}
|
|
2038
2495
|
throw new Error(`Unsupported Telegram bus API method: ${method}`);
|
|
2039
2496
|
};
|
|
@@ -2066,12 +2523,348 @@ function createTelegramBusFollowerMutationRunner(): TelegramBusFollowerMutationR
|
|
|
2066
2523
|
};
|
|
2067
2524
|
}
|
|
2068
2525
|
|
|
2069
|
-
|
|
2526
|
+
/** One delegated text effect through existing Workspace/API owners; no closure transport, ordinary proxy or replay loop. */
|
|
2527
|
+
export function createTelegramBusSelectedMenuDeliveryHandler(deps: {
|
|
2528
|
+
followerRegistry: TelegramBusFollowerRegistry;
|
|
2529
|
+
protocolIdentity: TelegramBusProtocolIdentity;
|
|
2530
|
+
workspace: {
|
|
2531
|
+
/** Exact active bot/profile namespace, independently captured by the leader; never follower-supplied. */
|
|
2532
|
+
getScopeKey(): string | undefined;
|
|
2533
|
+
captureAuthority(): Threads.TelegramWorkspaceRestoreAuthority | undefined;
|
|
2534
|
+
getStore(): Threads.TelegramWorkspaceRestore | undefined;
|
|
2535
|
+
threadStore: Pick<
|
|
2536
|
+
Threads.TelegramTopicTargetStore,
|
|
2537
|
+
"withWorkspaceLiveRebindSnapshot"
|
|
2538
|
+
>;
|
|
2539
|
+
getJournalBindingKey(follower: TelegramBusFollowerView): string | undefined;
|
|
2540
|
+
run: TelegramBusWorkspaceAdmissionRunner;
|
|
2541
|
+
};
|
|
2542
|
+
api: {
|
|
2543
|
+
runtime: Pick<TelegramBridgeApiRuntime, "call">;
|
|
2544
|
+
authorize(input: {
|
|
2545
|
+
follower: TelegramBusFollowerView;
|
|
2546
|
+
method: string;
|
|
2547
|
+
args: unknown[];
|
|
2548
|
+
}): boolean;
|
|
2549
|
+
/** Synchronous publication acceptance through the existing ownership owner, never delivery/cleanup permission. */
|
|
2550
|
+
record(
|
|
2551
|
+
record: TelegramBusFollowerMessageOwnershipRecord,
|
|
2552
|
+
assertCurrent: () => void,
|
|
2553
|
+
): boolean;
|
|
2554
|
+
};
|
|
2555
|
+
}) {
|
|
2556
|
+
return async (
|
|
2557
|
+
input: TelegramBusSelectedMenuDelivery,
|
|
2558
|
+
isCallerCurrent: () => boolean,
|
|
2559
|
+
): Promise<TelegramBusEnvelope> => {
|
|
2560
|
+
let issued = false,
|
|
2561
|
+
method = "sendMessage";
|
|
2562
|
+
try {
|
|
2563
|
+
const decoded = parseTelegramBusEnvelope(JSON.stringify(input));
|
|
2564
|
+
if (decoded?.kind !== "follower.deliverSelectedMenu")
|
|
2565
|
+
throw new Error("Selected-menu wire is unavailable.");
|
|
2566
|
+
const expected = decoded,
|
|
2567
|
+
{ recipient, executor, effect } = expected;
|
|
2568
|
+
method = effect.kind === "send-text" ? "sendMessage" : "editMessageText";
|
|
2569
|
+
const workspace = deps.workspace,
|
|
2570
|
+
api = deps.api,
|
|
2571
|
+
registry = deps.followerRegistry,
|
|
2572
|
+
protocol = structuredClone(deps.protocolIdentity);
|
|
2573
|
+
const scope = workspace?.getScopeKey,
|
|
2574
|
+
scopeKey = scope?.call(workspace);
|
|
2575
|
+
const capture = workspace?.captureAuthority,
|
|
2576
|
+
getStore = workspace?.getStore,
|
|
2577
|
+
threads = workspace?.threadStore,
|
|
2578
|
+
resolveJournal = workspace?.getJournalBindingKey,
|
|
2579
|
+
run = workspace?.run,
|
|
2580
|
+
runtime = api?.runtime,
|
|
2581
|
+
call = runtime?.call,
|
|
2582
|
+
authorize = api?.authorize,
|
|
2583
|
+
record = api?.record;
|
|
2584
|
+
const authority = capture?.call(workspace),
|
|
2585
|
+
store = getStore?.call(workspace),
|
|
2586
|
+
follower = registry.get(expected.instanceId);
|
|
2587
|
+
if (
|
|
2588
|
+
!scopeKey?.trim() ||
|
|
2589
|
+
!authority ||
|
|
2590
|
+
!isDeepStrictEqual(authority.executor, executor) ||
|
|
2591
|
+
authority.operatorUserId !== expected.operatorUserId ||
|
|
2592
|
+
!store ||
|
|
2593
|
+
typeof threads?.withWorkspaceLiveRebindSnapshot !== "function" ||
|
|
2594
|
+
typeof resolveJournal !== "function" ||
|
|
2595
|
+
typeof run !== "function" ||
|
|
2596
|
+
typeof call !== "function" ||
|
|
2597
|
+
typeof authorize !== "function" ||
|
|
2598
|
+
typeof record !== "function" ||
|
|
2599
|
+
!follower
|
|
2600
|
+
)
|
|
2601
|
+
throw new Error("Selected-menu proof/API owner is unavailable.");
|
|
2602
|
+
const retained = store
|
|
2603
|
+
.listLiveRebindings()
|
|
2604
|
+
.find((value) => value.request.operationId === expected.operationId);
|
|
2605
|
+
if (
|
|
2606
|
+
!retained ||
|
|
2607
|
+
retained.phase !== "released" ||
|
|
2608
|
+
retained.cleanup ||
|
|
2609
|
+
retained.request.source.updateIds.length !== 1 ||
|
|
2610
|
+
retained.recipient.kind !== "follower" ||
|
|
2611
|
+
!isDeepStrictEqual(retained.executor, executor) ||
|
|
2612
|
+
retained.operatorUserId !== expected.operatorUserId ||
|
|
2613
|
+
retained.recipient.instanceId !== expected.instanceId ||
|
|
2614
|
+
retained.recipient.sessionId !== recipient.sessionId ||
|
|
2615
|
+
retained.recipient.generation !== expected.registrationGeneration ||
|
|
2616
|
+
retained.request.owner.profileKey !== recipient.profileKey ||
|
|
2617
|
+
!isDeepStrictEqual(retained.request.target, recipient.target)
|
|
2618
|
+
)
|
|
2619
|
+
throw new Error("Selected-menu operation/recipient is unavailable.");
|
|
2620
|
+
const intent = structuredClone(retained),
|
|
2621
|
+
snapshot = threads.withWorkspaceLiveRebindSnapshot;
|
|
2622
|
+
const registered = structuredClone(follower);
|
|
2623
|
+
const body: Record<string, unknown> = {
|
|
2624
|
+
chat_id: recipient.target.chatId,
|
|
2625
|
+
text: effect.text,
|
|
2626
|
+
...(effect.parseMode ? { parse_mode: effect.parseMode } : {}),
|
|
2627
|
+
...(effect.replyMarkup
|
|
2628
|
+
? { reply_markup: structuredClone(effect.replyMarkup) }
|
|
2629
|
+
: {}),
|
|
2630
|
+
...(effect.kind === "send-text"
|
|
2631
|
+
? {
|
|
2632
|
+
message_thread_id: recipient.target.threadId,
|
|
2633
|
+
...(effect.replyToMessageId
|
|
2634
|
+
? {
|
|
2635
|
+
reply_parameters: {
|
|
2636
|
+
message_id: effect.replyToMessageId,
|
|
2637
|
+
allow_sending_without_reply: true,
|
|
2638
|
+
},
|
|
2639
|
+
}
|
|
2640
|
+
: {}),
|
|
2641
|
+
}
|
|
2642
|
+
: { message_id: effect.messageId }),
|
|
2643
|
+
};
|
|
2644
|
+
const assertRecipientCurrent = () => {
|
|
2645
|
+
const activeAuthority = capture.call(workspace),
|
|
2646
|
+
live = registry.get(expected.instanceId);
|
|
2647
|
+
if (
|
|
2648
|
+
!isCallerCurrent() ||
|
|
2649
|
+
deps.workspace !== workspace ||
|
|
2650
|
+
deps.api !== api ||
|
|
2651
|
+
deps.followerRegistry !== registry ||
|
|
2652
|
+
workspace.getScopeKey !== scope ||
|
|
2653
|
+
scope!.call(workspace) !== scopeKey ||
|
|
2654
|
+
workspace.captureAuthority !== capture ||
|
|
2655
|
+
workspace.getStore !== getStore ||
|
|
2656
|
+
workspace.threadStore !== threads ||
|
|
2657
|
+
workspace.getJournalBindingKey !== resolveJournal ||
|
|
2658
|
+
workspace.run !== run ||
|
|
2659
|
+
threads.withWorkspaceLiveRebindSnapshot !== snapshot ||
|
|
2660
|
+
api.runtime !== runtime ||
|
|
2661
|
+
runtime.call !== call ||
|
|
2662
|
+
api.authorize !== authorize ||
|
|
2663
|
+
api.record !== record ||
|
|
2664
|
+
!authority.isCurrent() ||
|
|
2665
|
+
!activeAuthority?.isCurrent() ||
|
|
2666
|
+
activeAuthority.operatorUserId !== expected.operatorUserId ||
|
|
2667
|
+
!isDeepStrictEqual(activeAuthority.executor, executor) ||
|
|
2668
|
+
getStore.call(workspace) !== store ||
|
|
2669
|
+
!isDeepStrictEqual(deps.protocolIdentity, protocol) ||
|
|
2670
|
+
!hasTelegramBusCapability(
|
|
2671
|
+
protocol,
|
|
2672
|
+
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
2673
|
+
) ||
|
|
2674
|
+
!live ||
|
|
2675
|
+
!getTelegramBusProtocolCompatibility({
|
|
2676
|
+
local: protocol,
|
|
2677
|
+
remote: live.protocol,
|
|
2678
|
+
}).compatible ||
|
|
2679
|
+
!hasTelegramBusCapability(
|
|
2680
|
+
live.protocol,
|
|
2681
|
+
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
2682
|
+
) ||
|
|
2683
|
+
!isDeepStrictEqual(live.protocol, registered.protocol) ||
|
|
2684
|
+
live.registrationGeneration !== expected.registrationGeneration ||
|
|
2685
|
+
live.sessionId !== recipient.sessionId ||
|
|
2686
|
+
live.sessionGeneration !== recipient.sessionGeneration ||
|
|
2687
|
+
live.pid !== recipient.processId ||
|
|
2688
|
+
live.processBirthId !== recipient.processBirthId ||
|
|
2689
|
+
live.profileKey !== recipient.profileKey ||
|
|
2690
|
+
live.cwd !== registered.cwd ||
|
|
2691
|
+
live.slot !== registered.slot ||
|
|
2692
|
+
live.busSocketPath !== registered.busSocketPath ||
|
|
2693
|
+
live.target?.chatId !== recipient.target.chatId ||
|
|
2694
|
+
live.target.threadId !== recipient.target.threadId ||
|
|
2695
|
+
resolveJournal.call(workspace, live) !== recipient.journalBindingKey
|
|
2696
|
+
)
|
|
2697
|
+
throw new Error("Selected-menu authority changed.");
|
|
2698
|
+
let confirmed = false;
|
|
2699
|
+
snapshot.call(threads, intent, (view) => {
|
|
2700
|
+
const bindings = (view.workspaceBindings ?? []).filter(
|
|
2701
|
+
(value) => value.bindingKey === intent.request.binding.bindingKey,
|
|
2702
|
+
);
|
|
2703
|
+
const owners = view.threads.filter(
|
|
2704
|
+
(value) =>
|
|
2705
|
+
value.status === "active" &&
|
|
2706
|
+
(value.slot === intent.request.binding.slot ||
|
|
2707
|
+
isDeepStrictEqual(value.target, recipient.target)),
|
|
2708
|
+
);
|
|
2709
|
+
const binding = bindings[0],
|
|
2710
|
+
owner = owners[0];
|
|
2711
|
+
confirmed =
|
|
2712
|
+
bindings.length === 1 &&
|
|
2713
|
+
binding?.sessionId === recipient.sessionId &&
|
|
2714
|
+
binding.cwd === intent.request.binding.cwd &&
|
|
2715
|
+
binding.workspaceKey === intent.request.binding.workspaceKey &&
|
|
2716
|
+
binding.slot === live.slot &&
|
|
2717
|
+
binding.inactiveSinceMs === undefined &&
|
|
2718
|
+
isDeepStrictEqual(binding.target, recipient.target) &&
|
|
2719
|
+
owners.length === 1 &&
|
|
2720
|
+
owner?.instanceId === expected.instanceId &&
|
|
2721
|
+
owner.slot === binding.slot &&
|
|
2722
|
+
owner.profileKey === recipient.profileKey &&
|
|
2723
|
+
isDeepStrictEqual(owner.owner, intent.request.owner.owner) &&
|
|
2724
|
+
isDeepStrictEqual(owner.target, recipient.target) &&
|
|
2725
|
+
!!live.cwd &&
|
|
2726
|
+
WorkspaceIdentity.normalizeTelegramWorkspacePath(live.cwd) ===
|
|
2727
|
+
binding.cwd;
|
|
2728
|
+
});
|
|
2729
|
+
if (!confirmed || !isCallerCurrent() || !authority.isCurrent())
|
|
2730
|
+
throw new Error("Selected-menu canonical recipient is unavailable.");
|
|
2731
|
+
};
|
|
2732
|
+
const assertCurrent = () => {
|
|
2733
|
+
assertRecipientCurrent();
|
|
2734
|
+
const live = registry.get(expected.instanceId);
|
|
2735
|
+
if (
|
|
2736
|
+
!live ||
|
|
2737
|
+
authorize.call(api, {
|
|
2738
|
+
follower: structuredClone(live),
|
|
2739
|
+
method: "call",
|
|
2740
|
+
args: [method, structuredClone(body)],
|
|
2741
|
+
}) !== true
|
|
2742
|
+
)
|
|
2743
|
+
throw new Error("Selected-menu API recipient is unavailable.");
|
|
2744
|
+
assertRecipientCurrent();
|
|
2745
|
+
};
|
|
2746
|
+
assertCurrent();
|
|
2747
|
+
const result = await run(
|
|
2748
|
+
{
|
|
2749
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
2750
|
+
operationKind: "workspace.selected-menu-delivery",
|
|
2751
|
+
scopes: [{ kind: "profile" }],
|
|
2752
|
+
},
|
|
2753
|
+
async () => {
|
|
2754
|
+
assertCurrent();
|
|
2755
|
+
issued = true;
|
|
2756
|
+
const result = await call.call(runtime, method, body, {
|
|
2757
|
+
assertAuthority: assertCurrent,
|
|
2758
|
+
maxAttempts: 1,
|
|
2759
|
+
retryRateLimit: false,
|
|
2760
|
+
retrySafety: "non-idempotent",
|
|
2761
|
+
});
|
|
2762
|
+
assertCurrent();
|
|
2763
|
+
const value =
|
|
2764
|
+
result && typeof result === "object" && !Array.isArray(result)
|
|
2765
|
+
? (result as Record<string, unknown>)
|
|
2766
|
+
: undefined;
|
|
2767
|
+
const messageId =
|
|
2768
|
+
effect.kind === "edit-text" ? effect.messageId : value?.message_id;
|
|
2769
|
+
if (
|
|
2770
|
+
!Number.isSafeInteger(messageId) ||
|
|
2771
|
+
(messageId as number) <= 0 ||
|
|
2772
|
+
(effect.kind === "edit-text" &&
|
|
2773
|
+
result !== true &&
|
|
2774
|
+
value?.message_id !== messageId)
|
|
2775
|
+
)
|
|
2776
|
+
throw new Error("Selected-menu API result is unconfirmed.");
|
|
2777
|
+
if (
|
|
2778
|
+
effect.kind === "send-text" &&
|
|
2779
|
+
record.call(
|
|
2780
|
+
api,
|
|
2781
|
+
{
|
|
2782
|
+
follower: structuredClone(registered),
|
|
2783
|
+
chatId: recipient.target.chatId,
|
|
2784
|
+
messageId: messageId as number,
|
|
2785
|
+
target: { ...recipient.target },
|
|
2786
|
+
},
|
|
2787
|
+
assertCurrent,
|
|
2788
|
+
) !== true
|
|
2789
|
+
)
|
|
2790
|
+
throw new Error(
|
|
2791
|
+
"Selected-menu ownership publication is unconfirmed.",
|
|
2792
|
+
);
|
|
2793
|
+
assertCurrent();
|
|
2794
|
+
return {
|
|
2795
|
+
operationId: expected.operationId,
|
|
2796
|
+
recipient: structuredClone(recipient),
|
|
2797
|
+
registrationGeneration: expected.registrationGeneration,
|
|
2798
|
+
effect: effect.kind,
|
|
2799
|
+
messageId,
|
|
2800
|
+
};
|
|
2801
|
+
},
|
|
2802
|
+
);
|
|
2803
|
+
assertCurrent();
|
|
2804
|
+
return {
|
|
2805
|
+
kind: "bus.ack",
|
|
2806
|
+
requestId: expected.requestId,
|
|
2807
|
+
ok: true,
|
|
2808
|
+
result,
|
|
2809
|
+
};
|
|
2810
|
+
} catch (error) {
|
|
2811
|
+
const unknown =
|
|
2812
|
+
issued &&
|
|
2813
|
+
!(error instanceof TelegramApiAuthorityError && !error.requestIssued);
|
|
2814
|
+
return {
|
|
2815
|
+
kind: "bus.ack",
|
|
2816
|
+
requestId: input.requestId,
|
|
2817
|
+
ok: false,
|
|
2818
|
+
message: "Selected-menu delivery is refused or unconfirmed.",
|
|
2819
|
+
...(unknown
|
|
2820
|
+
? { error: { code: "commit-unknown" as const, method } }
|
|
2821
|
+
: {}),
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
/** The current registration a follower request names, or the negative ack refusing an unknown or stale one. */
|
|
2828
|
+
function resolveRegisteredTelegramBusFollower(
|
|
2829
|
+
registry: Pick<TelegramBusFollowerRegistry, "get">,
|
|
2830
|
+
requestId: string,
|
|
2831
|
+
instanceId: string,
|
|
2832
|
+
registrationGeneration: string | undefined,
|
|
2833
|
+
):
|
|
2834
|
+
| { follower: TelegramBusFollowerView }
|
|
2835
|
+
| { rejection: Extract<TelegramBusEnvelope, { kind: "bus.ack" }> } {
|
|
2836
|
+
const follower = registry.get(instanceId);
|
|
2837
|
+
if (!follower) {
|
|
2838
|
+
return {
|
|
2839
|
+
rejection: rejectTelegramBusRequest(
|
|
2840
|
+
requestId,
|
|
2841
|
+
"Unknown Telegram bus follower instance.",
|
|
2842
|
+
),
|
|
2843
|
+
};
|
|
2844
|
+
}
|
|
2845
|
+
if (
|
|
2846
|
+
!follower.registrationGeneration ||
|
|
2847
|
+
registrationGeneration !== follower.registrationGeneration
|
|
2848
|
+
) {
|
|
2849
|
+
return {
|
|
2850
|
+
rejection: rejectTelegramBusRequest(
|
|
2851
|
+
requestId,
|
|
2852
|
+
"Stale Telegram bus follower registration generation.",
|
|
2853
|
+
),
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
return { follower };
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
export interface TelegramBusLeaderEnvelopeHandlerDeps {
|
|
2070
2860
|
followerRegistry: TelegramBusFollowerRegistry;
|
|
2071
2861
|
authSecret?: string;
|
|
2072
2862
|
protocolIdentity: TelegramBusProtocolIdentity;
|
|
2073
2863
|
getNowMs?: () => number;
|
|
2074
2864
|
timeoutMs?: number;
|
|
2865
|
+
selectedMenuDelivery?: ReturnType<
|
|
2866
|
+
typeof createTelegramBusSelectedMenuDeliveryHandler
|
|
2867
|
+
>;
|
|
2075
2868
|
callApi?: (method: string, args: unknown[]) => Promise<unknown> | unknown;
|
|
2076
2869
|
authorizeFollowerApiCall?: (input: {
|
|
2077
2870
|
follower: TelegramBusFollowerView;
|
|
@@ -2105,8 +2898,7 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
2105
2898
|
options?: { existingWorkspaceBindingOnly?: boolean },
|
|
2106
2899
|
) =>
|
|
2107
2900
|
| Promise<
|
|
2108
|
-
|
|
2109
|
-
| undefined
|
|
2901
|
+
(TelegramTarget & { slot?: string; threadName?: string }) | undefined
|
|
2110
2902
|
>
|
|
2111
2903
|
| (TelegramTarget & { slot?: string; threadName?: string })
|
|
2112
2904
|
| undefined;
|
|
@@ -2123,12 +2915,26 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
2123
2915
|
) => Promise<{ threadName: string }> | { threadName: string };
|
|
2124
2916
|
publishFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
|
|
2125
2917
|
settleFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
|
|
2126
|
-
getFollowerDisplayTitle?: (
|
|
2918
|
+
getFollowerDisplayTitle?: (
|
|
2919
|
+
follower: TelegramBusFollowerView,
|
|
2920
|
+
) => string | undefined;
|
|
2127
2921
|
onFollowerRegistered?: () => void;
|
|
2128
|
-
|
|
2922
|
+
/** Observation only: reacquire admission and inspect work, never infer completion.
|
|
2923
|
+
* Listener settlement ends its currentness fence; return asynchronous work to retain that fence. */
|
|
2924
|
+
onWorkspaceRestoreRecipientObserved?: (
|
|
2925
|
+
follower: TelegramBusFollowerView,
|
|
2926
|
+
isCurrent: () => boolean,
|
|
2927
|
+
) => Promise<void> | void;
|
|
2129
2928
|
getWorkspaceRestoreObservationGeneration?: () => number | undefined;
|
|
2130
|
-
recordRuntimeEvent?:
|
|
2131
|
-
|
|
2929
|
+
recordRuntimeEvent?: (
|
|
2930
|
+
category: string,
|
|
2931
|
+
error: unknown,
|
|
2932
|
+
details?: Record<string, unknown>,
|
|
2933
|
+
) => void;
|
|
2934
|
+
applyThreadDisplayMode?: (
|
|
2935
|
+
mode: TelegramThreadDisplayMode,
|
|
2936
|
+
isCurrent: () => boolean,
|
|
2937
|
+
) => Promise<void>;
|
|
2132
2938
|
getThreadDisplayMode?: () => TelegramThreadDisplayMode;
|
|
2133
2939
|
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
2134
2940
|
getTelegramProfile?: () => string | undefined;
|
|
@@ -2136,83 +2942,227 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
2136
2942
|
runFollowerMutation?: TelegramBusFollowerMutationRunner;
|
|
2137
2943
|
runWorkspaceAdmission?: TelegramBusWorkspaceAdmissionRunner;
|
|
2138
2944
|
runWithWorkspaceCapacity?: TelegramWorkspaceCapacityRunner;
|
|
2139
|
-
}
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
export function createTelegramBusLeaderEnvelopeHandler(
|
|
2948
|
+
deps: TelegramBusLeaderEnvelopeHandlerDeps,
|
|
2949
|
+
): (
|
|
2140
2950
|
envelope: TelegramBusEnvelope,
|
|
2141
2951
|
) => Promise<TelegramBusEnvelope> | TelegramBusEnvelope {
|
|
2142
2952
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
2143
2953
|
const runFollowerMutation =
|
|
2144
2954
|
deps.runFollowerMutation ?? createTelegramBusFollowerMutationRunner();
|
|
2145
|
-
const recipientObservations = new Map<
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2955
|
+
const recipientObservations = new Map<
|
|
2956
|
+
string,
|
|
2957
|
+
{
|
|
2958
|
+
epoch: number | string;
|
|
2959
|
+
operator: number;
|
|
2960
|
+
profile: string | undefined;
|
|
2961
|
+
runtimeGeneration: number | undefined;
|
|
2962
|
+
follower: Omit<
|
|
2963
|
+
TelegramBusFollowerView,
|
|
2964
|
+
"lastHeartbeatMs" | "connectedAtMs" | "threadName"
|
|
2965
|
+
>;
|
|
2966
|
+
}
|
|
2967
|
+
>();
|
|
2968
|
+
/** Rename and reset share one generation- and target-fenced Workspace Thread name change. */
|
|
2969
|
+
const changeFollowerThreadName = (
|
|
2970
|
+
envelope: Extract<
|
|
2971
|
+
TelegramBusEnvelope,
|
|
2972
|
+
{ kind: "follower.renameThread" | "follower.resetThreadName" }
|
|
2973
|
+
>,
|
|
2974
|
+
action: "rename" | "reset",
|
|
2975
|
+
apply:
|
|
2976
|
+
| ((
|
|
2977
|
+
follower: TelegramBusFollowerView,
|
|
2978
|
+
) => Promise<{ threadName: string }> | { threadName: string })
|
|
2979
|
+
| undefined,
|
|
2980
|
+
): Promise<TelegramBusEnvelope> =>
|
|
2981
|
+
runFollowerMutation(
|
|
2982
|
+
deps.followerRegistry.get(envelope.instanceId) ?? {
|
|
2983
|
+
instanceId: envelope.instanceId,
|
|
2984
|
+
},
|
|
2985
|
+
async (): Promise<TelegramBusEnvelope> => {
|
|
2986
|
+
const resolved = resolveRegisteredTelegramBusFollower(
|
|
2987
|
+
deps.followerRegistry,
|
|
2988
|
+
envelope.requestId,
|
|
2989
|
+
envelope.instanceId,
|
|
2990
|
+
envelope.registrationGeneration,
|
|
2991
|
+
);
|
|
2992
|
+
if ("rejection" in resolved) return resolved.rejection;
|
|
2993
|
+
const follower = resolved.follower;
|
|
2994
|
+
if (
|
|
2995
|
+
follower.target?.chatId !== envelope.target.chatId ||
|
|
2996
|
+
follower.target?.threadId !== envelope.target.threadId
|
|
2997
|
+
)
|
|
2998
|
+
return rejectTelegramBusRequest(
|
|
2999
|
+
envelope.requestId,
|
|
3000
|
+
"Stale Telegram bus follower registration generation.",
|
|
3001
|
+
);
|
|
3002
|
+
if (
|
|
3003
|
+
!hasTelegramBusCapability(
|
|
3004
|
+
deps.protocolIdentity,
|
|
3005
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
3006
|
+
) ||
|
|
3007
|
+
!hasTelegramBusCapability(
|
|
3008
|
+
follower.protocol,
|
|
3009
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
3010
|
+
) ||
|
|
3011
|
+
!apply
|
|
3012
|
+
)
|
|
3013
|
+
return rejectTelegramBusRequest(
|
|
3014
|
+
envelope.requestId,
|
|
3015
|
+
`Telegram Workspace Thread ${action} is unavailable.`,
|
|
3016
|
+
);
|
|
3017
|
+
try {
|
|
3018
|
+
const result = await apply(follower);
|
|
3019
|
+
const current = deps.followerRegistry.get(follower.instanceId);
|
|
3020
|
+
if (
|
|
3021
|
+
!current ||
|
|
3022
|
+
current.registrationGeneration !== follower.registrationGeneration
|
|
3023
|
+
)
|
|
3024
|
+
return rejectTelegramBusRequest(
|
|
3025
|
+
envelope.requestId,
|
|
3026
|
+
"Stale Telegram bus follower registration generation.",
|
|
3027
|
+
);
|
|
3028
|
+
deps.followerRegistry.register({
|
|
3029
|
+
...current,
|
|
3030
|
+
threadName: result.threadName,
|
|
3031
|
+
connectedAtMs: current.connectedAtMs,
|
|
3032
|
+
});
|
|
3033
|
+
return {
|
|
3034
|
+
kind: "bus.ack",
|
|
3035
|
+
requestId: envelope.requestId,
|
|
3036
|
+
ok: true,
|
|
3037
|
+
result,
|
|
3038
|
+
};
|
|
3039
|
+
} catch (error) {
|
|
3040
|
+
return rejectTelegramBusRequest(
|
|
3041
|
+
envelope.requestId,
|
|
3042
|
+
error instanceof Error
|
|
3043
|
+
? error.message
|
|
3044
|
+
: `Telegram Workspace Thread ${action} failed.`,
|
|
3045
|
+
);
|
|
3046
|
+
}
|
|
3047
|
+
},
|
|
3048
|
+
);
|
|
2149
3049
|
const observeRestoreRecipient = (follower: TelegramBusFollowerView): void => {
|
|
2150
|
-
if (
|
|
2151
|
-
|
|
2152
|
-
|
|
3050
|
+
if (
|
|
3051
|
+
!deps.authSecret ||
|
|
3052
|
+
!deps.onWorkspaceRestoreRecipientObserved ||
|
|
3053
|
+
!deps.getTelegramProfile ||
|
|
3054
|
+
!hasTelegramBusCapability(
|
|
3055
|
+
deps.protocolIdentity,
|
|
3056
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
|
|
3057
|
+
) ||
|
|
3058
|
+
!hasTelegramBusCapability(
|
|
3059
|
+
follower.protocol,
|
|
3060
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
|
|
3061
|
+
)
|
|
3062
|
+
)
|
|
3063
|
+
return;
|
|
2153
3064
|
const instanceId = follower.instanceId;
|
|
2154
3065
|
const report = (error: unknown): void => {
|
|
2155
|
-
try {
|
|
2156
|
-
|
|
3066
|
+
try {
|
|
3067
|
+
deps.recordRuntimeEvent?.("bus", error, {
|
|
3068
|
+
phase: "workspace-restore-recipient-observation",
|
|
3069
|
+
instanceId,
|
|
3070
|
+
});
|
|
3071
|
+
} catch {
|
|
3072
|
+
/* Optional observation diagnostics must not break transport liveness. */
|
|
3073
|
+
}
|
|
2157
3074
|
};
|
|
2158
3075
|
try {
|
|
2159
|
-
const epoch = deps.getCurrentLeaderEpoch?.(),
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
3076
|
+
const epoch = deps.getCurrentLeaderEpoch?.(),
|
|
3077
|
+
operator = deps.getAllowedUserId?.();
|
|
3078
|
+
const runtimeGeneration =
|
|
3079
|
+
deps.getWorkspaceRestoreObservationGeneration?.();
|
|
3080
|
+
if (
|
|
3081
|
+
epoch === undefined ||
|
|
3082
|
+
operator === undefined ||
|
|
3083
|
+
(deps.getWorkspaceRestoreObservationGeneration &&
|
|
3084
|
+
runtimeGeneration === undefined)
|
|
3085
|
+
)
|
|
3086
|
+
return;
|
|
2163
3087
|
const snapshot = structuredClone(follower);
|
|
2164
|
-
const observed = {
|
|
2165
|
-
|
|
2166
|
-
|
|
3088
|
+
const observed = {
|
|
3089
|
+
epoch,
|
|
3090
|
+
operator,
|
|
3091
|
+
profile: deps.getTelegramProfile(),
|
|
3092
|
+
runtimeGeneration,
|
|
3093
|
+
follower: {
|
|
3094
|
+
...snapshot,
|
|
3095
|
+
lastHeartbeatMs: undefined,
|
|
3096
|
+
connectedAtMs: undefined,
|
|
3097
|
+
threadName: undefined,
|
|
3098
|
+
},
|
|
3099
|
+
};
|
|
3100
|
+
if (isDeepStrictEqual(recipientObservations.get(instanceId), observed))
|
|
3101
|
+
return;
|
|
2167
3102
|
recipientObservations.set(instanceId, observed);
|
|
2168
3103
|
const isCurrent = (): boolean => {
|
|
2169
3104
|
const live = deps.followerRegistry.get(instanceId);
|
|
2170
|
-
return
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
3105
|
+
return (
|
|
3106
|
+
recipientObservations.get(instanceId) === observed &&
|
|
3107
|
+
deps.getWorkspaceRestoreObservationGeneration?.() ===
|
|
3108
|
+
observed.runtimeGeneration &&
|
|
3109
|
+
hasTelegramBusCapability(
|
|
3110
|
+
deps.protocolIdentity,
|
|
3111
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
|
|
3112
|
+
) &&
|
|
3113
|
+
deps.getCurrentLeaderEpoch?.() === observed.epoch &&
|
|
3114
|
+
deps.getAllowedUserId?.() === observed.operator &&
|
|
3115
|
+
deps.getTelegramProfile?.() === observed.profile &&
|
|
3116
|
+
!!live &&
|
|
3117
|
+
hasTelegramBusCapability(
|
|
3118
|
+
live.protocol,
|
|
3119
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
|
|
3120
|
+
) &&
|
|
3121
|
+
isDeepStrictEqual(
|
|
3122
|
+
{
|
|
3123
|
+
...live,
|
|
3124
|
+
lastHeartbeatMs: undefined,
|
|
3125
|
+
connectedAtMs: undefined,
|
|
3126
|
+
threadName: undefined,
|
|
3127
|
+
},
|
|
3128
|
+
observed.follower,
|
|
3129
|
+
)
|
|
3130
|
+
);
|
|
2177
3131
|
};
|
|
2178
3132
|
// A heartbeat is a hint, not completion proof. Never await the listener before acknowledging it.
|
|
2179
|
-
void Promise.resolve()
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
3133
|
+
void Promise.resolve()
|
|
3134
|
+
.then(() => {
|
|
3135
|
+
if (isCurrent())
|
|
3136
|
+
return deps.onWorkspaceRestoreRecipientObserved?.(
|
|
3137
|
+
structuredClone(snapshot),
|
|
3138
|
+
isCurrent,
|
|
3139
|
+
);
|
|
3140
|
+
})
|
|
3141
|
+
.catch(report)
|
|
3142
|
+
.finally(() => {
|
|
3143
|
+
if (recipientObservations.get(instanceId) === observed)
|
|
3144
|
+
recipientObservations.delete(instanceId);
|
|
3145
|
+
});
|
|
3146
|
+
} catch (error) {
|
|
3147
|
+
report(error);
|
|
3148
|
+
}
|
|
2185
3149
|
};
|
|
2186
3150
|
const handleAgentRequest = async (
|
|
2187
3151
|
envelope: Extract<
|
|
2188
3152
|
TelegramBusEnvelope,
|
|
2189
3153
|
{
|
|
2190
|
-
kind:
|
|
2191
|
-
| "follower.resolveAgentTarget"
|
|
2192
|
-
| "follower.routeAgentMessage";
|
|
3154
|
+
kind: "follower.resolveAgentTarget" | "follower.routeAgentMessage";
|
|
2193
3155
|
}
|
|
2194
3156
|
>,
|
|
2195
3157
|
): Promise<TelegramBusEnvelope> => {
|
|
2196
|
-
const
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
}
|
|
2205
|
-
if (
|
|
2206
|
-
!follower.registrationGeneration ||
|
|
2207
|
-
envelope.registrationGeneration !== follower.registrationGeneration
|
|
2208
|
-
) {
|
|
2209
|
-
return {
|
|
2210
|
-
kind: "bus.ack",
|
|
2211
|
-
requestId: envelope.requestId,
|
|
2212
|
-
ok: false,
|
|
2213
|
-
message: "Stale Telegram bus follower registration generation.",
|
|
2214
|
-
};
|
|
2215
|
-
}
|
|
3158
|
+
const followerResolution = resolveRegisteredTelegramBusFollower(
|
|
3159
|
+
deps.followerRegistry,
|
|
3160
|
+
envelope.requestId,
|
|
3161
|
+
envelope.instanceId,
|
|
3162
|
+
envelope.registrationGeneration,
|
|
3163
|
+
);
|
|
3164
|
+
if ("rejection" in followerResolution) return followerResolution.rejection;
|
|
3165
|
+
const follower = followerResolution.follower;
|
|
2216
3166
|
deps.followerRegistry.heartbeat(envelope.instanceId, getNowMs());
|
|
2217
3167
|
if (envelope.kind === "follower.resolveAgentTarget") {
|
|
2218
3168
|
const target = await deps.resolveAgentTarget?.(
|
|
@@ -2226,20 +3176,16 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
2226
3176
|
ok: true,
|
|
2227
3177
|
result: target,
|
|
2228
3178
|
}
|
|
2229
|
-
:
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
message: "Telegram agent target is unavailable or ambiguous.",
|
|
2234
|
-
};
|
|
3179
|
+
: rejectTelegramBusRequest(
|
|
3180
|
+
envelope.requestId,
|
|
3181
|
+
"Telegram agent target is unavailable or ambiguous.",
|
|
3182
|
+
);
|
|
2235
3183
|
}
|
|
2236
3184
|
if (!deps.routeAgentMessage) {
|
|
2237
|
-
return
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
message: "Telegram agent message routing is unavailable.",
|
|
2242
|
-
};
|
|
3185
|
+
return rejectTelegramBusRequest(
|
|
3186
|
+
envelope.requestId,
|
|
3187
|
+
"Telegram agent message routing is unavailable.",
|
|
3188
|
+
);
|
|
2243
3189
|
}
|
|
2244
3190
|
await deps.routeAgentMessage(follower, envelope.message);
|
|
2245
3191
|
return { kind: "bus.ack", requestId: envelope.requestId, ok: true };
|
|
@@ -2250,26 +3196,14 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
2250
3196
|
{ kind: "follower.offerQueueHandoff" }
|
|
2251
3197
|
>,
|
|
2252
3198
|
): Promise<TelegramBusEnvelope> => {
|
|
2253
|
-
const
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
}
|
|
2262
|
-
if (
|
|
2263
|
-
!donor.registrationGeneration ||
|
|
2264
|
-
envelope.registrationGeneration !== donor.registrationGeneration
|
|
2265
|
-
) {
|
|
2266
|
-
return {
|
|
2267
|
-
kind: "bus.ack",
|
|
2268
|
-
requestId: envelope.requestId,
|
|
2269
|
-
ok: false,
|
|
2270
|
-
message: "Stale Telegram bus follower registration generation.",
|
|
2271
|
-
};
|
|
2272
|
-
}
|
|
3199
|
+
const donorResolution = resolveRegisteredTelegramBusFollower(
|
|
3200
|
+
deps.followerRegistry,
|
|
3201
|
+
envelope.requestId,
|
|
3202
|
+
envelope.instanceId,
|
|
3203
|
+
envelope.registrationGeneration,
|
|
3204
|
+
);
|
|
3205
|
+
if ("rejection" in donorResolution) return donorResolution.rejection;
|
|
3206
|
+
const donor = donorResolution.follower;
|
|
2273
3207
|
const recipient = deps.followerRegistry.get(envelope.recipientInstanceId);
|
|
2274
3208
|
if (
|
|
2275
3209
|
!hasTelegramBusCapability(
|
|
@@ -2285,32 +3219,26 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
2285
3219
|
TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF,
|
|
2286
3220
|
)
|
|
2287
3221
|
) {
|
|
2288
|
-
return
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
message: "Telegram queue handoff capability was not negotiated.",
|
|
2293
|
-
};
|
|
3222
|
+
return rejectTelegramBusRequest(
|
|
3223
|
+
envelope.requestId,
|
|
3224
|
+
"Telegram queue handoff capability was not negotiated.",
|
|
3225
|
+
);
|
|
2294
3226
|
}
|
|
2295
3227
|
if (
|
|
2296
3228
|
!recipient?.registrationGeneration ||
|
|
2297
3229
|
envelope.recipientRegistrationGeneration !==
|
|
2298
3230
|
recipient.registrationGeneration
|
|
2299
3231
|
) {
|
|
2300
|
-
return
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
message: "Stale Telegram queue handoff recipient registration generation.",
|
|
2305
|
-
};
|
|
3232
|
+
return rejectTelegramBusRequest(
|
|
3233
|
+
envelope.requestId,
|
|
3234
|
+
"Stale Telegram queue handoff recipient registration generation.",
|
|
3235
|
+
);
|
|
2306
3236
|
}
|
|
2307
3237
|
if (donor.instanceId === recipient.instanceId) {
|
|
2308
|
-
return
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
message: "Telegram queue handoff recipient must be another runtime.",
|
|
2313
|
-
};
|
|
3238
|
+
return rejectTelegramBusRequest(
|
|
3239
|
+
envelope.requestId,
|
|
3240
|
+
"Telegram queue handoff recipient must be another runtime.",
|
|
3241
|
+
);
|
|
2314
3242
|
}
|
|
2315
3243
|
if (deps.routeQueueHandoff) {
|
|
2316
3244
|
const result = await deps.routeQueueHandoff(donor, envelope);
|
|
@@ -2380,27 +3308,14 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
2380
3308
|
}
|
|
2381
3309
|
>,
|
|
2382
3310
|
): Promise<TelegramBusEnvelope> => {
|
|
2383
|
-
const
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
}
|
|
2392
|
-
if (
|
|
2393
|
-
!follower.registrationGeneration ||
|
|
2394
|
-
envelope.recipientRegistrationGeneration !==
|
|
2395
|
-
follower.registrationGeneration
|
|
2396
|
-
) {
|
|
2397
|
-
return {
|
|
2398
|
-
kind: "bus.ack",
|
|
2399
|
-
requestId: envelope.requestId,
|
|
2400
|
-
ok: false,
|
|
2401
|
-
message: "Stale Telegram bus follower registration generation.",
|
|
2402
|
-
};
|
|
2403
|
-
}
|
|
3311
|
+
const followerResolution = resolveRegisteredTelegramBusFollower(
|
|
3312
|
+
deps.followerRegistry,
|
|
3313
|
+
envelope.requestId,
|
|
3314
|
+
envelope.recipientInstanceId,
|
|
3315
|
+
envelope.recipientRegistrationGeneration,
|
|
3316
|
+
);
|
|
3317
|
+
if ("rejection" in followerResolution) return followerResolution.rejection;
|
|
3318
|
+
const follower = followerResolution.follower;
|
|
2404
3319
|
const followerSocketPath =
|
|
2405
3320
|
follower.busSocketPath ??
|
|
2406
3321
|
getTelegramBusFollowerSocketPath(envelope.recipientInstanceId);
|
|
@@ -2447,12 +3362,10 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
2447
3362
|
return async (envelope) => {
|
|
2448
3363
|
const trafficClass = getTelegramBusEnvelopeTrafficClass(envelope);
|
|
2449
3364
|
if (trafficClass === "response") {
|
|
2450
|
-
return
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
message: "Telegram bus response envelope cannot be used as a request.",
|
|
2455
|
-
};
|
|
3365
|
+
return rejectTelegramBusRequest(
|
|
3366
|
+
envelope.requestId,
|
|
3367
|
+
"Telegram bus response envelope cannot be used as a request.",
|
|
3368
|
+
);
|
|
2456
3369
|
}
|
|
2457
3370
|
if (!isTelegramBusEnvelopeAuthorized(envelope, deps.authSecret)) {
|
|
2458
3371
|
return createUnauthorizedBusAck(envelope.requestId);
|
|
@@ -2465,10 +3378,15 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
2465
3378
|
remote: envelope.registration.protocol,
|
|
2466
3379
|
});
|
|
2467
3380
|
const displayCompatible = () =>
|
|
2468
|
-
(deps.getThreadDisplayMode?.() ?? "names") === "names" ||
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
3381
|
+
(deps.getThreadDisplayMode?.() ?? "names") === "names" ||
|
|
3382
|
+
(hasTelegramBusCapability(
|
|
3383
|
+
deps.protocolIdentity,
|
|
3384
|
+
TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
|
|
3385
|
+
) &&
|
|
3386
|
+
hasTelegramBusCapability(
|
|
3387
|
+
envelope.registration.protocol,
|
|
3388
|
+
TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
|
|
3389
|
+
));
|
|
2472
3390
|
const restoringWorkspace =
|
|
2473
3391
|
envelope.kind === "follower.restoreWorkspace";
|
|
2474
3392
|
const supportsWorkspaceAutoConnect =
|
|
@@ -2481,7 +3399,8 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
2481
3399
|
TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT,
|
|
2482
3400
|
);
|
|
2483
3401
|
if (
|
|
2484
|
-
!compatibility.compatible ||
|
|
3402
|
+
!compatibility.compatible ||
|
|
3403
|
+
!displayCompatible() ||
|
|
2485
3404
|
(envelope.registration.cwd !== undefined &&
|
|
2486
3405
|
envelope.registration.sessionId === undefined) ||
|
|
2487
3406
|
(restoringWorkspace && !supportsWorkspaceAutoConnect)
|
|
@@ -2494,133 +3413,211 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
2494
3413
|
error: { code: "incompatible-protocol" as const },
|
|
2495
3414
|
message: `Incompatible Telegram bus protocol: ${
|
|
2496
3415
|
compatibility.reason ??
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
3416
|
+
(envelope.registration.cwd !== undefined &&
|
|
3417
|
+
envelope.registration.sessionId === undefined
|
|
3418
|
+
? "missing-session-identity"
|
|
3419
|
+
: "missing-capability")
|
|
2501
3420
|
}.`,
|
|
2502
3421
|
};
|
|
2503
3422
|
}
|
|
2504
3423
|
if (!envelope.registration.registrationGeneration) {
|
|
2505
3424
|
return {
|
|
2506
|
-
kind: "bus.ack",
|
|
3425
|
+
kind: "bus.ack",
|
|
3426
|
+
requestId: envelope.requestId,
|
|
3427
|
+
ok: false,
|
|
2507
3428
|
protocol: deps.protocolIdentity,
|
|
2508
|
-
message:
|
|
3429
|
+
message:
|
|
3430
|
+
"Telegram follower registration requires an exact generation.",
|
|
2509
3431
|
};
|
|
2510
3432
|
}
|
|
2511
3433
|
const registrationOperation = () =>
|
|
2512
3434
|
runFollowerMutation(envelope.registration, async () => {
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
3435
|
+
try {
|
|
3436
|
+
const leaderEpoch = deps.getCurrentLeaderEpoch?.();
|
|
3437
|
+
const telegramProfile = deps.getTelegramProfile?.();
|
|
3438
|
+
const operatorUserId = deps.getAllowedUserId?.();
|
|
3439
|
+
const registrationCurrent = (): boolean =>
|
|
3440
|
+
(!deps.getCurrentLeaderEpoch ||
|
|
3441
|
+
deps.getCurrentLeaderEpoch() === leaderEpoch) &&
|
|
3442
|
+
(!deps.getTelegramProfile ||
|
|
3443
|
+
deps.getTelegramProfile() === telegramProfile) &&
|
|
3444
|
+
(!deps.getAllowedUserId ||
|
|
3445
|
+
deps.getAllowedUserId() === operatorUserId);
|
|
3446
|
+
if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
|
|
3447
|
+
throw new Error(
|
|
3448
|
+
"Telegram follower registration requires leader ownership.",
|
|
3449
|
+
);
|
|
3450
|
+
}
|
|
3451
|
+
if (
|
|
3452
|
+
hasTelegramBusCapability(
|
|
3453
|
+
deps.protocolIdentity,
|
|
3454
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
|
|
3455
|
+
) &&
|
|
3456
|
+
!deps.commitFollowerRegistration
|
|
3457
|
+
)
|
|
3458
|
+
throw new Error(
|
|
3459
|
+
"Workspace Restore registration publication protection is unavailable.",
|
|
3460
|
+
);
|
|
3461
|
+
const target = await deps.provisionFollowerTarget?.(
|
|
3462
|
+
envelope.registration,
|
|
3463
|
+
{ existingWorkspaceBindingOnly: restoringWorkspace },
|
|
2524
3464
|
);
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
3465
|
+
if (restoringWorkspace && !target) {
|
|
3466
|
+
return {
|
|
3467
|
+
kind: "bus.ack" as const,
|
|
3468
|
+
requestId: envelope.requestId,
|
|
3469
|
+
ok: false,
|
|
3470
|
+
protocol: deps.protocolIdentity,
|
|
3471
|
+
error: { code: "workspace-binding-unavailable" as const },
|
|
3472
|
+
message:
|
|
3473
|
+
"No remembered Telegram Workspace Thread is available.",
|
|
3474
|
+
};
|
|
3475
|
+
}
|
|
3476
|
+
if (!registrationCurrent())
|
|
3477
|
+
throw new Error(
|
|
3478
|
+
"Telegram follower registration lost leader ownership.",
|
|
3479
|
+
);
|
|
3480
|
+
if (!displayCompatible()) {
|
|
3481
|
+
throw new Error(
|
|
3482
|
+
"Thread display mode changed; update or restart this follower before connecting.",
|
|
3483
|
+
);
|
|
3484
|
+
}
|
|
3485
|
+
const registeredTarget = target ?? envelope.registration.target;
|
|
3486
|
+
const registeredSlot = target?.slot ?? envelope.registration.slot;
|
|
3487
|
+
if (
|
|
3488
|
+
deps.provisionFollowerTarget &&
|
|
3489
|
+
registeredTarget?.threadId !== undefined &&
|
|
3490
|
+
(!registeredSlot || !/^[A-Z]$/u.test(registeredSlot))
|
|
3491
|
+
) {
|
|
3492
|
+
throw new Error(
|
|
3493
|
+
"Telegram Thread slot authority is unavailable.",
|
|
3494
|
+
);
|
|
3495
|
+
}
|
|
3496
|
+
const assertRegistrationPublishable = (): void => {
|
|
3497
|
+
if (!registrationCurrent())
|
|
3498
|
+
throw new Error(
|
|
3499
|
+
"Telegram follower registration lost leader ownership.",
|
|
3500
|
+
);
|
|
3501
|
+
if (!displayCompatible())
|
|
3502
|
+
throw new Error(
|
|
3503
|
+
"Thread display mode changed; update or restart this follower before connecting.",
|
|
3504
|
+
);
|
|
3505
|
+
};
|
|
3506
|
+
let publicationOpen = true;
|
|
3507
|
+
let published: TelegramBusFollowerView | undefined;
|
|
3508
|
+
const publish = (): void => {
|
|
3509
|
+
if (!publicationOpen)
|
|
3510
|
+
throw new Error(
|
|
3511
|
+
"Telegram follower registration publication is no longer available.",
|
|
3512
|
+
);
|
|
3513
|
+
publicationOpen = false;
|
|
3514
|
+
assertRegistrationPublishable();
|
|
3515
|
+
published = deps.followerRegistry.register({
|
|
3516
|
+
...envelope.registration,
|
|
3517
|
+
connectedAtMs: getNowMs(),
|
|
3518
|
+
target: registeredTarget,
|
|
3519
|
+
...(registeredSlot ? { slot: registeredSlot } : {}),
|
|
3520
|
+
...((target?.threadName ?? envelope.registration.threadName)
|
|
3521
|
+
? {
|
|
3522
|
+
threadName:
|
|
3523
|
+
target?.threadName ??
|
|
3524
|
+
envelope.registration.threadName,
|
|
3525
|
+
}
|
|
3526
|
+
: {}),
|
|
3527
|
+
});
|
|
3528
|
+
};
|
|
3529
|
+
try {
|
|
3530
|
+
if (deps.commitFollowerRegistration)
|
|
3531
|
+
await deps.commitFollowerRegistration(
|
|
3532
|
+
{
|
|
3533
|
+
registration: envelope.registration,
|
|
3534
|
+
target: registeredTarget,
|
|
3535
|
+
slot: registeredSlot,
|
|
3536
|
+
},
|
|
3537
|
+
publish,
|
|
3538
|
+
);
|
|
3539
|
+
else publish();
|
|
3540
|
+
} finally {
|
|
3541
|
+
publicationOpen = false;
|
|
3542
|
+
}
|
|
3543
|
+
if (!published)
|
|
3544
|
+
throw new Error(
|
|
3545
|
+
"Telegram follower registration publication was not confirmed.",
|
|
3546
|
+
);
|
|
3547
|
+
assertRegistrationPublishable();
|
|
3548
|
+
const follower = deps.followerRegistry.get(
|
|
3549
|
+
envelope.registration.instanceId,
|
|
3550
|
+
);
|
|
3551
|
+
if (!follower)
|
|
3552
|
+
throw new Error(
|
|
3553
|
+
"Telegram follower registration changed before acknowledgement.",
|
|
3554
|
+
);
|
|
3555
|
+
const {
|
|
3556
|
+
lastHeartbeatMs: _publishedHeartbeat,
|
|
3557
|
+
connectedAtMs: _publishedAt,
|
|
3558
|
+
threadName: _publishedName,
|
|
3559
|
+
...publishedIdentity
|
|
3560
|
+
} = published;
|
|
3561
|
+
const {
|
|
3562
|
+
lastHeartbeatMs: _heartbeat,
|
|
3563
|
+
connectedAtMs: _connectedAt,
|
|
3564
|
+
threadName: _threadName,
|
|
3565
|
+
...identity
|
|
3566
|
+
} = follower;
|
|
3567
|
+
if (!isDeepStrictEqual(publishedIdentity, identity))
|
|
3568
|
+
throw new Error(
|
|
3569
|
+
"Telegram follower registration changed before acknowledgement.",
|
|
3570
|
+
);
|
|
3571
|
+
const displayTitle = deps.getFollowerDisplayTitle?.(follower);
|
|
3572
|
+
deps.onFollowerRegistered?.();
|
|
3573
|
+
return {
|
|
3574
|
+
kind: "bus.ack" as const,
|
|
3575
|
+
requestId: envelope.requestId,
|
|
3576
|
+
ok: true,
|
|
3577
|
+
protocol: deps.protocolIdentity,
|
|
3578
|
+
...(registeredTarget
|
|
3579
|
+
? {
|
|
3580
|
+
result: {
|
|
3581
|
+
...registeredTarget,
|
|
3582
|
+
...(displayTitle !== undefined ? { displayTitle } : {}),
|
|
3583
|
+
},
|
|
3584
|
+
}
|
|
3585
|
+
: {}),
|
|
3586
|
+
};
|
|
3587
|
+
} catch (error) {
|
|
3588
|
+
if (
|
|
3589
|
+
!restoringWorkspace &&
|
|
3590
|
+
deps.runWithWorkspaceCapacity &&
|
|
3591
|
+
error instanceof TelegramWorkspaceSlotUnavailableError
|
|
3592
|
+
)
|
|
3593
|
+
throw error;
|
|
2533
3594
|
return {
|
|
2534
3595
|
kind: "bus.ack" as const,
|
|
2535
3596
|
requestId: envelope.requestId,
|
|
2536
3597
|
ok: false,
|
|
2537
3598
|
protocol: deps.protocolIdentity,
|
|
2538
|
-
|
|
2539
|
-
|
|
3599
|
+
message:
|
|
3600
|
+
error instanceof Error
|
|
3601
|
+
? error.message
|
|
3602
|
+
: "Telegram bus follower target provisioning failed.",
|
|
2540
3603
|
};
|
|
2541
3604
|
}
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
(!registeredSlot || !/^[A-Z]$/u.test(registeredSlot))) {
|
|
2551
|
-
throw new Error("Telegram Thread slot authority is unavailable.");
|
|
2552
|
-
}
|
|
2553
|
-
let publicationOpen = true;
|
|
2554
|
-
let published: TelegramBusFollowerView | undefined;
|
|
2555
|
-
const publish = (): void => {
|
|
2556
|
-
if (!publicationOpen) throw new Error("Telegram follower registration publication is no longer available.");
|
|
2557
|
-
publicationOpen = false;
|
|
2558
|
-
if (!registrationCurrent()) throw new Error("Telegram follower registration lost leader ownership.");
|
|
2559
|
-
if (!displayCompatible()) throw new Error("Thread display mode changed; update or restart this follower before connecting.");
|
|
2560
|
-
published = deps.followerRegistry.register({
|
|
2561
|
-
...envelope.registration,
|
|
2562
|
-
connectedAtMs: getNowMs(),
|
|
2563
|
-
target: registeredTarget,
|
|
2564
|
-
...(registeredSlot ? { slot: registeredSlot } : {}),
|
|
2565
|
-
...((target?.threadName ?? envelope.registration.threadName)
|
|
2566
|
-
? { threadName: target?.threadName ?? envelope.registration.threadName } : {}),
|
|
2567
|
-
});
|
|
2568
|
-
};
|
|
2569
|
-
try {
|
|
2570
|
-
if (deps.commitFollowerRegistration) await deps.commitFollowerRegistration({
|
|
2571
|
-
registration: envelope.registration, target: registeredTarget, slot: registeredSlot,
|
|
2572
|
-
}, publish);
|
|
2573
|
-
else publish();
|
|
2574
|
-
} finally { publicationOpen = false; }
|
|
2575
|
-
if (!published) throw new Error("Telegram follower registration publication was not confirmed.");
|
|
2576
|
-
if (!registrationCurrent()) throw new Error("Telegram follower registration lost leader ownership.");
|
|
2577
|
-
if (!displayCompatible()) throw new Error("Thread display mode changed; update or restart this follower before connecting.");
|
|
2578
|
-
const follower = deps.followerRegistry.get(envelope.registration.instanceId);
|
|
2579
|
-
if (!follower) throw new Error("Telegram follower registration changed before acknowledgement.");
|
|
2580
|
-
const { lastHeartbeatMs: _publishedHeartbeat, connectedAtMs: _publishedAt, threadName: _publishedName, ...publishedIdentity } = published;
|
|
2581
|
-
const { lastHeartbeatMs: _heartbeat, connectedAtMs: _connectedAt, threadName: _threadName, ...identity } = follower;
|
|
2582
|
-
if (!isDeepStrictEqual(publishedIdentity, identity)) throw new Error("Telegram follower registration changed before acknowledgement.");
|
|
2583
|
-
const displayTitle = deps.getFollowerDisplayTitle?.(follower);
|
|
2584
|
-
deps.onFollowerRegistered?.();
|
|
2585
|
-
return {
|
|
2586
|
-
kind: "bus.ack" as const,
|
|
2587
|
-
requestId: envelope.requestId,
|
|
2588
|
-
ok: true,
|
|
2589
|
-
protocol: deps.protocolIdentity,
|
|
2590
|
-
...(registeredTarget ? {
|
|
2591
|
-
result: {
|
|
2592
|
-
...registeredTarget,
|
|
2593
|
-
...(displayTitle !== undefined ? { displayTitle } : {}),
|
|
3605
|
+
});
|
|
3606
|
+
const register = () =>
|
|
3607
|
+
deps.runWorkspaceAdmission
|
|
3608
|
+
? deps.runWorkspaceAdmission(
|
|
3609
|
+
{
|
|
3610
|
+
operationId: `follower-registration:${envelope.requestId}`,
|
|
3611
|
+
operationKind: "workspace.register-follower",
|
|
3612
|
+
scopes: [{ kind: "profile" }],
|
|
2594
3613
|
},
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
if (!restoringWorkspace && deps.runWithWorkspaceCapacity &&
|
|
2599
|
-
error instanceof TelegramWorkspaceSlotUnavailableError) throw error;
|
|
2600
|
-
return {
|
|
2601
|
-
kind: "bus.ack" as const,
|
|
2602
|
-
requestId: envelope.requestId,
|
|
2603
|
-
ok: false,
|
|
2604
|
-
protocol: deps.protocolIdentity,
|
|
2605
|
-
message:
|
|
2606
|
-
error instanceof Error
|
|
2607
|
-
? error.message
|
|
2608
|
-
: "Telegram bus follower target provisioning failed.",
|
|
2609
|
-
};
|
|
2610
|
-
}
|
|
2611
|
-
},
|
|
2612
|
-
);
|
|
2613
|
-
const register = () => deps.runWorkspaceAdmission ? deps.runWorkspaceAdmission(
|
|
2614
|
-
{
|
|
2615
|
-
operationId: `follower-registration:${envelope.requestId}`,
|
|
2616
|
-
operationKind: "workspace.register-follower",
|
|
2617
|
-
scopes: [{ kind: "profile" }],
|
|
2618
|
-
},
|
|
2619
|
-
registrationOperation,
|
|
2620
|
-
) : registrationOperation();
|
|
3614
|
+
registrationOperation,
|
|
3615
|
+
)
|
|
3616
|
+
: registrationOperation();
|
|
2621
3617
|
try {
|
|
2622
3618
|
return await (!restoringWorkspace && deps.runWithWorkspaceCapacity
|
|
2623
|
-
? deps.runWithWorkspaceCapacity(register)
|
|
3619
|
+
? deps.runWithWorkspaceCapacity(register)
|
|
3620
|
+
: register());
|
|
2624
3621
|
} catch (error) {
|
|
2625
3622
|
return {
|
|
2626
3623
|
kind: "bus.ack" as const,
|
|
@@ -2638,199 +3635,96 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
2638
3635
|
return routeQueueHandoff(envelope);
|
|
2639
3636
|
case "follower.setThreadDisplayMode": {
|
|
2640
3637
|
const epoch = deps.getCurrentLeaderEpoch?.();
|
|
2641
|
-
const current = () =>
|
|
2642
|
-
|
|
3638
|
+
const current = () =>
|
|
3639
|
+
epoch !== undefined &&
|
|
3640
|
+
deps.getCurrentLeaderEpoch?.() === epoch &&
|
|
3641
|
+
deps.followerRegistry.get(envelope.instanceId)
|
|
3642
|
+
?.registrationGeneration === envelope.registrationGeneration;
|
|
2643
3643
|
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
2644
|
-
const requiredCapability =
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
3644
|
+
const requiredCapability =
|
|
3645
|
+
envelope.mode === "directory-snake" ||
|
|
3646
|
+
envelope.mode === "directory-title"
|
|
3647
|
+
? TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT
|
|
3648
|
+
: TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE;
|
|
3649
|
+
if (
|
|
3650
|
+
!current() ||
|
|
3651
|
+
!follower?.registrationGeneration ||
|
|
3652
|
+
!deps.applyThreadDisplayMode ||
|
|
3653
|
+
!hasTelegramBusCapability(
|
|
3654
|
+
deps.protocolIdentity,
|
|
3655
|
+
requiredCapability,
|
|
3656
|
+
) ||
|
|
3657
|
+
!hasTelegramBusCapability(follower.protocol, requiredCapability)
|
|
3658
|
+
) {
|
|
3659
|
+
return rejectTelegramBusRequest(
|
|
3660
|
+
envelope.requestId,
|
|
3661
|
+
"Thread display settings require current registration and compatible leader authority.",
|
|
3662
|
+
);
|
|
2652
3663
|
}
|
|
2653
3664
|
try {
|
|
2654
3665
|
await deps.applyThreadDisplayMode(envelope.mode, current);
|
|
2655
|
-
if (!current())
|
|
2656
|
-
|
|
3666
|
+
if (!current())
|
|
3667
|
+
throw new Error(
|
|
3668
|
+
"Thread display setting completed for a stale registration.",
|
|
3669
|
+
);
|
|
3670
|
+
return {
|
|
3671
|
+
kind: "bus.ack",
|
|
3672
|
+
requestId: envelope.requestId,
|
|
3673
|
+
ok: true,
|
|
3674
|
+
result: { mode: envelope.mode },
|
|
3675
|
+
};
|
|
2657
3676
|
} catch (error) {
|
|
2658
|
-
return {
|
|
2659
|
-
|
|
3677
|
+
return {
|
|
3678
|
+
kind: "bus.ack",
|
|
3679
|
+
requestId: envelope.requestId,
|
|
3680
|
+
ok: false,
|
|
3681
|
+
message:
|
|
3682
|
+
error instanceof Error
|
|
3683
|
+
? error.message
|
|
3684
|
+
: "Thread display setting was not fully applied.",
|
|
3685
|
+
};
|
|
2660
3686
|
}
|
|
2661
3687
|
}
|
|
2662
|
-
case "follower.renameThread":
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
return {
|
|
2670
|
-
kind: "bus.ack" as const,
|
|
2671
|
-
requestId: envelope.requestId,
|
|
2672
|
-
ok: false,
|
|
2673
|
-
message: "Unknown Telegram bus follower instance.",
|
|
2674
|
-
};
|
|
2675
|
-
}
|
|
2676
|
-
if (
|
|
2677
|
-
!follower.registrationGeneration ||
|
|
2678
|
-
envelope.registrationGeneration !== follower.registrationGeneration ||
|
|
2679
|
-
follower.target?.chatId !== envelope.target.chatId ||
|
|
2680
|
-
follower.target?.threadId !== envelope.target.threadId
|
|
2681
|
-
) {
|
|
2682
|
-
return {
|
|
2683
|
-
kind: "bus.ack" as const,
|
|
2684
|
-
requestId: envelope.requestId,
|
|
2685
|
-
ok: false,
|
|
2686
|
-
message: "Stale Telegram bus follower registration generation.",
|
|
2687
|
-
};
|
|
2688
|
-
}
|
|
2689
|
-
if (
|
|
2690
|
-
!hasTelegramBusCapability(
|
|
2691
|
-
deps.protocolIdentity,
|
|
2692
|
-
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
2693
|
-
) ||
|
|
2694
|
-
!hasTelegramBusCapability(
|
|
2695
|
-
follower.protocol,
|
|
2696
|
-
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
2697
|
-
) ||
|
|
2698
|
-
!deps.renameFollowerThread
|
|
2699
|
-
) {
|
|
2700
|
-
return {
|
|
2701
|
-
kind: "bus.ack" as const,
|
|
2702
|
-
requestId: envelope.requestId,
|
|
2703
|
-
ok: false,
|
|
2704
|
-
message: "Telegram Workspace Thread rename is unavailable.",
|
|
2705
|
-
};
|
|
2706
|
-
}
|
|
2707
|
-
try {
|
|
2708
|
-
const result = await deps.renameFollowerThread(
|
|
2709
|
-
follower,
|
|
2710
|
-
envelope.threadName,
|
|
2711
|
-
);
|
|
2712
|
-
const current = deps.followerRegistry.get(follower.instanceId);
|
|
2713
|
-
if (
|
|
2714
|
-
current?.registrationGeneration !== follower.registrationGeneration
|
|
2715
|
-
) {
|
|
2716
|
-
return {
|
|
2717
|
-
kind: "bus.ack" as const,
|
|
2718
|
-
requestId: envelope.requestId,
|
|
2719
|
-
ok: false,
|
|
2720
|
-
message: "Stale Telegram bus follower registration generation.",
|
|
2721
|
-
};
|
|
2722
|
-
}
|
|
2723
|
-
deps.followerRegistry.register({
|
|
2724
|
-
...current,
|
|
2725
|
-
threadName: result.threadName,
|
|
2726
|
-
connectedAtMs: current.connectedAtMs,
|
|
2727
|
-
});
|
|
2728
|
-
return {
|
|
2729
|
-
kind: "bus.ack" as const,
|
|
2730
|
-
requestId: envelope.requestId,
|
|
2731
|
-
ok: true,
|
|
2732
|
-
result,
|
|
2733
|
-
};
|
|
2734
|
-
} catch (error) {
|
|
2735
|
-
return {
|
|
2736
|
-
kind: "bus.ack" as const,
|
|
2737
|
-
requestId: envelope.requestId,
|
|
2738
|
-
ok: false,
|
|
2739
|
-
message:
|
|
2740
|
-
error instanceof Error
|
|
2741
|
-
? error.message
|
|
2742
|
-
: "Telegram Workspace Thread rename failed.",
|
|
2743
|
-
};
|
|
2744
|
-
}
|
|
2745
|
-
},
|
|
3688
|
+
case "follower.renameThread":
|
|
3689
|
+
return changeFollowerThreadName(
|
|
3690
|
+
envelope,
|
|
3691
|
+
"rename",
|
|
3692
|
+
deps.renameFollowerThread &&
|
|
3693
|
+
((follower) =>
|
|
3694
|
+
deps.renameFollowerThread!(follower, envelope.threadName)),
|
|
2746
3695
|
);
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
async () => {
|
|
2753
|
-
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
2754
|
-
if (!follower || !follower.registrationGeneration ||
|
|
2755
|
-
envelope.registrationGeneration !== follower.registrationGeneration ||
|
|
2756
|
-
follower.target?.chatId !== envelope.target.chatId ||
|
|
2757
|
-
follower.target?.threadId !== envelope.target.threadId) {
|
|
2758
|
-
return {
|
|
2759
|
-
kind: "bus.ack" as const,
|
|
2760
|
-
requestId: envelope.requestId,
|
|
2761
|
-
ok: false,
|
|
2762
|
-
message: "Stale or unknown Telegram bus follower registration.",
|
|
2763
|
-
};
|
|
2764
|
-
}
|
|
2765
|
-
if (!hasTelegramBusCapability(
|
|
2766
|
-
deps.protocolIdentity,
|
|
2767
|
-
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
2768
|
-
) || !hasTelegramBusCapability(
|
|
2769
|
-
follower.protocol,
|
|
2770
|
-
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
2771
|
-
) || !deps.resetFollowerThreadName) {
|
|
2772
|
-
return {
|
|
2773
|
-
kind: "bus.ack" as const,
|
|
2774
|
-
requestId: envelope.requestId,
|
|
2775
|
-
ok: false,
|
|
2776
|
-
message: "Telegram Workspace Thread reset is unavailable.",
|
|
2777
|
-
};
|
|
2778
|
-
}
|
|
2779
|
-
try {
|
|
2780
|
-
const result = await deps.resetFollowerThreadName(follower);
|
|
2781
|
-
const current = deps.followerRegistry.get(follower.instanceId);
|
|
2782
|
-
if (current?.registrationGeneration !==
|
|
2783
|
-
follower.registrationGeneration) {
|
|
2784
|
-
return {
|
|
2785
|
-
kind: "bus.ack" as const,
|
|
2786
|
-
requestId: envelope.requestId,
|
|
2787
|
-
ok: false,
|
|
2788
|
-
message: "Stale Telegram bus follower registration generation.",
|
|
2789
|
-
};
|
|
2790
|
-
}
|
|
2791
|
-
deps.followerRegistry.register({
|
|
2792
|
-
...current,
|
|
2793
|
-
threadName: result.threadName,
|
|
2794
|
-
connectedAtMs: current.connectedAtMs,
|
|
2795
|
-
});
|
|
2796
|
-
return {
|
|
2797
|
-
kind: "bus.ack" as const,
|
|
2798
|
-
requestId: envelope.requestId,
|
|
2799
|
-
ok: true,
|
|
2800
|
-
result,
|
|
2801
|
-
};
|
|
2802
|
-
} catch (error) {
|
|
2803
|
-
return {
|
|
2804
|
-
kind: "bus.ack" as const,
|
|
2805
|
-
requestId: envelope.requestId,
|
|
2806
|
-
ok: false,
|
|
2807
|
-
message: error instanceof Error
|
|
2808
|
-
? error.message : "Telegram Workspace Thread reset failed.",
|
|
2809
|
-
};
|
|
2810
|
-
}
|
|
2811
|
-
},
|
|
3696
|
+
case "follower.resetThreadName":
|
|
3697
|
+
return changeFollowerThreadName(
|
|
3698
|
+
envelope,
|
|
3699
|
+
"reset",
|
|
3700
|
+
deps.resetFollowerThreadName,
|
|
2812
3701
|
);
|
|
2813
|
-
}
|
|
2814
3702
|
case "follower.publishSessionReplacement":
|
|
2815
3703
|
case "follower.settleSessionReplacement": {
|
|
2816
|
-
const registeredFollower = deps.followerRegistry.get(
|
|
3704
|
+
const registeredFollower = deps.followerRegistry.get(
|
|
3705
|
+
envelope.instanceId,
|
|
3706
|
+
);
|
|
2817
3707
|
return runFollowerMutation(
|
|
2818
3708
|
registeredFollower ?? { instanceId: envelope.instanceId },
|
|
2819
3709
|
async () => {
|
|
2820
|
-
const publish =
|
|
3710
|
+
const publish =
|
|
3711
|
+
envelope.kind === "follower.publishSessionReplacement";
|
|
2821
3712
|
const operation = publish
|
|
2822
3713
|
? deps.publishFollowerSessionReplacement
|
|
2823
3714
|
: deps.settleFollowerSessionReplacement;
|
|
2824
3715
|
const epoch = deps.getCurrentLeaderEpoch?.();
|
|
2825
3716
|
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
2826
|
-
const isCurrent = () =>
|
|
3717
|
+
const isCurrent = () =>
|
|
3718
|
+
epoch !== undefined &&
|
|
2827
3719
|
deps.getCurrentLeaderEpoch?.() === epoch &&
|
|
2828
|
-
deps.followerRegistry.get(envelope.instanceId)
|
|
2829
|
-
envelope.registrationGeneration;
|
|
3720
|
+
deps.followerRegistry.get(envelope.instanceId)
|
|
3721
|
+
?.registrationGeneration === envelope.registrationGeneration;
|
|
2830
3722
|
if (
|
|
2831
3723
|
!follower?.registrationGeneration ||
|
|
2832
|
-
follower.registrationGeneration !==
|
|
2833
|
-
|
|
3724
|
+
follower.registrationGeneration !==
|
|
3725
|
+
envelope.registrationGeneration ||
|
|
3726
|
+
!isCurrent() ||
|
|
3727
|
+
!operation ||
|
|
2834
3728
|
!hasTelegramBusCapability(
|
|
2835
3729
|
deps.protocolIdentity,
|
|
2836
3730
|
TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
@@ -2840,16 +3734,17 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
2840
3734
|
TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
2841
3735
|
)
|
|
2842
3736
|
) {
|
|
2843
|
-
return
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
message:
|
|
2848
|
-
"Telegram session replacement requires current follower registration and compatible leader authority.",
|
|
2849
|
-
};
|
|
3737
|
+
return rejectTelegramBusRequest(
|
|
3738
|
+
envelope.requestId,
|
|
3739
|
+
"Telegram session replacement requires current follower registration and compatible leader authority.",
|
|
3740
|
+
);
|
|
2850
3741
|
}
|
|
2851
3742
|
try {
|
|
2852
|
-
const committed = await operation(
|
|
3743
|
+
const committed = await operation(
|
|
3744
|
+
follower,
|
|
3745
|
+
envelope.intent,
|
|
3746
|
+
isCurrent,
|
|
3747
|
+
);
|
|
2853
3748
|
if (!committed || !isCurrent()) {
|
|
2854
3749
|
return {
|
|
2855
3750
|
kind: "bus.ack" as const,
|
|
@@ -2871,88 +3766,76 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
2871
3766
|
kind: "bus.ack" as const,
|
|
2872
3767
|
requestId: envelope.requestId,
|
|
2873
3768
|
ok: false,
|
|
2874
|
-
message:
|
|
2875
|
-
|
|
2876
|
-
|
|
3769
|
+
message:
|
|
3770
|
+
error instanceof Error
|
|
3771
|
+
? error.message
|
|
3772
|
+
: "Telegram session replacement request failed.",
|
|
2877
3773
|
};
|
|
2878
3774
|
}
|
|
2879
3775
|
},
|
|
2880
3776
|
);
|
|
2881
3777
|
}
|
|
2882
3778
|
case "follower.disconnect": {
|
|
2883
|
-
const registeredFollower = deps.followerRegistry.get(
|
|
3779
|
+
const registeredFollower = deps.followerRegistry.get(
|
|
3780
|
+
envelope.instanceId,
|
|
3781
|
+
);
|
|
2884
3782
|
return runFollowerMutation(
|
|
2885
3783
|
registeredFollower ?? { instanceId: envelope.instanceId },
|
|
2886
3784
|
async () => {
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
3785
|
+
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
3786
|
+
if (!follower) {
|
|
3787
|
+
return rejectTelegramBusRequest(
|
|
3788
|
+
envelope.requestId,
|
|
3789
|
+
"Unknown Telegram bus follower instance.",
|
|
3790
|
+
);
|
|
3791
|
+
}
|
|
3792
|
+
if (
|
|
3793
|
+
!follower.registrationGeneration ||
|
|
3794
|
+
!envelope.registrationGeneration ||
|
|
3795
|
+
envelope.registrationGeneration !==
|
|
3796
|
+
follower.registrationGeneration
|
|
3797
|
+
) {
|
|
3798
|
+
return rejectTelegramBusRequest(
|
|
3799
|
+
envelope.requestId,
|
|
3800
|
+
"Stale Telegram bus follower registration generation.",
|
|
3801
|
+
);
|
|
3802
|
+
}
|
|
3803
|
+
await deps.onFollowerDisconnected?.(follower);
|
|
3804
|
+
const current = deps.followerRegistry.get(follower.instanceId);
|
|
3805
|
+
if (
|
|
3806
|
+
current?.registrationGeneration !==
|
|
3807
|
+
follower.registrationGeneration
|
|
3808
|
+
) {
|
|
3809
|
+
return rejectTelegramBusRequest(
|
|
3810
|
+
envelope.requestId,
|
|
3811
|
+
"Stale Telegram bus follower registration generation.",
|
|
3812
|
+
);
|
|
3813
|
+
}
|
|
3814
|
+
deps.followerRegistry.remove(follower.instanceId);
|
|
2913
3815
|
return {
|
|
2914
3816
|
kind: "bus.ack" as const,
|
|
2915
3817
|
requestId: envelope.requestId,
|
|
2916
|
-
ok:
|
|
2917
|
-
message: "Stale Telegram bus follower registration generation.",
|
|
3818
|
+
ok: true,
|
|
2918
3819
|
};
|
|
2919
|
-
}
|
|
2920
|
-
deps.followerRegistry.remove(follower.instanceId);
|
|
2921
|
-
return {
|
|
2922
|
-
kind: "bus.ack" as const,
|
|
2923
|
-
requestId: envelope.requestId,
|
|
2924
|
-
ok: true,
|
|
2925
|
-
};
|
|
2926
3820
|
},
|
|
2927
3821
|
);
|
|
2928
3822
|
}
|
|
2929
3823
|
case "follower.heartbeat": {
|
|
2930
|
-
const current =
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
};
|
|
2938
|
-
}
|
|
2939
|
-
if (
|
|
2940
|
-
!current.registrationGeneration ||
|
|
2941
|
-
envelope.registrationGeneration !== current.registrationGeneration
|
|
2942
|
-
) {
|
|
2943
|
-
return {
|
|
2944
|
-
kind: "bus.ack",
|
|
2945
|
-
requestId: envelope.requestId,
|
|
2946
|
-
ok: false,
|
|
2947
|
-
message: "Stale Telegram bus follower registration generation.",
|
|
2948
|
-
};
|
|
2949
|
-
}
|
|
3824
|
+
const current = resolveRegisteredTelegramBusFollower(
|
|
3825
|
+
deps.followerRegistry,
|
|
3826
|
+
envelope.requestId,
|
|
3827
|
+
envelope.instanceId,
|
|
3828
|
+
envelope.registrationGeneration,
|
|
3829
|
+
);
|
|
3830
|
+
if ("rejection" in current) return current.rejection;
|
|
2950
3831
|
const follower = deps.followerRegistry.heartbeat(
|
|
2951
3832
|
envelope.instanceId,
|
|
2952
3833
|
getNowMs(),
|
|
2953
3834
|
);
|
|
2954
3835
|
if (follower) observeRestoreRecipient(follower);
|
|
2955
|
-
const displayTitle = follower
|
|
3836
|
+
const displayTitle = follower
|
|
3837
|
+
? deps.getFollowerDisplayTitle?.(follower)
|
|
3838
|
+
: undefined;
|
|
2956
3839
|
return follower
|
|
2957
3840
|
? {
|
|
2958
3841
|
kind: "bus.ack",
|
|
@@ -2972,18 +3855,17 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
2972
3855
|
),
|
|
2973
3856
|
)
|
|
2974
3857
|
.map((candidate) => candidate.slot)
|
|
2975
|
-
.filter(
|
|
2976
|
-
|
|
3858
|
+
.filter(
|
|
3859
|
+
(slot): slot is string =>
|
|
3860
|
+
typeof slot === "string" && /^[A-Z]$/.test(slot),
|
|
2977
3861
|
)
|
|
2978
3862
|
.sort(),
|
|
2979
3863
|
},
|
|
2980
3864
|
}
|
|
2981
|
-
:
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
message: "Unknown Telegram bus follower instance.",
|
|
2986
|
-
};
|
|
3865
|
+
: rejectTelegramBusRequest(
|
|
3866
|
+
envelope.requestId,
|
|
3867
|
+
"Unknown Telegram bus follower instance.",
|
|
3868
|
+
);
|
|
2987
3869
|
}
|
|
2988
3870
|
case "follower.resolveAgentTarget":
|
|
2989
3871
|
case "follower.routeAgentMessage":
|
|
@@ -2993,15 +3875,33 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
2993
3875
|
case "leader.forwardMessage":
|
|
2994
3876
|
case "leader.forwardEditedMessage":
|
|
2995
3877
|
return forwardToFollower(envelope);
|
|
3878
|
+
case "follower.deliverSelectedMenu": {
|
|
3879
|
+
const deliver = deps.selectedMenuDelivery,
|
|
3880
|
+
secret = deps.authSecret,
|
|
3881
|
+
protocol = structuredClone(deps.protocolIdentity);
|
|
3882
|
+
const current = () =>
|
|
3883
|
+
!!secret &&
|
|
3884
|
+
deps.authSecret === secret &&
|
|
3885
|
+
deps.selectedMenuDelivery === deliver &&
|
|
3886
|
+
isDeepStrictEqual(deps.protocolIdentity, protocol) &&
|
|
3887
|
+
hasTelegramBusCapability(
|
|
3888
|
+
protocol,
|
|
3889
|
+
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
3890
|
+
);
|
|
3891
|
+
if (!deliver || !current())
|
|
3892
|
+
return rejectTelegramBusRequest(
|
|
3893
|
+
envelope.requestId,
|
|
3894
|
+
"Selected-menu delivery is unavailable.",
|
|
3895
|
+
);
|
|
3896
|
+
return deliver(structuredClone(envelope), current);
|
|
3897
|
+
}
|
|
2996
3898
|
case "follower.callApi":
|
|
2997
3899
|
return handleFollowerApiCall(envelope, { ...deps, getNowMs });
|
|
2998
3900
|
default:
|
|
2999
|
-
return
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
message: "Telegram bus envelope is not handled by this leader.",
|
|
3004
|
-
};
|
|
3901
|
+
return rejectTelegramBusRequest(
|
|
3902
|
+
envelope.requestId,
|
|
3903
|
+
"Telegram bus envelope is not handled by this leader.",
|
|
3904
|
+
);
|
|
3005
3905
|
}
|
|
3006
3906
|
};
|
|
3007
3907
|
}
|
|
@@ -3012,13 +3912,6 @@ function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
|
3012
3912
|
: undefined;
|
|
3013
3913
|
}
|
|
3014
3914
|
|
|
3015
|
-
function asInteger(value: unknown): number | undefined {
|
|
3016
|
-
if (typeof value === "number" && Number.isInteger(value)) return value;
|
|
3017
|
-
if (typeof value !== "string" || value.trim() === "") return undefined;
|
|
3018
|
-
const parsed = Number(value);
|
|
3019
|
-
return Number.isInteger(parsed) ? parsed : undefined;
|
|
3020
|
-
}
|
|
3021
|
-
|
|
3022
3915
|
function getFollowerApiMethodAndBody(
|
|
3023
3916
|
envelope: Extract<TelegramBusEnvelope, { kind: "follower.callApi" }>,
|
|
3024
3917
|
): {
|
|
@@ -3088,26 +3981,14 @@ async function handleFollowerApiCall(
|
|
|
3088
3981
|
recordFollowerMessageOwnership?: TelegramBusFollowerMessageOwnershipRecorder;
|
|
3089
3982
|
},
|
|
3090
3983
|
): Promise<TelegramBusEnvelope> {
|
|
3091
|
-
const
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
}
|
|
3100
|
-
if (
|
|
3101
|
-
!follower.registrationGeneration ||
|
|
3102
|
-
envelope.registrationGeneration !== follower.registrationGeneration
|
|
3103
|
-
) {
|
|
3104
|
-
return {
|
|
3105
|
-
kind: "bus.ack",
|
|
3106
|
-
requestId: envelope.requestId,
|
|
3107
|
-
ok: false,
|
|
3108
|
-
message: "Stale Telegram bus follower registration generation.",
|
|
3109
|
-
};
|
|
3110
|
-
}
|
|
3984
|
+
const followerResolution = resolveRegisteredTelegramBusFollower(
|
|
3985
|
+
deps.followerRegistry,
|
|
3986
|
+
envelope.requestId,
|
|
3987
|
+
envelope.instanceId,
|
|
3988
|
+
envelope.registrationGeneration,
|
|
3989
|
+
);
|
|
3990
|
+
if ("rejection" in followerResolution) return followerResolution.rejection;
|
|
3991
|
+
const follower = followerResolution.follower;
|
|
3111
3992
|
deps.followerRegistry.heartbeat(envelope.instanceId, deps.getNowMs());
|
|
3112
3993
|
if (
|
|
3113
3994
|
deps.authorizeFollowerApiCall &&
|
|
@@ -3117,20 +3998,16 @@ async function handleFollowerApiCall(
|
|
|
3117
3998
|
args: envelope.args,
|
|
3118
3999
|
})
|
|
3119
4000
|
) {
|
|
3120
|
-
return
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
message: "Telegram bus API call is not allowed for this follower.",
|
|
3125
|
-
};
|
|
4001
|
+
return rejectTelegramBusRequest(
|
|
4002
|
+
envelope.requestId,
|
|
4003
|
+
"Telegram bus API call is not allowed for this follower.",
|
|
4004
|
+
);
|
|
3126
4005
|
}
|
|
3127
4006
|
if (!deps.callApi) {
|
|
3128
|
-
return
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
message: "Telegram bus leader does not expose API calling.",
|
|
3133
|
-
};
|
|
4007
|
+
return rejectTelegramBusRequest(
|
|
4008
|
+
envelope.requestId,
|
|
4009
|
+
"Telegram bus leader does not expose API calling.",
|
|
4010
|
+
);
|
|
3134
4011
|
}
|
|
3135
4012
|
try {
|
|
3136
4013
|
const result = await deps.callApi(envelope.method, envelope.args);
|
|
@@ -3147,10 +4024,9 @@ async function handleFollowerApiCall(
|
|
|
3147
4024
|
result,
|
|
3148
4025
|
};
|
|
3149
4026
|
} catch (error) {
|
|
3150
|
-
const staleTarget =
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
: undefined;
|
|
4027
|
+
const staleTarget = Threads.isTelegramTopicTargetStaleError(error)
|
|
4028
|
+
? getTelegramApiErrorRequestTarget(error)
|
|
4029
|
+
: undefined;
|
|
3154
4030
|
return {
|
|
3155
4031
|
kind: "bus.ack",
|
|
3156
4032
|
requestId: envelope.requestId,
|
|
@@ -3179,63 +4055,6 @@ async function handleFollowerApiCall(
|
|
|
3179
4055
|
}
|
|
3180
4056
|
}
|
|
3181
4057
|
|
|
3182
|
-
export interface TelegramBusLeaderActivationSchedulerDeps<TContext> {
|
|
3183
|
-
isBusEnabled: () => boolean;
|
|
3184
|
-
ownsPolling: (ctx: TContext) => boolean;
|
|
3185
|
-
isBusPollingStarted: () => boolean;
|
|
3186
|
-
setBusPollingStarted: (started: boolean) => void;
|
|
3187
|
-
stopClassicPolling: () => Promise<void>;
|
|
3188
|
-
startClassicPolling: (ctx: TContext) => void | Promise<void>;
|
|
3189
|
-
startBusLeaderPolling: (ctx: TContext) => Promise<void>;
|
|
3190
|
-
updateStatus: (ctx: TContext) => void;
|
|
3191
|
-
recordRuntimeEvent?: (
|
|
3192
|
-
category: string,
|
|
3193
|
-
error: unknown,
|
|
3194
|
-
details?: Record<string, unknown>,
|
|
3195
|
-
) => void;
|
|
3196
|
-
}
|
|
3197
|
-
|
|
3198
|
-
export function createTelegramBusLeaderActivationScheduler<TContext>(
|
|
3199
|
-
deps: TelegramBusLeaderActivationSchedulerDeps<TContext>,
|
|
3200
|
-
): (ctx: TContext) => void {
|
|
3201
|
-
let pending = false;
|
|
3202
|
-
return (ctx) => {
|
|
3203
|
-
if (deps.isBusPollingStarted() || pending) return;
|
|
3204
|
-
if (!deps.isBusEnabled()) return;
|
|
3205
|
-
if (!deps.ownsPolling(ctx)) return;
|
|
3206
|
-
pending = true;
|
|
3207
|
-
const timer = setTimeout(() => {
|
|
3208
|
-
void (async () => {
|
|
3209
|
-
try {
|
|
3210
|
-
if (deps.isBusPollingStarted()) return;
|
|
3211
|
-
if (!deps.isBusEnabled()) return;
|
|
3212
|
-
if (!deps.ownsPolling(ctx)) return;
|
|
3213
|
-
await deps.stopClassicPolling();
|
|
3214
|
-
try {
|
|
3215
|
-
await deps.startBusLeaderPolling(ctx);
|
|
3216
|
-
deps.setBusPollingStarted(true);
|
|
3217
|
-
deps.updateStatus(ctx);
|
|
3218
|
-
deps.recordRuntimeEvent?.(
|
|
3219
|
-
"bus",
|
|
3220
|
-
"Telegram bus leader mode activated",
|
|
3221
|
-
{ phase: "leader-hot-switch" },
|
|
3222
|
-
);
|
|
3223
|
-
} catch (error) {
|
|
3224
|
-
deps.recordRuntimeEvent?.("bus", error, {
|
|
3225
|
-
phase: "leader-hot-switch",
|
|
3226
|
-
});
|
|
3227
|
-
deps.setBusPollingStarted(false);
|
|
3228
|
-
await deps.startClassicPolling(ctx);
|
|
3229
|
-
}
|
|
3230
|
-
} finally {
|
|
3231
|
-
pending = false;
|
|
3232
|
-
}
|
|
3233
|
-
})();
|
|
3234
|
-
}, 0);
|
|
3235
|
-
timer.unref?.();
|
|
3236
|
-
};
|
|
3237
|
-
}
|
|
3238
|
-
|
|
3239
4058
|
export function createTelegramBusLeaderRuntime<TContext>(
|
|
3240
4059
|
deps: TelegramBusLeaderRuntimeDeps<TContext>,
|
|
3241
4060
|
): TelegramBusLeaderRuntime<TContext> {
|
|
@@ -3250,20 +4069,27 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
3250
4069
|
type PreservationObservation = {
|
|
3251
4070
|
follower: TelegramBusFollowerView;
|
|
3252
4071
|
operationId: string;
|
|
3253
|
-
registration: ReturnType<
|
|
4072
|
+
registration: ReturnType<
|
|
4073
|
+
TelegramBusFollowerRegistry["observeUnregistered"]
|
|
4074
|
+
>;
|
|
3254
4075
|
isCurrent: () => boolean;
|
|
3255
4076
|
};
|
|
3256
4077
|
const pendingPreservations = new Map<string, PreservationObservation>();
|
|
3257
|
-
const forgetPreservation = (
|
|
4078
|
+
const forgetPreservation = (
|
|
4079
|
+
observation: PreservationObservation | undefined,
|
|
4080
|
+
) => {
|
|
3258
4081
|
if (!observation) return;
|
|
3259
|
-
if (
|
|
4082
|
+
if (
|
|
4083
|
+
pendingPreservations.get(observation.follower.instanceId) === observation
|
|
4084
|
+
) {
|
|
3260
4085
|
pendingPreservations.delete(observation.follower.instanceId);
|
|
3261
4086
|
}
|
|
3262
4087
|
observation.registration.release();
|
|
3263
4088
|
};
|
|
3264
4089
|
const stopPruning = () => {
|
|
3265
4090
|
pruneGeneration += 1;
|
|
3266
|
-
for (const observation of pendingPreservations.values())
|
|
4091
|
+
for (const observation of pendingPreservations.values())
|
|
4092
|
+
forgetPreservation(observation);
|
|
3267
4093
|
if (pruneInterval) clearInterval(pruneInterval);
|
|
3268
4094
|
pruneInterval = undefined;
|
|
3269
4095
|
prunePromise = undefined;
|
|
@@ -3281,9 +4107,11 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
3281
4107
|
const pruneFollowers = async (expectedGeneration: number) => {
|
|
3282
4108
|
const epoch = deps.getCurrentLeaderEpoch?.();
|
|
3283
4109
|
const profile = deps.getTelegramProfile?.();
|
|
3284
|
-
const isCurrent = (): boolean =>
|
|
4110
|
+
const isCurrent = (): boolean =>
|
|
4111
|
+
pruneGeneration === expectedGeneration &&
|
|
3285
4112
|
deps.getTelegramProfile?.() === profile &&
|
|
3286
|
-
(!deps.getCurrentLeaderEpoch ||
|
|
4113
|
+
(!deps.getCurrentLeaderEpoch ||
|
|
4114
|
+
(epoch !== undefined && deps.getCurrentLeaderEpoch() === epoch));
|
|
3287
4115
|
try {
|
|
3288
4116
|
await localServer.ensureEndpoint();
|
|
3289
4117
|
} catch (error) {
|
|
@@ -3294,22 +4122,38 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
3294
4122
|
if (!observation.isCurrent()) forgetPreservation(observation);
|
|
3295
4123
|
}
|
|
3296
4124
|
if (!isCurrent()) return;
|
|
3297
|
-
const attempts: {
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
4125
|
+
const attempts: {
|
|
4126
|
+
follower: TelegramBusFollowerView;
|
|
4127
|
+
observation?: PreservationObservation;
|
|
4128
|
+
fresh: boolean;
|
|
4129
|
+
}[] = [...pendingPreservations.values()].map((observation) => ({
|
|
4130
|
+
follower: observation.follower,
|
|
4131
|
+
observation,
|
|
4132
|
+
fresh: false,
|
|
4133
|
+
}));
|
|
4134
|
+
const removed = deps.followerRegistry.pruneStale(
|
|
4135
|
+
getNowMs(),
|
|
4136
|
+
followerStaleAfterMs,
|
|
4137
|
+
);
|
|
3302
4138
|
for (const follower of removed) {
|
|
3303
4139
|
forgetPreservation(pendingPreservations.get(follower.instanceId));
|
|
3304
4140
|
let observation: PreservationObservation | undefined;
|
|
3305
|
-
if (
|
|
3306
|
-
|
|
3307
|
-
|
|
4141
|
+
if (
|
|
4142
|
+
deps.onFollowerConfirmedDeadPreserved &&
|
|
4143
|
+
epoch !== undefined &&
|
|
4144
|
+
follower.registrationGeneration &&
|
|
4145
|
+
Number.isSafeInteger(follower.pid) &&
|
|
4146
|
+
follower.pid! > 0 &&
|
|
4147
|
+
Number.isSafeInteger(follower.target?.threadId) &&
|
|
4148
|
+
follower.target!.threadId! > 0
|
|
4149
|
+
) {
|
|
3308
4150
|
if (pendingPreservations.size < TELEGRAM_WORKSPACE_SLOTS.length) {
|
|
3309
|
-
const registration =
|
|
4151
|
+
const registration =
|
|
4152
|
+
deps.followerRegistry.observeUnregistered(follower);
|
|
3310
4153
|
let current = true;
|
|
3311
4154
|
observation = {
|
|
3312
|
-
follower,
|
|
4155
|
+
follower,
|
|
4156
|
+
registration,
|
|
3313
4157
|
// Reuse exact admission authority after a lost acquisition acknowledgement.
|
|
3314
4158
|
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
3315
4159
|
isCurrent() {
|
|
@@ -3319,9 +4163,13 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
3319
4163
|
};
|
|
3320
4164
|
pendingPreservations.set(follower.instanceId, observation);
|
|
3321
4165
|
} else {
|
|
3322
|
-
recordPruneEvent(
|
|
3323
|
-
|
|
3324
|
-
|
|
4166
|
+
recordPruneEvent(
|
|
4167
|
+
"Telegram follower preservation observation capacity exhausted; retaining binding",
|
|
4168
|
+
{
|
|
4169
|
+
phase: "follower-preservation-capacity",
|
|
4170
|
+
instanceId: follower.instanceId,
|
|
4171
|
+
},
|
|
4172
|
+
);
|
|
3325
4173
|
}
|
|
3326
4174
|
}
|
|
3327
4175
|
attempts.push({ follower, observation, fresh: true });
|
|
@@ -3333,21 +4181,37 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
3333
4181
|
continue;
|
|
3334
4182
|
}
|
|
3335
4183
|
let processConfirmedDead = false;
|
|
3336
|
-
if (
|
|
3337
|
-
|
|
4184
|
+
if (
|
|
4185
|
+
follower.pid !== undefined &&
|
|
4186
|
+
Number.isSafeInteger(follower.pid) &&
|
|
4187
|
+
follower.pid > 0 &&
|
|
4188
|
+
deps.isFollowerProcessAlive
|
|
4189
|
+
) {
|
|
3338
4190
|
try {
|
|
3339
|
-
processConfirmedDead =
|
|
4191
|
+
processConfirmedDead =
|
|
4192
|
+
deps.isFollowerProcessAlive(follower.pid) === false;
|
|
3340
4193
|
} catch (error) {
|
|
3341
|
-
if (fresh)
|
|
3342
|
-
|
|
3343
|
-
|
|
4194
|
+
if (fresh)
|
|
4195
|
+
recordPruneEvent(error, {
|
|
4196
|
+
phase: "follower-process-liveness",
|
|
4197
|
+
instanceId: follower.instanceId,
|
|
4198
|
+
pid: follower.pid,
|
|
4199
|
+
});
|
|
3344
4200
|
}
|
|
3345
4201
|
}
|
|
3346
4202
|
if (!processConfirmedDead) {
|
|
3347
|
-
if (fresh)
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
4203
|
+
if (fresh)
|
|
4204
|
+
recordPruneEvent(
|
|
4205
|
+
"Telegram bus follower heartbeat stale; preserving thread binding",
|
|
4206
|
+
{
|
|
4207
|
+
phase: "follower-pruned",
|
|
4208
|
+
instanceId: follower.instanceId,
|
|
4209
|
+
processLiveness:
|
|
4210
|
+
follower.pid === undefined || !deps.isFollowerProcessAlive
|
|
4211
|
+
? "unknown"
|
|
4212
|
+
: "alive-or-unknown",
|
|
4213
|
+
},
|
|
4214
|
+
);
|
|
3351
4215
|
continue;
|
|
3352
4216
|
}
|
|
3353
4217
|
let cleanupEnabled: boolean | undefined;
|
|
@@ -3355,55 +4219,89 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
3355
4219
|
cleanupEnabled = await deps.shouldCleanupConfirmedDeadFollower?.();
|
|
3356
4220
|
} catch (error) {
|
|
3357
4221
|
recordPruneEvent(error, {
|
|
3358
|
-
phase: "follower-confirmed-dead-cleanup-policy",
|
|
4222
|
+
phase: "follower-confirmed-dead-cleanup-policy",
|
|
4223
|
+
instanceId: follower.instanceId,
|
|
4224
|
+
pid: follower.pid,
|
|
3359
4225
|
});
|
|
3360
4226
|
}
|
|
3361
4227
|
if (!isCurrent()) {
|
|
3362
4228
|
forgetPreservation(observation);
|
|
3363
4229
|
return;
|
|
3364
4230
|
}
|
|
3365
|
-
const recordPreserved = () =>
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
4231
|
+
const recordPreserved = () =>
|
|
4232
|
+
recordPruneEvent(
|
|
4233
|
+
"Telegram bus follower process confirmed dead; preserving thread binding",
|
|
4234
|
+
{
|
|
4235
|
+
phase: "follower-confirmed-dead-preserved",
|
|
4236
|
+
instanceId: follower.instanceId,
|
|
4237
|
+
pid: follower.pid,
|
|
4238
|
+
cleanupEnabled,
|
|
4239
|
+
},
|
|
4240
|
+
);
|
|
3370
4241
|
// Deferred observations authorize only non-destructive publication, never deletion retries.
|
|
3371
4242
|
if (cleanupEnabled === true && !fresh) {
|
|
3372
4243
|
forgetPreservation(observation);
|
|
3373
4244
|
recordPreserved();
|
|
3374
4245
|
continue;
|
|
3375
4246
|
}
|
|
3376
|
-
const onConfirmedDead =
|
|
3377
|
-
cleanupEnabled ===
|
|
4247
|
+
const onConfirmedDead =
|
|
4248
|
+
cleanupEnabled === true
|
|
4249
|
+
? deps.onFollowerConfirmedDead
|
|
4250
|
+
: cleanupEnabled === false && observation
|
|
4251
|
+
? deps.onFollowerConfirmedDeadPreserved
|
|
4252
|
+
: undefined;
|
|
3378
4253
|
if (!onConfirmedDead) {
|
|
3379
4254
|
if (cleanupEnabled === true) forgetPreservation(observation);
|
|
3380
4255
|
if (fresh) recordPreserved();
|
|
3381
4256
|
continue;
|
|
3382
4257
|
}
|
|
3383
|
-
const findReplacement = () =>
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
4258
|
+
const findReplacement = () =>
|
|
4259
|
+
deps.followerRegistry
|
|
4260
|
+
.list()
|
|
4261
|
+
.find(
|
|
4262
|
+
(candidate) =>
|
|
4263
|
+
candidate.instanceId === follower.instanceId ||
|
|
4264
|
+
(!!follower.profileKey &&
|
|
4265
|
+
candidate.profileKey === follower.profileKey) ||
|
|
4266
|
+
(!!follower.target &&
|
|
4267
|
+
candidate.target?.chatId === follower.target.chatId &&
|
|
4268
|
+
candidate.target?.threadId === follower.target.threadId),
|
|
4269
|
+
);
|
|
3389
4270
|
let settled = false;
|
|
3390
4271
|
try {
|
|
3391
4272
|
await runFollowerMutation(follower, async () => {
|
|
3392
|
-
const isDetached = () =>
|
|
3393
|
-
|
|
4273
|
+
const isDetached = () =>
|
|
4274
|
+
isCurrent() &&
|
|
4275
|
+
(!observation || observation.isCurrent()) &&
|
|
4276
|
+
!findReplacement() &&
|
|
4277
|
+
deps.isFollowerProcessAlive?.(follower.pid!) === false;
|
|
3394
4278
|
if (!isDetached()) return;
|
|
3395
|
-
if (cleanupEnabled === true)
|
|
3396
|
-
|
|
4279
|
+
if (cleanupEnabled === true)
|
|
4280
|
+
await deps.onFollowerConfirmedDead!(follower);
|
|
4281
|
+
else
|
|
4282
|
+
settled = await deps.onFollowerConfirmedDeadPreserved!(
|
|
4283
|
+
follower,
|
|
4284
|
+
isDetached,
|
|
4285
|
+
observation!.operationId,
|
|
4286
|
+
);
|
|
3397
4287
|
});
|
|
3398
4288
|
if (!cleanupEnabled) recordPreserved();
|
|
3399
4289
|
} catch (error) {
|
|
3400
4290
|
recordPruneEvent(error, {
|
|
3401
|
-
phase: cleanupEnabled
|
|
3402
|
-
|
|
3403
|
-
|
|
4291
|
+
phase: cleanupEnabled
|
|
4292
|
+
? "follower-confirmed-dead-cleanup"
|
|
4293
|
+
: "follower-confirmed-dead-preserve",
|
|
4294
|
+
instanceId: follower.instanceId,
|
|
4295
|
+
pid: follower.pid,
|
|
4296
|
+
chatId: follower.target?.chatId,
|
|
4297
|
+
threadId: follower.target?.threadId,
|
|
3404
4298
|
});
|
|
3405
4299
|
} finally {
|
|
3406
|
-
if (
|
|
4300
|
+
if (
|
|
4301
|
+
settled ||
|
|
4302
|
+
cleanupEnabled === true ||
|
|
4303
|
+
(observation && !observation.isCurrent())
|
|
4304
|
+
) {
|
|
3407
4305
|
forgetPreservation(observation);
|
|
3408
4306
|
}
|
|
3409
4307
|
}
|
|
@@ -3435,7 +4333,28 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
3435
4333
|
}, followerPruneIntervalMs);
|
|
3436
4334
|
pruneInterval.unref?.();
|
|
3437
4335
|
};
|
|
4336
|
+
const selectedMenuDelivery = deps.selectedMenuDelivery,
|
|
4337
|
+
selectedMenuProtocol = deps.protocolIdentity,
|
|
4338
|
+
selectedMenuRegistry = deps.followerRegistry,
|
|
4339
|
+
selectedMenuSecret = deps.authSecret;
|
|
3438
4340
|
const handleEnvelope = createTelegramBusLeaderEnvelopeHandler({
|
|
4341
|
+
selectedMenuDelivery: selectedMenuDelivery
|
|
4342
|
+
? (input, isCallerCurrent) => {
|
|
4343
|
+
const generation = pruneGeneration;
|
|
4344
|
+
return selectedMenuDelivery.call(
|
|
4345
|
+
deps,
|
|
4346
|
+
input,
|
|
4347
|
+
() =>
|
|
4348
|
+
!!pruneInterval &&
|
|
4349
|
+
pruneGeneration === generation &&
|
|
4350
|
+
deps.selectedMenuDelivery === selectedMenuDelivery &&
|
|
4351
|
+
deps.protocolIdentity === selectedMenuProtocol &&
|
|
4352
|
+
deps.followerRegistry === selectedMenuRegistry &&
|
|
4353
|
+
deps.authSecret === selectedMenuSecret &&
|
|
4354
|
+
isCallerCurrent(),
|
|
4355
|
+
);
|
|
4356
|
+
}
|
|
4357
|
+
: undefined,
|
|
3439
4358
|
followerRegistry: deps.followerRegistry,
|
|
3440
4359
|
authSecret: deps.authSecret,
|
|
3441
4360
|
protocolIdentity: deps.protocolIdentity,
|
|
@@ -3446,11 +4365,13 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
3446
4365
|
resolveAgentTarget: deps.resolveAgentTarget,
|
|
3447
4366
|
routeAgentMessage: deps.routeAgentMessage,
|
|
3448
4367
|
routeQueueHandoff: deps.routeQueueHandoff,
|
|
3449
|
-
provisionFollowerTarget: deps.provisionFollowerTarget
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
4368
|
+
provisionFollowerTarget: deps.provisionFollowerTarget
|
|
4369
|
+
? (registration, options) => {
|
|
4370
|
+
// Same-instance ownership can change in the store before registry publication.
|
|
4371
|
+
forgetPreservation(pendingPreservations.get(registration.instanceId));
|
|
4372
|
+
return deps.provisionFollowerTarget!(registration, options);
|
|
4373
|
+
}
|
|
4374
|
+
: undefined,
|
|
3454
4375
|
commitFollowerRegistration: deps.commitFollowerRegistration,
|
|
3455
4376
|
onFollowerDisconnected: deps.onFollowerDisconnected,
|
|
3456
4377
|
renameFollowerThread: deps.renameFollowerThread,
|
|
@@ -3459,8 +4380,10 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
3459
4380
|
settleFollowerSessionReplacement: deps.settleFollowerSessionReplacement,
|
|
3460
4381
|
getFollowerDisplayTitle: deps.getFollowerDisplayTitle,
|
|
3461
4382
|
onFollowerRegistered: deps.onFollowerRegistered,
|
|
3462
|
-
onWorkspaceRestoreRecipientObserved:
|
|
3463
|
-
|
|
4383
|
+
onWorkspaceRestoreRecipientObserved:
|
|
4384
|
+
deps.onWorkspaceRestoreRecipientObserved,
|
|
4385
|
+
getWorkspaceRestoreObservationGeneration: () =>
|
|
4386
|
+
pruneInterval ? pruneGeneration : undefined,
|
|
3464
4387
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
3465
4388
|
applyThreadDisplayMode: deps.applyThreadDisplayMode,
|
|
3466
4389
|
getThreadDisplayMode: deps.getThreadDisplayMode,
|
|
@@ -3472,7 +4395,9 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
3472
4395
|
runWithWorkspaceCapacity: deps.runWithWorkspaceCapacity,
|
|
3473
4396
|
});
|
|
3474
4397
|
const routeQueueHandoffEnvelope = async (
|
|
3475
|
-
input: Parameters<
|
|
4398
|
+
input: Parameters<
|
|
4399
|
+
TelegramBusLeaderRuntime<TContext>["routeQueueHandoff"]
|
|
4400
|
+
>[0],
|
|
3476
4401
|
): Promise<TelegramBusEnvelope> => {
|
|
3477
4402
|
const recipient = deps.followerRegistry.get(input.recipientInstanceId);
|
|
3478
4403
|
if (
|
|
@@ -3485,24 +4410,19 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
3485
4410
|
TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF,
|
|
3486
4411
|
)
|
|
3487
4412
|
) {
|
|
3488
|
-
return
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
message: "Telegram queue handoff capability was not negotiated.",
|
|
3493
|
-
};
|
|
4413
|
+
return rejectTelegramBusRequest(
|
|
4414
|
+
input.requestId,
|
|
4415
|
+
"Telegram queue handoff capability was not negotiated.",
|
|
4416
|
+
);
|
|
3494
4417
|
}
|
|
3495
4418
|
if (
|
|
3496
4419
|
!recipient?.registrationGeneration ||
|
|
3497
|
-
input.recipientRegistrationGeneration !==
|
|
3498
|
-
recipient.registrationGeneration
|
|
4420
|
+
input.recipientRegistrationGeneration !== recipient.registrationGeneration
|
|
3499
4421
|
) {
|
|
3500
|
-
return
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
message: "Stale Telegram queue handoff recipient registration generation.",
|
|
3505
|
-
};
|
|
4422
|
+
return rejectTelegramBusRequest(
|
|
4423
|
+
input.requestId,
|
|
4424
|
+
"Stale Telegram queue handoff recipient registration generation.",
|
|
4425
|
+
);
|
|
3506
4426
|
}
|
|
3507
4427
|
const recipientSocketPath =
|
|
3508
4428
|
recipient.busSocketPath ??
|
|
@@ -3562,8 +4482,7 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
3562
4482
|
handleEnvelope,
|
|
3563
4483
|
});
|
|
3564
4484
|
return {
|
|
3565
|
-
routeQueueHandoff: (envelope) =>
|
|
3566
|
-
routeQueueHandoffEnvelope(envelope),
|
|
4485
|
+
routeQueueHandoff: (envelope) => routeQueueHandoffEnvelope(envelope),
|
|
3567
4486
|
startPolling: async (ctx) => {
|
|
3568
4487
|
// Replay durable cleanup before publishing the follower endpoint so a
|
|
3569
4488
|
// replacement registration cannot reclaim a target while it is deleted.
|